Skip to main content

x64架构汇编

掌握汇编语言对于恶意软件开发有着很大的作用,例如可以编写自定义 Shellcode、在木马加载器中插入汇编代码以实现混淆以及底层的指令操作等。


基本概念

汇编语言是我们可以用来为给定 CPU 编写程序的最底层的编程语言,汇编可以被翻译为 CPU 操作码,即 CPU 可以直接执行的机器码。 通常,汇编指令与操作码具有 1:1 的关系,但在 C 等高级语言中情况并非如此,它们有多种方法将书面代码编译或转换为机器代码。接下来,我们分别来讨论汇编中设计的名词与概念。


字节顺序

字节顺序指的是数据在计算机内存中的存储方式。大端是指数据的最高有效字节 (最左端) 存储在低的内存地址中,最低有效位存储在高的内存地址中。字节顺序只适用于字节,而非

image.png

如上图所示,0x11223344 的 4 个字节 0x11,0x22,0x33,0x44 分别存储在由低往高的内存地址中。

小端则正好相反,如下图所示,0x11223344 的 4 个字节 0x11,0x22,0x33,0x44 分别存储在由高往低的内存地址中。

image.png

因为小段运用更多,请尝试理解下图:

image.png


有符号与无符号数字

如果存储一个无符号的数,那么我们不需要指定其正负符号,那么该数的范围为 0 到 2^64 -1 。但如果要存储一个有符号的数,因为要额外留出一位存储正负符号,那么该数的范围为 -2^63 2^63-1

计算一个数的负数形式,有 2 个步骤:翻转所有位,再加上 1。以 42 为例,过程如下:

42:    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1010
翻转:  1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1101 0101
加1:    1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1101 0110



CPU 寄存器

由于访问内存 (RAM) 对于 CPU 来说通常是一个缓慢的过程,因此处理器内总是包含许多寄存器,这些寄存器是处理器内部的小型存储位置,可以非常快速地访问数据。在 64 位 x64 处理器上,寄存器可以保存 64 位8 字节。 让我们分别查看一下如下的常用寄存器:

寄存器名称
作用
备注
RIP
指令寄存器,指向要被执行的下一条指令的地址 只读,不能拆分
RAX
累加寄存器,用于算术运算,I/O 操作,存储函数返回值等
通用寄存器。在 Windows syscall 中保存 SSN
RBX
基址寄存器,用作数据指针
通用寄存器
RCX
计数寄存器,常用于循环中的计数器
通用寄存器
RDX
数据寄存器,用于算数运算和 I/O 操作,以及扩展精度结果
通用寄存器
RSI
源索引,通常用作字符串操作中的输入字符串的指针
通用寄存器
RDI
目标索引,通常用作字符串操作中的输出字符串的指针
通用寄存器
RBP
栈帧指针,指向栈帧的基址,配合偏移定位变量
通用寄存器
RSP
栈指针,指向栈的顶部
通用寄存器
R8-R15
额外的通用寄存器
通用寄存器
RFLAGS
FLAG 寄存器,存储着一系列标志位,揭示操作的结果,例如两数的数值大小比较。

在 x64 处理器中,每个寄存器是 8 字节,但可以被分为更小的部分以及被直接饮用。例如,RAX 的后 4 字节为 EAX,EAX 的后 2 字节为 AX 等,如下图所示。AH 与 AL 中的 A 与 H 分别表示 High 和 Low。RIP 不可被拆分。

image.png

寄存器 R8 也可以被类似的方式细分:

image.png

当我们使用后 32 位的时候,例如 EAX,那么前面的 32 位被全部填充了 0。但当我们使用后 8 或者 16 位的时候,却不是这样。

对于通用寄存器,各个部分的名称如下表所示:

64 位寄存器 低 32 位
低 16 位
低 8 位
rax eax ax al
rbx ebx bx bl
rcx ecx cx cl
rdx edx dx dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b


以及讨论一下 RFLAGS 寄存器,该寄存器包含了一系列的标志位,每个标志位都有特定的含义,用于反映操作的结果,例如两数大小的比较。以下是一些最常见和重要的位:


