寄存器
8086 16 位
通用寄存器
寄存器名称 | 主要用途 | 补充说明 |
---|---|---|
AX | 累加器,常用于算术和 I/O 操作 | 可拆分为 AH(高 8 位)和 AL(低 8 位)单独使用 |
BX | 基址寄存器,可作为数据指针指向数据段 | 可用来存放内存地址,方便对数据段内的数据进行访问 |
CX | 计数寄存器,用于循环控制 | |
DX | 数据寄存器,常用于 I/O 端口地址和扩展乘除运算 | 在 I/O 操作中可存放端口号 |
段寄存器
寄存器名称 | 主要用途 | 补充说明 |
---|---|---|
CS | 代码段寄存器,指向当前执行代码所在的段 | 与指令指针 IP 配合,确定下一条要执行指令的物理地址 |
DS | 数据段寄存器,指向当前数据所在的段 | 多数数据访问指令默认使用 DS 作为段寄存器 |
ES | 附加段寄存器,常用于字符串操作的目标段 | 在字符串操作指令中,可作为目的地址的段寄存器 |
SS | 堆栈段寄存器,指向当前堆栈所在的段 | 与堆栈指针 SP 配合,管理堆栈的操作 |
x86 32 位
通用寄存器
寄存器 | 主要用途 | 补充说明 |
---|---|---|
EAX | 累加器 | |
EBX | 基址 | |
ECX | 计数 | 常用于控制 REP 等指令的重复次数 |
EDX | I/O指针 | |
ESI | 源变址 | 在字符串操作指令中指向源字符串 |
EDI | 目的变址 | 在字符串操作指令中指向目标字符串 |
ESP | 堆栈指针,指向栈顶 | 栈操作(如 PUSH 和 POP )会自动更新 ESP |
EBP | 基址指针,指向当前栈帧的基址 | 用于访问栈帧内的局部变量和函数参数 |
MOV 目标操作数,源操作数
作用:拷贝源操作数到目标操作数
- 源操作数可以是立即数、通用寄存器、段寄存器或者内存单元.
- 目标操作数可以是通用寄存器、段寄存器或者内存单元.
- 操作数的宽度必须一样.
- 源操作数和目标操作数不能为同一目标单元.
函数
函数入口
push 1 * 函数参数
push 2
call _func * 调用函数
汇编中的函数
; 函数调用前
; ESP 指向栈顶
; EBP 可能指向之前栈帧的基地址
; 函数调用开始
PUSH EBP ; 保存当前 EBP 的值到栈中
MOV EBP, ESP ; 将 EBP 指向当前栈帧的基地址
SUB ESP, 10H ; 为局部变量分配空间,ESP 减小
; 在函数内部
; 可以通过 [EBP + 偏移量] 访问参数
; 可以通过 [EBP - 偏移量] 访问局部变量
; 函数返回前
MOV ESP, EBP ; 恢复 ESP 的值
POP EBP ; 恢复 EBP 的值
; 函数返回
RET
函数有入口出口,但不一定有返回值和参数
堆栈
windows堆栈
- 先进后出
- 向低地址扩展
堆栈平衡
定义
堆栈平衡指的是在函数调用前后,栈指针(如 x86 架构中的ESP
或RSP
)要恢复到调用函数之前的位置。也就是说,在函数调用过程中,若有数据被压入栈中,那么在函数调用结束后,这些数据必须从栈中弹出,从而保证栈的状态不发生改变。
实现方式
__cdecl:
C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。
PUSH param2 ; 压入第二个参数
PUSH param1 ; 压入第一个参数
CALL function ; 调用函数
ADD ESP, 8 ; 调用者清理栈中的两个4字节参数
__stdcall:
windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
PUSH param2 ; 压入第二个参数
PUSH param1 ; 压入第一个参数
CALL function ; 调用函数
; 被调用函数在返回前会清理栈中的参数
简单示例
; 定义数据段
section .data
num1 dd 10
num2 dd 20
; 定义代码段
section .text
global _main
extern _printf
; 自定义函数
add_numbers:
; 保存寄存器
PUSH EBP
MOV EBP, ESP
; 计算两个数的和
MOV EAX, [EBP + 8] ; 第一个参数
ADD EAX, [EBP + 12] ; 第二个参数
; 恢复寄存器
MOV ESP, EBP
POP EBP
; 返回结果
RET
_main:
; 保存寄存器
PUSH EBP
MOV EBP, ESP
; 压入参数
PUSH DWORD [num2]
PUSH DWORD [num1]
; 调用函数
CALL add_numbers
; 清理栈中的参数
ADD ESP, 8
; 打印结果
PUSH EAX
PUSH format
CALL _printf
ADD ESP, 8
; 恢复寄存器
MOV ESP, EBP
POP EBP
; 返回 0
MOV EAX, 0
RET
format:
db '%d', 10, 0
db
:汇编中的伪指令,表示定义字节。'%d'
:printf
函数的格式化字符串,用于打印整数。10
:ASCII 码表示换行符。0
:字符串的结束符。
C 语言
分析下面代码的反编译
int plus1(int x, int y) {
return x + y;
}
int plus2(int x, int y, int z) {
int i;
i = plus1(x, y);
int r;
r = plus1(i, z);
return r;
}
int main() {
plus2(1, 2, 3);
return 0;
}
打上断点,转到反汇编文件
int main() {
00FD1860 push ebp
00FD1861 mov ebp,esp
; 为局部变量分配栈空间
00FD1863 sub esp,0C0h
00FD1869 push ebx
00FD186A push esi
00FD186B push edi
00FD186C mov edi,ebp
; 清零,用于后续重复操作计数
00FD186E xor ecx,ecx
00FD1870 mov eax,0CCCCCCCCh
00FD1875 rep stos dword ptr es:[edi]
; 调试相关
00FD1877 mov ecx,offset _959A5141_test@cpp (0FDC000h)
00FD187C call @__CheckForDebuggerJustMyCode@4 (0FD1320h)
00FD1881 nop
plus2(1, 2, 3);
00FD1882 push 3
00FD1884 push 2
00FD1886 push 1
00FD1888 call plus2 (0FD1258h) ; 在此处单步调试,步入到 jmp 指令
00FD188D add esp,0Ch ; 清理传递给 plus2 函数的参数所占用的栈空间, 12字节
return 0;
00FD1890 xor eax,eax
}
; 恢复原数据
00FD1892 pop edi
00FD1893 pop esi
00FD1894 pop ebx
00FD1895 add esp,0C0h
; 检查指针是否正确
00FD189B cmp ebp,esp
00FD189D call __RTC_CheckEsp (0FD123Fh)
00FD18A2 mov esp,ebp
00FD18A4 pop ebp
00FD18A5 ret
单步调试跳转到此,函数规范写法,统一在一个地方 jmp
解析PE文件结构
PE 头字段说明
1、DOS头:
WORD e_magic * "MZ标记" 用于判断是否为可执行文件.
DWORD e_lfanew; * PE头相对于文件的偏移,用于定位PE文件
2、标准PE头:
WORD Machine; * 程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器
WORD NumberOfSections; * 文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值.大小表示不包括DOS头、NT头、节表,此文件分为几个节(例如.text、.idata等)
DWORD TimeDateS tamp; * 时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的.
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; * 可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h 大小可以自定义.
WORD Characteristics; * 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1
3、可选PE头:
WORD Magic; * 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; * 所有代码节的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfInitializedData;* 已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfUninitializedData; 未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD AddressOfEntryPoint; * 程序入口
DWORD BaseOfCode; * 代码开始的基址,编译器填的 没用
DWORD BaseOfData; * 数据开始的基址,编译器填的 没用
DWORD ImageBase; * 内存镜像基址
DWORD SectionAlignment; * 内存对齐
DWORD FileAlignment; * 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; * 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; * 所有头+节表按照文件对齐后的大小,否则加载会出错
DWORD CheckSum; * 校验和,一些系统文件x有要求.用来判断文件是否被修改.
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; * 初始化时保留的堆栈大小
DWORD SizeOfStackCommit; * 初始化时实际提交的大小
DWORD SizeOfHeapReserve; * 初始化时保留的堆大小
DWORD SizeOfHeapCommit; * 初始化时实践提交的大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; * 目录项数目
Characteristics
把内存中的值读出来后,即读作0x010F,此时化成二进制,第七位省略,其他的每一位都表示一个特征,如果为1,则表示此文件有此位对应的特征;为0表示没有此特征
标志位名称(十六进制值) | 二进制位位置(从右往左,从 0 开始计数) | 含义描述 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED (0x0001) | 0 | 文件中已剥离重定位信息 |
IMAGE_FILE_EXECUTABLE_IMAGE (0x0002) | 1 | 该文件是可执行文件(不是对象文件或库文件) |
IMAGE_FILE_LINE_NUMS_STRIPPED (0x0004) | 2 | 文件中已剥离行号信息(用于调试) |
IMAGE_FILE_LOCAL_SYMS_STRIPPED (0x0008) | 3 | 文件中已剥离局部符号信息(用于调试) |
IMAGE_FILE_AGGRESSIVE_WS_TRIM (0x0010) | 4 | 应用程序积极修剪工作集 |
IMAGE_FILE_LARGE_ADDRESS_AWARE (0x0020) | 5 | 应用程序可以处理大于 2GB 的地址 |
IMAGE_FILE_BYTES_REVERSED_LO (0x0080) | 7 | 低字节序已反转(很少使用) |
IMAGE_FILE_32BIT_MACHINE (0x0100) | 8 | 该文件是为 32 位计算机系统设计的 |
IMAGE_FILE_DEBUG_STRIPPED (0x0200) | 9 | 文件中已剥离调试信息 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP (0x0400) | 10 | 如果文件在可移动媒体上,可从交换文件运行 |
IMAGE_FILE_DLL (0x2000) | 11 | 该文件是一个动态链接库(DLL) |
IMAGE_FILE_UP_SYSTEM_ONLY (0x4000) | 12 | 该文件仅在 UP(单处理器)系统上运行 |
IMAGE_FILE_BYTES_REVERSED_HI (0x8000) | 15 | 高字节序已反转(很少使用) |
AddressOfEntryPoint
程序入口点OEP(程序真正执行的起始地址):这个值是偏移量,而不是真正运行在内存中的程序入口地址。需要再加上加载到内存的基址(imagebase),才是程序运行在内存中(4GB虚拟内存)的程序入口。这个值不是确定的
[!CAUTION]
- 程序入口在默认情况下一般都在.code代码节当中,且OEP不是只能在.code代码节开始的位置,可以从此节当中的任何合理位置开始,也可以在其他节(如.text等)的任意合理位置开始。OEP可以人为修改,但是最后一定要让.exe文件能运行起来
- 程序入口不能理解为C语言的main函数,那只是我们写的代码的执行入口,因为在main函数被调用前还做了很多事情,所以OEP一定是.exe双击开始运行时程序开始的那个地址,可以用OD打开看一下,如下
- 内存中的程序入口地址:使用OD打开文件(完全模拟文件运行时加载到内存中的状态,不是硬盘上的状态)。所以OD打开一个可执行文件后,会在程序入口地址处设置断点,让程序停下来,这里就是文件在内存中真正的入口点。即文件装入到4GB虚拟内存中的起始基地址 +相对于文件首地址的偏移的程序入口地址,即
imagebase + AddressOfEntryPoint
ImageBase
- 内存镜像基址:我们知道每一个.exe程序都有属于自己的4GB虚拟内存,这个值就是当程序运行装入到自己的虚拟4GB内存中后的文件的起始位置。imagebase一般都是0x00400000(也可以改),不能超过0x80000000,因为我们写的程序的数据只能在内存的2GB用户区中,不能占用2GB系统区
- 内存镜像地址可能会重复,但是加载的时候操作系统会转换镜像地址
- 为什么imagebase不从0开始? 因为内存保护!我们前面学过,free一个动态分配内存的指针后,一定要将指针 = NULL,那么指针等于NULL后,这个指针指向的地址就是0x0,那么如果此时访问此指针指向的数据,或者向后偏移一定大小的范围内的数据,编译器会立马报错。所以4GB内存中开始空出来一些内存空间就是为了内存保护的。因为查找效率更高,可以理解为模块对齐。
- 模块的概念 一个exe内可能有多个pe文件结构,例如一些dll,exe本身就是一个pe文件满足pe结构,但是exe中可能用到的多个dll也是pe文件,也满足pe结构,相当于一个exe里面有很多个模块,每个dll都是一个模块 通过od可以看到exe的PE文件里面还有很多PE结构
可执行文件加载进内存的过程
- 编译器生成exe PE文件 在 VS 中编译链接时,链接器确定 imagebase(基址)、OEP(程序入口点)等 PE 文件数据,将生成的 PE 文件存于硬盘
- 硬盘文件数据读到 FileBuffer 借助 winhex 或十六进制编译器,把硬盘上可执行文件的数据原样复制到内存的 FileBuffer 中,打开后显示的数据就是文件在硬盘上的状态,此时文件格式不具 Windows 运行格式
- 文件从 FileBuffer 加载到 ImageBuffer 通过 PE loader 操作,一般因磁盘文件对齐值小于内存对齐值,需拉伸文件以满足内存对齐,再将其加载到 4GB 虚拟内存。imageBase 是建议加载基址,实际加载可能因冲突重定位。用实际加载地址加偏移可得文件其他内容地址,此时 ImageBuffer 中文件满足运行格式但未执行
[!NOTE]
将硬盘上的 exe 文件按内存对齐要求处理(若磁盘与内存对齐值不同则拉伸),加载到 4GB 虚拟内存中,该过程称为 PE loader。
- 操作系统会将虚拟地址转化成物理地址 FileBuffer 和 ImageBuffer 中的地址皆为虚拟地址,操作系统自动处理虚拟地址到物理地址的转换。在此之前,虽 ImageBuffer 中文件有可执行基础格式,仍需完成地址映射等操作才会被 CPU 执行 流程图如下所示:
flowchart TD
A[编译器生成exe PE文件] --> B[硬盘文件数据读到FileBuffer]
B --> C[文件从FileBuffer加载到ImageBuffer]
C --> D[操作系统将虚拟地址转化成物理地址]
subgraph 1
A1[VS编译链接,确定imagebase、OEP等数据] --> A2[存PE文件到硬盘]
end
subgraph 2
B1[用工具打开可执行文件] --> B2[复制数据到FileBuffer]
B2 --> B3[文件格式非Windows运行格式]
end
subgraph 3
C1[PE loader处理文件]
C1 --> C2{磁盘文件对齐值与内存对齐值关系}
C2 -->|小于| C3[拉伸文件,满足内存对齐]
C2 -->|等于| C4[直接加载]
C3 --> C5[加载到4GB虚拟内存,确定imageBase为建议基址]
C4 --> C5
C5 --> C6[可能因冲突重定位,确定实际加载地址]
C6 --> C7[用实际地址+偏移获取其他内容地址]
C7 --> C8[ImageBuffer中文件满足运行格式,但未执行]
end
subgraph 4
D1[FileBuffer和ImageBuffer地址为虚拟地址] --> D2[操作系统转换为物理地址]
D2 --> D3[完成地址映射等,文件待CPU执行]
end