前言
X86由英特尔开发的微处理器架构,广泛用于个人计算机,支持多种操作系统。x86汇编模型包括寄存器、指令集、寻址模式及其他重要方面。
一段汇编程序
.586 ; 使用 586 及以上的处理器指令集
.MODEL FLAT, CDECL ; 定义内存模型为平面模型,使用 CDECL 调用约定
.STACK 4096 ; 定义栈大小为 4096 字节(可根据需要调整)
.DATA ; 数据段开始
num1 DWORD 5 ; 第一个数字,存储在 DWORD 类型中
num2 DWORD 10 ; 第二个数字,存储在 DWORD 类型中
result DWORD ? ; 存储结果的变量,初始值未定义
format DB "Result: %d", 0 ; 输出格式字符串,末尾加 0 作为结束符
.CODE ; 代码段开始
main PROC ; 定义主过程,程序入口点
; 初始化
mov eax, num1 ; 将 num1 的值加载到 EAX 寄存器
add eax, num2 ; 将 num2 加到 EAX 寄存器的值上
mov result, eax ; 将计算结果存储到 result 变量中
; 输出结果
invoke crt_printf, ADDR format, result ; 调用 printf 函数输出结果
; 结束程序
invoke ExitProcess, 0 ; 调用系统函数退出程序
main ENDP ; 主过程结束
END main ; 指示汇编器程序结束,main 为入口点
.586这里是定义指令集.model flat这里是值汇编使用的模型.cdecl表示的是调用约定
内存模型
内存模型有很多,但是在 Windows 操作系统中,主要使用的内存模型包括 Flat Model
Flat 模型
Flat 模型将整个内存视为一个连续的线性地址空间,所有的代码、数据和堆栈都在同一个地址空间中,Flat 模型是一种高效、简单的内存管理方式,适合现代计算环境。目前,windows的x86与X64均采用该内存模型.程序可以访问4GB的虚拟内存空间.(单进程)可以使用的 调用约定: cdecl,stdcall,fastcall
Small模型
一般用于嵌入式操作系统,限制为64KN-128KB,调用约定: cdecl
调用约定
调用约定是指在程序中如何传递参数、返回值以及如何管理调用栈的规则。不同的调用约定适用于不同的编程语言、编译器和操作系统
1. cdecl (C Declaration)
- 参数传递:所有参数通过栈传递,参数从右向左压栈.
- 返回值:返回值通过
EAX寄存器传递。 - 栈清理:调用者负责清理栈。
- 特点:支持可变参数函数(如
printf),广泛应用于 C 语言。
2. stdcall (Standard Call)
- 参数传递:所有参数通过栈传递,参数从右向左压栈.
- 返回值:返回值通过
EAX寄存器传递。 - 栈清理:被调用者负责清理栈。
- 特点:常用于 Windows API 函数,简化了调用者的栈管理。
3. fastcall (X86)
-
参数传递:
- 前两个整数参数通过寄存器
ECX和EDX传递。 - 其余参数通过栈传递(参数从右向左压栈)。
- 前两个整数参数通过寄存器
-
返回值:返回值通过
EAX寄存器传递。 -
栈清理:调用者负责清理栈。
-
特点:提高了性能,适合参数较少的情况。
4. thiscall
- 参数传递:对象的指针(
this)隐式传递在ECX中,其他参数通过栈传递。 - 返回值:返回值通过
EAX寄存器传递。 - 栈清理:调用者负责清理栈。
- 特点:用于 C++ 的成员函数调用。
5. Microsoft x64 Calling Convention(X64 下的fastcall)
-
参数传递:
- 前四个整数或指针参数通过寄存器
RCX,RDX,R8,R9传递。 - 其余参数通过栈传递(参数从右向左压栈)。
- 前四个整数或指针参数通过寄存器
-
返回值:返回值通过
RAX寄存器传递。 -
特点:适用于 x64 Windows 系统,显著提高了性能。
6. System V AMD64 ABI (UNIX/LINUX)
-
参数传递:
- 前六个整数或指针参数通过寄存器
RDI,RSI,RDX,RCX,R8,R9传递。 - 其余参数通过栈传递(参数从右向左压栈)。
- 前六个整数或指针参数通过寄存器
-
返回值:返回值通过
RAX寄存器传递。 -
特点:主要用于 Linux 和其他 Unix 系统。
调用约定的返回值,均通过eax(rax 64位) 进行传参,cdecl 与 windows的 stdcall 唯一的区别是平栈方是调用者还是被调用者.stdcall是被调用者负责平栈.(个人觉得这种比较好),fastcall 是寄存器传参,X86(win)下是前两个整数通过ecx edx 传递,其他通过栈传递.X64下前4个参数
绝大部分调用约定的压栈都是从右到左依次压栈.唯一不是的是passcall调用约定. delphi这种上古世纪的编译器好像用的是这种调用约定.
寄存器
x86架构包括多个寄存器,主要分为以下几类:
-
通用寄存器:
EAX:累加寄存器,用于算术运算。EBX:基址寄存器,常用作数据存储的基址。ECX:计数寄存器,常用于循环计数。EDX:数据寄存器,常用于I/O操作。
-
索引寄存器:
ESI:源索引寄存器,通常用于字符串处理。EDI:目的索引寄存器,通常用于字符串处理。
-
指针寄存器:
EBP:基指针寄存器,常用于栈帧指针。ESP:栈指针寄存器,指向当前栈顶。
-
程序计数器:
EIP:指向下一条要执行的指令。
-
状态寄存器:
EFLAGS:包含处理器状态和控制信息的寄存器。
通用寄存器,一般用于程序控制和参数传递. 在不同的调用约定中,作用一样.这里比较重要的有指针寄存器(栈指针),EBP与ESP 在汇编程序的调用中,理解其作用非常重要.X86与X64的栈是从高地址向低地址生长的.ESP指向的是栈的顶端.
; 初始 ESP
mov esp, 0x0012FF00 ; ESP = 0x0012FF00
; 压入第二个参数
push arg2 ; ESP = 0x0012FEFC
; 压入第一个参数
push arg1 ; ESP = 0x0012FEF8
; 调用函数
call my_function ; ESP = 0x0012FEF4 (返回地址被压入栈)
my_function:
; 保存调用者的基指针
push ebp ; ESP = 0x0012FEF0 (保存的 EBP 被压入栈)
; 设置新的基指针
mov ebp, esp ; EBP = 0x0012FEF0 (EBP 现在指向当前栈帧的底部)
; 访问第一个参数 arg1
mov eax, [ebp + 8] ; EAX = arg1 的值,ESP不会变.
; 访问第二个参数 arg2
mov ebx, [ebp + 12] ; EBX = arg2 的值,ESP不会变.
; 恢复栈指针
mov esp, ebp ; ESP = 0x0012FEF0 (ESP 恢复到 EBP 的位置)
; 恢复调用者的基指针
pop ebp ; ESP = 0x0012FEF4 (恢复调用者的栈顶)
; 返回到调用者
ret ; ESP = 0x0012FEF8 (返回后 ESP 会指向 arg1 的位置)
这里可以看出,EBP一般是用于使用的一个临时指针,ESP在PUSH POP CALL RET 指令或者与ESP相关操作的指令时,ESP 的值才会变化.EBP 只有在与EBP相关指令操作时,才会变化.调用函数的时候,ESP会压入函数的返回地址.
如果需要使用局部变量,那么使用ESP - 分配空间
sub esp, 16 ; 为局部变量分配 16 字节的空间
然后进行局部变量的赋值
mov [ebp - 4], 89 ; 将89的值存储到第一个局部变量
函数返回的时候,也需要恢复栈帧
mov esp, ebp ; 恢复 ESP 到栈帧的底部
pop ebp ; 恢复调用者的基指针
ret ; 返回到调用者
3. 指令集
x86指令集包括各种指令,主要分为以下几类:
- 数据传输指令:如
MOV,PUSH,POP。 - 算术运算指令:如
ADD,SUB,MUL,DIV。 - 逻辑运算指令:如
AND,OR,XOR,NOT。 - 控制流指令:如
JMP,CALL,RET,LOOP。 - 字符串操作指令:如
MOVSB,MOVSW,REP。
4. 寻址模式
x86汇编支持多种寻址模式,以便访问内存中的数据:
- 立即寻址:直接使用常数值,如
MOV EAX, 5。 - 寄存器寻址:使用寄存器中的值,如
MOV EAX, EBX。 - 直接寻址:使用内存地址,如
MOV EAX, [1000h]。 - 间接寻址:通过寄存器指向内存地址,如
MOV EAX, [EBX]。 - 基址加偏移:结合基址寄存器和偏移量,如
MOV EAX, [EBX+4]。 - 索引寻址:结合索引寄存器和基址寄存器,如
MOV EAX, [EBX+ESI]。