汇编语言
pwn.college 网络安全学院 --- pwn.college
寄存器
正经理解
| 寄存器 | 主要用途 | 编号 | 存储数据的范围 |
|---|---|---|---|
| EAX | 累加器 | 0 | 0 - 0xFFFFFFFF |
| ECX | 计数 | 1 | 0 - 0xFFFFFFFF |
| EDX | I/O指针 | 2 | 0 - 0xFFFFFFFF |
| EBX | DS段的数据指针 | 3 | 0 - 0xFFFFFFFF |
| ESP | 堆栈指针 | 4 | 0 - 0xFFFFFFFF |
| EBP | SS段的数据指针 | 5 | 0 - 0xFFFFFFFF |
| ESI | 字符串操作的源指针;SS段的数据指针 | 6 | 0 - 0xFFFFFFFF |
| EDI | 字符串操作的目标指针;ES段的数据指针 | 7 | 0 - 0xFFFFFFFF |
| 寄存器 | 编号(二进制) | 编号(十进制) | ||
|---|---|---|---|---|
| 32位 | 16位 | 8位 | ||
| EAX | AX | AL | 000 | 0 |
| ECX | CX | CL | 001 | 1 |
| EDX | DX | DL | 010 | 2 |
| EBX | BX | BL | 011 | 3 |
| ESP | SP | AH | 100 | 4 |
| EBP | BP | CH | 101 | 5 |
| ESI | SI | DH | 110 | 6 |
| EDI | DI | BH | 111 | 7 |
| 寄存器 | 32 位 (IA-32) | 64 位 (x86-64) |
|---|---|---|
| 通用寄存器 | EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP | RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, R8-R15 |
| 段寄存器 | CS, DS, SS, ES, FS, GS | CS, DS, SS, ES, FS, GS |
| 控制寄存器 | EFLAGS, EIP | RFLAGS, RIP |
| 指针寄存器 | ESP, EBP | RSP, RBP |
个人理解
RIP
存放下一条指令的偏移地址
RSP
存放当前栈帧的栈顶偏移地址
RBP
存放当前栈帧的栈底偏移地址
RAX
通用寄存器。存放函数的返回值
寄存器的关系
MSB LSB
+----------------------------------------+
| rax |
+--------------------+-------------------+
| eax |
+---------+---------+
| ax |
+----+----+
| ah | al |
+----+----+
汇编
实际中执行指令时,EIP已经移动到下一行了
标志位
更新标志位的操作:
- 大多数算术指令。
- 比较指令cmp(相减,但丢弃结果)。
- 比较指令test(想与,但丢弃结果)。
mov
mov 指令要求目标寄存器必须是 rax、rbx、rcx、rdx、r8、r9、r10、r11、r12、r13、r14 或 r15 中的一个。
在 x86-64 汇编中,不能直接将大于 1 字节的数据(如 qword 或 dword)直接存储到内存地址。必须先将数据加载到寄存器中,然后再通过 qword ptr、dword ptr 等操作符将寄存器中的值存储到指定的内存地址。
所以想要执行 [rdi] = 0xdeadbeef00001337 [rsi] = 0xc0ffee0000操作就要先赋值给额外的寄存器,再把额外的寄存器给目标寄存器的地址。
而不是赋值给地址再把地址给目标寄存器的地址
mov r8,0xdeadbeef00001337
mov r9,0xc0ffee0000
mov [rdi],r8
mov [rsi],r9
add
add 指令的两个操作数必须满足以下条件之一:
- 两个操作数都是寄存器。
- 一个操作数是寄存器,另一个操作数是立即数。
- 一个操作数是寄存器,另一个操作数是内存地址。
但不能是:
- 一个操作数是寄存器,另一个操作数是内存地址的直接内容。
也就是如果要实现add rsi, [rdi+8]
就要先将内存中的值加载到一个寄存器中,然后再进行加法操作。
; 将 rdi + 8 处的四字数据加载到 rbx
mov rbx, [rdi + 8]
; 将 rbx 的值加到 rsi 中
add rsi, rbx
mul/imul
表示乘法
imul rax rdx rax*=rdx
div
表示除法
div 是一种特殊指令,可以在一个寄存器操作数的情况下,将 128 位被除数除以 64 位除数,同时存储商和余数。
对于div rag等效于
rax = rdx:rax / regrdx = remainder
rdx:rax 表示 rdx 将是 128 位被除数的最高 64 位,而 rax 将是 128 位被除数的最低 64 位。
也就是计算完成后rax存放商,rdx存放余数
所以在调用 div 之前必须小心查看 rdx 和 rax 中的内容。
rep
rep 前缀会重复执行后续的字符串操作指令,重复次数由 ECX 寄存器指定。每次执行后,ECX 减 1,直到 ECX 为 0 时停止。
rep <字符串操作指令>
以下是 rep 常见的组合指令及其作用:
| 指令 | 功能描述 |
|---|---|
rep movsb |
按字节从源地址 (DS:ESI) 复制到目标地址 (ES:EDI),重复 ECX 次 |
rep movsw |
按字(2字节)复制,地址每次增减 2 |
rep movsd |
按双字(4字节)复制,地址每次增减 4 |
rep stosb |
将 AL 的值填充到 ES:EDI 指向的内存,重复 ECX 次 |
rep stosw |
将 AX 的值填充到内存,地址每次增减 2 |
rep stosd |
将 EAX 的值填充到内存,地址每次增减 4 |
rep cmpsb |
逐字节比较 DS:ESI 和 ES:EDI 的内容,直到不匹配或 ECX 归零 |
rep scasb |
在 ES:EDI 指向的内存中搜索 AL 的值,直到找到或 ECX 归零 |
字符串操作指令的地址增减方向由 方向标志位(DF) 决定:
cld指令:清除方向标志(DF=0),使地址递增(ESI/EDI增加)。std指令:设置方向标志(DF=1),使地址递减(ESI/EDI减少)。
cmp
| 语句 | cmp a1,0x61 | sub a1,0x61 |
|---|---|---|
| 本质 | a1-0x61 | a1=a1-0x61 |
| 不同 | a1不会赋值 | a1会赋值 |
cmp只会判断减去后值是不是为0
lea
把寄存器存储的地址进行加减操作后赋值给另外一个寄存器
lea rax,[rbp-0x18] :rax=rbp-0x18
为什么不用sub
- 如果用sub,需要sub rbp,0x18;mov rax,rbp;add rbp,0x18,指令太长了。
- mov rax,[rbp-0x18]会直接把[rbp-0x18]存储的信息赋值给rax,而不是地址
xor
xor eax,eax:让eax等于0
不用mov ebx 0
xor eax,eax短
test
test eax,eax => cmp eax,0 exa=0 -> 0 eax!=0 -> !0
相当于and eax,eax,但是不赋值给eax
test rax, rax; jnz STAY_LEET # rax != 0
push
push ebp : esp-4,把ebp放入esp所指的位置
pop
pop ebp :把esp指向的地方赋给ebp,esp+4。esp往下挪
push和pop其他相关指令
pusha:将所有的16位通用寄存器压入堆栈
popa:将所有的16位通用寄存器取出堆栈
pushf::将的16位标志寄存器EFLAGS压入堆栈
popf:将16位标志寄存器EFLAGS取出堆栈
pushad:将所有的32位通用寄存器压入堆栈,方面后面随意使用寄存器,用于保护现场
jump
junp 0x11111 -> mov eip 0x11111
绝对跳跃
方式 1:通过寄存器实现绝对跳转
这种方式是将目标地址先放入一个寄存器,然后使用 jmp 指令跳转到该寄存器中的地址。这种方式在某些情况下(比如目标地址是动态计算的)非常有用。
方式 2:直接在 jmp 指令中使用绝对地址
这种方式是直接在 jmp 指令中指定目标地址。这种方式更简洁,适用于目标地址是已知的常量的情况。
相对跳跃
相对跳转到0x51字节字后的位置
.intel_syntax noprefix
.global _start
_start:
jmp .target
.rept 0x51 ;.rept指令,重复以下指令0x51次
nop
.endr
.target:
mov rax,0x1
call
call : push call的下一条指令(返回地址); jump func;
leave
leave : mov esp,ebp;pop ebp;
把esp移到ebp的位置,ebp再到此时栈顶(esp)的地址上,然后esp再加4。也就是往下挪
ret
pop eip ; 从栈中弹出返回地址并存入 EIP
到ret命令时,esp在哪,就把哪里的地址存储的参数给eip
把esp给eip,同时esp加4
对应关系
push ebp; --- pop ebp;
push ebp;pop ebp; --- leave
call; --- ret
cdq/cdqe
cdq是32位,cdqe是64位
作用都是将一个32位有符合数扩展为64位有符合数,数据能表示的数不变
具体是这样实现的,比如eax=fffffffb(值为-5),然后cdq把eax的最高位bit,也就是二进制1,全部复制到edx的每一个bit位,EDX 变成 FFFFFFFF,这时eax与edx连起来就是一个64位数,FFFFFFFF FFFFFFFB ,它是一个 64 bit 的大型数字,数值依旧是 -5。
传参
64函数调用传参
rdi->rsi->rdx->rcx->r8->r9
系统调用参数
rax:系统调用号,返回值
rdi->rsi->rdx->r10->r8->r9
字节大小
- Quad Word = 8 Bytes = 64 bits
- Double Word = 4 bytes = 32 bits
- Word = 2 bytes = 16 bits
- Byte = 1 byte = 8 bits
地址表示
段寄存器:在 x86 架构中,内存地址是由 段寄存器 和 偏移量 共同组成的。
默认段寄存器的规则
在 x86 汇编中,内存操作数的默认段寄存器取决于基址寄存器:
- 如果使用
ebp或esp作为基址寄存器,默认段寄存器是ss(栈段)。 - 如果使用其他寄存器(如
eax、ebx等),默认段寄存器是ds(数据段)。
比如,对于 [ebp-4],默认段寄存器是 ss,而不是 ds。如果你明确想访问 ds 段中的数据,则需要加上 ds: 前缀。
对于dword ptr ds:[ebp-4]
ds是段寄存器
是否可以去掉 ds:?
- 大多数情况下可以去掉:
- 在 现代操作系统(如 Windows、Linux)的 保护模式 下,
ds(数据段寄存器)和ss(栈段寄存器)的值 通常是相同的。这是因为现代操作系统使用 平坦内存模型(Flat Memory Model),所有段寄存器都被设置为指向同一个线性地址空间。 - 因此,
ds:通常是默认的,可以省略不写。
- 在 现代操作系统(如 Windows、Linux)的 保护模式 下,
- 某些情况下不能去掉:
- 如果程序中使用了多个段(例如在实模式或某些特殊场景下),则需要明确指定段寄存器(如
ds:、cs:、es:等)。 - 如果汇编器无法确定默认的段寄存器,则需要显式加上
ds:
- 如果程序中使用了多个段(例如在实模式或某些特殊场景下),则需要明确指定段寄存器(如
位置无关代码(PIC)
拿这个创建socket然后bind的汇编语言的lea rsi, [rip+sockaddr]代码举例子
.intel_syntax noprefix
.globl _start
.section .text
_start:
# Socket syscall
mov rdi, 2
mov rsi, 1
mov rdx, 0
mov rax, 0x29
syscall
# Bind syscall
mov rdi, 3
lea rsi, [rip+sockaddr]
mov rdx, 16
mov rax, 0x31
syscall
# Exit syscall
mov rdi, 0
mov rax, 0x3c
syscall
.section .data
sockaddr:
.2byte 2
.2byte 0x5000
.4byte 0
.8byte 0
为什么要使用
-
位置无关代码(PIC)的需求
现代操作系统和程序(尤其是共享库、动态链接库)通常需要支持 位置无关代码。这意味着代码可以被加载到内存中的任意位置运行,而不需要修改代码中的地址。
- 如果直接使用绝对地址(例如
mov rsi, sockaddr),代码只能在固定的内存地址运行。 - 使用
rip + sockaddr的相对寻址方式,代码可以在内存中的任何位置运行,因为地址是动态计算的。
- 如果直接使用绝对地址(例如
-
动态加载和共享库
在动态链接库(
.so文件)或共享库中,代码和数据的内存地址在编译时是未知的,只有在运行时才能确定。使用rip + sockaddr的方式,可以避免硬编码绝对地址,从而支持动态加载。 -
安全性
现代操作系统通常使用 地址空间布局随机化(ASLR) 技术,将程序的代码和数据加载到随机的内存地址,以增加安全性。如果代码中使用了绝对地址,ASLR 将无法正常工作。而使用
rip + sockaddr的相对寻址方式,可以兼容 ASLR。
相对寻址
原理
在 x86-64 汇编中,rip + offset 是一种相对寻址方式。它的计算方式是:
实际地址 = 下一条指令的地址 + offset
这里的 offset 是一个固定的偏移量,表示从下一条指令的地址到目标地址的距离。
在这个例子中,汇编器和链接器会根据 sockaddr 的地址和 rip 的值,计算出正确的偏移量,并将其编码到指令中。
rip是指令指针寄存器,指向下一条指令的地址。sockaddr是一个标签,表示sockaddr结构体的地址。rip + sockaddr中的sockaddr实际上是一个偏移量,表示从下一条指令的地址到sockaddr地址的距离。
当 CPU 执行指令时,会根据 rip 的值和偏移量动态计算出 sockaddr 的地址。
因此,即使 rip 的值在每个地方都不一样,rip + sockaddr 仍然能够正确计算出 sockaddr 的地址。
总结
开启 ASLR 时,需要使用 rip + offset 动态计算地址。
未开启 ASLR 时,可以直接使用绝对地址。

浙公网安备 33010602011771号