可执行文件
- windows 使用的是 PE 可执行文件
-
- 由 DOS 头,PE 文件头,节表及各节数据组成
- 如果需要引用外部的动态链接库,则有导入表
- 如果自己可以提供函数给其他程序来动态链接(DLL 文件),则有导出表
- Linux 使用的是 ELF 可执行文件
-
- 由 ELF 头,各节数据,节表,字符串段,符号表组成
- 都是由 COFF 格式发展而来,文件结构各种概念非常相似
-
- 节是程序中各部分的逻辑划分,
- .text 或.code 代表代码节,.data 代表数据节
- 运行时,可执行文件的各节被加载到内存的各位置,为方便管理和节省开销,一个或多个节会被映射到一个段中
- 段的划分是根据这部分内存需要的权限(读、写、执行)来进行的。如果在相应的段内进行了非法操作,如在只读代码段中进行了写操作,就会产生段错误
汇编语言基本知识
寄存器
寄存器是 CPU 的一部分,有限存储容量,高速存储部件,暂存指令,数据,地址
ESP 栈顶寄存器,EBP 栈底寄存器
汇编语言常用指令
x86/x64 常用指令
指令类型 | 操作码 | 指令示例 | 对应作用 |
数据传送指令 | mov | mov rax,rbx | rax=rbx |
mov qword ptr [rdi],rax | *(rdi)=rax | ||
取地址指令 | lea | lea rax,[rsi] | rax=&*(rsi) |
算术运算指令 | add | add rax,rbx | rax+=rbx |
add qword ptr [rdi],rax | *(rdi)+=rax | ||
sub | sub rax,rbx | rax-=rbx | |
逻辑运算指令 | and | and rax,rbx | rax&=rbx |
xor | xor rax,rbx | rax^=rbx | |
函数调用指令 | call | call 0x401000 | 执行函数 |
函数返回指令 | ret | ret | 函数返回 |
比较指令 | cmp | cmp rax,rbx | 根据比较的结果改变标志位 |
无条件跳转指令 | jmp | jmp 0x401000 | 跳到指定地址 |
栈操作指令 | push | push rax | 将 rax 的值压入栈中 |
pop | pop rax | 从栈上弹出一个元素放入 rax |
qword ptr
:这表示要移动的是 64 位(8 字节)的数据。"Qword" 是 "quadword" 的缩写,表示 64 位。[rdi]
:这是rdi
寄存器所指向的内存地址。在 x86-64 中,rdi
通常保存传递给函数的第一个参数,因此它通常指向一个内存位置。rax
:这是一个 64 位寄存器,用来存储要移动的数据。&=
是一个位运算赋值操作符。它表示将rax
和rbx
中的值进行按位与操作,并将结果赋值给rax
。按位与(AND)操作:对于两个数字的每一位,如果两个对应位都是 1,则结果为 1;否则,结果为 0。也就是说,只有当rax
和rbx
对应位的值都是 1 时,结果才会是 1。
rax = 0b1101
rbx = 0b1011
----------------
result = 0b1001 (按位与操作后的结果)
^=
是一个位运算赋值操作符。它表示将rax
和rbx
中的值进行按位异或操作,并将结果赋值给rax
。按位异或(XOR)操作:对于两个数字的每一位,如果对应位的值不同,则结果为 1;如果相同,则结果为 0。也就是说,当rax
和rbx
对应位的值不相同时,结果为 1;相同则为 0。
rax = 0b1101
rbx = 0b1011
----------------
result = 0b0110 (按位异或操作后的结果)
反汇编
冯·诺依曼架构模糊了代码与数据的界限,在代码节中可能穿插跳转表,常量池(ARM),普通常量数据,恶意的干扰数据。所以无法简单直接地一条条连续地向下解析指令,需要知道正确的指令的起始位置(如 label),来指引反汇编工具正确解析代码
汇编过程中,label 信息会丢失,因为 label 标识跳转位置,决定程序执行时可能执行到的位置,即汇编语句的起始位置,所以需要还原正确的 label
还原程序的流程有两种算法:
- 线性扫描反汇编算法:从代码的起始及位置一个接一个地解析指令,缺点是:如果有数据插入到代码段中,后续的所有反汇编的结果都是错误无用
- 递归下降反汇编算法:尝试推测每条指令后程序将如何执行。无条件跳转指令会立即跳转到目标位置。引擎先将一些已知的模式匹配到起始位置,再根据指令的执行模式,逐个对程序执行情况进行跟踪,最后将程序完全反汇编
调用约定
x86 32 位架构的调用约定
- __cdecl:参数从右向左压入栈中,调用完毕,由调用者负责将这些压入栈中的参数清理掉。返回值置于 EAX。绝大多数 x86 平台的 c 语言程序都是用这种约定
- __stdcall:参数从右向左压入栈中,调用完毕,由被调用者负责将这些压入栈中的参数清理掉。返回值置于 EAX。Windows 很多 API 都是用这种方式提供
- __thiscall:为类方法专门优化的调用约定,将类方法的 this 指针放在 ECX 中,然后将其余参数压入栈中
- __fastcall:为加速调用而生,将第一个参数放到 ECX 中,将第二个参数放入 EDX 中,然后将后续的参数从右至左压入栈中
x86 64 位架构的调用约定
- Microsoft x86:Windows 上使用
- SystemV x86:Linux、MacOS 上使用
局部变量
每次函数被调用时,程序从栈上分配一段空间,作为存储局部变量的区域,同时产生存储返回地址的区域和参数的区域。分配的这段区域就叫栈帧.
一个函数中每个局部变量相对于该函数栈帧的偏移都是固定的,所以引入一个寄存器专门存储当前栈帧的位置,即 ebp 帧指针
函数初始化阶段赋值 ebp 为栈帧中间的某个位置,这样可以用 ebp 引用所有的局部变量。
由于上一层的父函数也要使用 ebp,因此函数开始时先保存 ebp,再赋值 ebp 为自己栈帧的值
push ebp
mov ebp,esp
现在每个函数的栈帧便由局部变量、父栈帧的值、返回地址、参数四部分构成
ebp 再初始化后实际上指向的是父栈帧地址的存储位置,*ebp 形成了一个链表,代表一层层的函数调用链
编译器可以通过跟踪计算每个指令执行时栈的位置,从而直接越过 ebp 来引用局部变量,而使用栈指针 esp 来引用局部变量