windows汇编:X86汇编基础(一)

238 阅读7分钟

前言

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的x86X64均采用该内存模型.程序可以访问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)
  • 参数传递

    • 前两个整数参数通过寄存器 ECXEDX 传递。
    • 其余参数通过栈传递(参数从右向左压栈)。
  • 返回值:返回值通过 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:包含处理器状态和控制信息的寄存器。

通用寄存器,一般用于程序控制和参数传递. 在不同的调用约定中,作用一样.这里比较重要的有指针寄存器(栈指针),EBPESP 在汇编程序的调用中,理解其作用非常重要.X86X64的栈是从高地址向低地址生长的.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一般是用于使用的一个临时指针,ESPPUSH 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]