标签
描述
0
CF
进位标志
2
PF
奇偶校验标志
6
ZF
零标志
7
SF
符号标志
11
OF
溢出标志

在计算机体系结构中,栈是一个自动增长与收缩内存区域,允许临时存储数据,其中数据以后进先出 (LIFO) 方式添加或删除。在大多数现代计算机系统中,每个线程都有一个保留的内存区域,即栈。栈与内存的分配、函数的调用息息相关,我们来了解一下栈的一些特性以及关联的名词概念:

增长方向:栈从上往下、由高地址往低地址增长

栈帧:当有函数被调用的时候,栈中的一片内存区域被划分为栈帧,包含了函数的局部变量函数参数返回地址等部分。每当新的函数被调用,会创建新的栈帧并且处于栈的最顶层。

参数传递:在 Windows x64 (Linux x64 下的调用约定与 Windows 的有所不同) 下的调用约定中,函数调用的前 4 个参数通常使用寄存器 RCX、RDX、R8、R9 来传递,如果参数超过 4 个,则剩余参数将在栈上传递。

参数归位:在 x64 Windows 调用约定中,前四个参数通过寄存器 RCX、RDX、R8、R9 传递,参数归位是指在函数调用开始时将这些寄存器参数复制到栈上的操作,这样做是为了允许被调用的函数修改这些值而不影响原始参数。即使函数的参数少于 4 个,4 个参数的空间依旧会在栈帧上被预留。

易失性寄存器:这些寄存器不会在函数调用之间保留,在 Windows x64 调用约定中,RAX、RCX、RDX、R8、R9、R10、R11 被视为易失性的。

非易失性寄存器:这些是在函数调用期间保留的寄存器。 在 Windows x64 调用约定中,RBX、RBP、RDI、RSI、RSP、R12、R13、R14、R15 被视为非易失性的,如果函数使用其中任何一个,它必须保存原始值并在函数返回之前恢复它。

函数序言:与 x86 调用约定不同,x64 fastcall 调用约定限制了在函数序言与尾声之间使用 PUSH/POP 指令,因此 RSP 是不变的。函数序言是函数的初始部分,为局部变量设置栈空间,为保存的寄存器值分配空间,调整栈指针。

函数尾声:函数中最后的用于执行清理任务的部分,撤消了函数序言部分的所做工作,即释放局部变量的栈空间,恢复保存的寄存器值,并将栈指针重置为调用之前的值。

函数调用:当函数被调用时,函数的返回地址将在参数传递后被压入栈 (当前的栈顶)。当函数执行完毕后,RIP 指向调用该函数指令的下一条指令。

局部变量:函数中的局部变量通常存储在栈中,处在比函数参数更高的地址处。当函数完成执行时,这些变量将从堆栈中删除。

栈指针 (RSP):该寄存器指向栈顶,因为栈向低地址方向增长,当有新的元素入栈,RSP 会指向内存更低的地址。相反,当有元素出栈后,RSP 会指向内存更高的地址。

栈帧指针 (RBP):该寄存器指向当前栈帧的基址,用于引用局部变量以及函数参数。但是在 x64 架构下,因为 RSP 是可预测的,所以主要使用 RSP 对局部变量以及函数参数进行引用。

 

综上,如果在函数 A 中调用函数 B,那么栈的结构如图所示:

image.png

数据尺寸

我们会接触到的常见数据类型以及所占字节数如下所示:

名称
字节数
BYTE
1
WORD
2
DWORD
4
QWORD
8



常见汇编指令

值操作

mov

lodsb:从 RSI 中取下一个字节传递给 AL

cdq:清空 RDX 为 NULL


比较

test

cmp

jxx

栈操作

push:保存所有寄存器

pushad

pop

popad:恢复寄存器

取址

lea

基本运算

inc

dec

add

sub

mul

div

neg

位操作

and

or

xor

not

sal

sar

shi

sjr

rol

ror

跳转

jmp

jxx

jexcz: 如果 RAX 为 0,那么跳转到最后。

函数调用与返回

call

ret

其他

stosx

int3

nop

repe scasd:重复对比 RAX 与 RDI