寄存器

寄存器

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 等指令的重复次数
EDXI/O指针
ESI源变址在字符串操作指令中指向源字符串
EDI目的变址在字符串操作指令中指向目标字符串
ESP堆栈指针,指向栈顶栈操作(如 PUSHPOP)会自动更新 ESP
EBP基址指针,指向当前栈帧的基址用于访问栈帧内的局部变量和函数参数

MOV 目标操作数,源操作数

作用:拷贝源操作数到目标操作数

  1. 源操作数可以是立即数、通用寄存器、段寄存器或者内存单元.
  2. 目标操作数可以是通用寄存器、段寄存器或者内存单元.
  3. 操作数的宽度必须一样.
  4. 源操作数和目标操作数不能为同一目标单元.

函数

函数入口

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堆栈

  1. 先进后出
  2. 向低地址扩展

堆栈平衡

定义

堆栈平衡指的是在函数调用前后,栈指针(如 x86 架构中的ESPRSP)要恢复到调用函数之前的位置。也就是说,在函数调用过程中,若有数据被压入栈中,那么在函数调用结束后,这些数据必须从栈中弹出,从而保证栈的状态不发生改变。

实现方式

__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打开看一下,如下image-20250423142712072
  • 内存中的程序入口地址:使用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结构 image-20250423143250727

可执行文件加载进内存的过程

  1. 编译器生成exe PE文件 在 VS 中编译链接时,链接器确定 imagebase(基址)、OEP(程序入口点)等 PE 文件数据,将生成的 PE 文件存于硬盘
  2. 硬盘文件数据读到 FileBuffer 借助 winhex 或十六进制编译器,把硬盘上可执行文件的数据原样复制到内存的 FileBuffer 中,打开后显示的数据就是文件在硬盘上的状态,此时文件格式不具 Windows 运行格式
  3. 文件从 FileBuffer 加载到 ImageBuffer 通过 PE loader 操作,一般因磁盘文件对齐值小于内存对齐值,需拉伸文件以满足内存对齐,再将其加载到 4GB 虚拟内存。imageBase 是建议加载基址,实际加载可能因冲突重定位。用实际加载地址加偏移可得文件其他内容地址,此时 ImageBuffer 中文件满足运行格式但未执行

[!NOTE]

将硬盘上的 exe 文件按内存对齐要求处理(若磁盘与内存对齐值不同则拉伸),加载到 4GB 虚拟内存中,该过程称为 PE loader。

  1. 操作系统会将虚拟地址转化成物理地址 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
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!