勇闯汇编
今天心血来潮学了汇编,以下记录学到的知识
基本概念
extern 引用外部函数:
1 | extern ExitProcess |
section .data 标志初始化变量段
初始化变量格式:
1 | var1 db <var_data> |
section .text 标志可执行代码段
_start 代表程序入口点
常用指令
数据传送指令
mov 指令用于在寄存器之间或寄存器与内存之间移动数据。
1 | mov rax, 100 ; 将立即数 100 移动到 rax 寄存器 |
算术运算指令
add: 加法sub: 减法inc: 递增dec: 递减xor: 异或 (常用于将寄存器清零)
1 | mov rax, 100 |
比较与跳转指令
cmp: 比较两个操作数,并根据结果设置标志位。jmp: 无条件跳转。jne: 如果不相等则跳转 (jump if not equal)。jle: 如果小于或等于则跳转 (jump if less or equal)。
1 | ; if-else 结构 |
函数与调用约定
函数定义与调用
一个典型的函数包含函数序言、函数体和函数尾声。
1 | ; 定义一个函数 |
参数传递与返回值 (x64 Windows 调用约定)
在x64 Windows调用约定中,函数参数和返回值的传递有明确的规则:
- 参数传递: 前四个整数或指针参数按从左到右的顺序依次放入
rcx,rdx,r8,r9寄存器。更多的参数则通过栈传递。 - 返回值: 整数或指针类型的返回值通常存储在
rax寄存器中。
在上面的 AddTwoNumbers 例子中:
mov rcx, 50和mov rdx, 25将两个参数放入了正确的寄存器。- 函数内部直接从
rcx和rdx读取参数。 add rax, rdx的计算结果(返回值)被存放在rax中。- 调用
call AddTwoNumbers之后,主程序通过读取rax寄存器来获取函数的返回值。
栈操作与 sub rsp, 40 的重要性
栈的基本操作
push 和 pop 用于在函数调用期间保存和恢复寄存器的状态,确保函数调用不会破坏调用者的寄存器。
1 | ; 函数序言中保存基指针 |
sub rsp, 40 与 Shadow Space
在x64 Windows调用约定中,sub rsp, 40 这行代码至关重要。它不仅仅是为局部变量分配空间,更重要的是它遵循了调用约定中的 Shadow Space (影子空间) 规则。
- 什么是Shadow Space?: 调用约定规定,当一个函数(调用者)准备调用另一个函数(被调用者)时,它必须在栈上为被调用者预留一块空间。这块空间被称为 “Shadow Space”。在x64 Windows上,这块空间的大小是32字节(可以容纳4个64位寄存器)。
- 为什么需要它?: 这块空间允许被调用函数在不修改自己栈帧的情况下,方便地保存
rcx,rdx,r8,r9这四个参数寄存器的值。 - 为什么是40字节?:
call指令会将返回地址(8字节)压入栈中。为了保持栈的16字节对齐(这在x64下是推荐的,可以提高性能),并且为32字节的Shadow Space腾出空间,我们需要分配32 + 8 = 40字节。sub rsp, 40正好满足了这个要求,它为即将到来的call指令做好了准备。
总结: sub rsp, 40 是在函数调用前,为满足x64 Windows调用约定而进行的关键栈操作,它分配了32字节的Shadow Space并确保了栈的对齐。
Windows API 调用详解
通过 call 指令调用 extern 声明的外部函数。以下是常用API的参数和返回值说明:
ExitProcess: 终止当前进程。- 接收值:
rcx- 进程的退出码 (一个32位无符号整数)。0通常表示成功。 - 返回值: 无。
- 接收值:
GetStdHandle: 获取标准输入、输出或错误设备的句柄。- 接收值:
rcx- 一个指定标准设备的32位整数。常用值包括:-10: 标准输入 (STD_INPUT_HANDLE)-11: 标准输出 (STD_OUTPUT_HANDLE)-12: 标准错误 (STD_ERROR_HANDLE)
- 返回值:
rax- 返回一个句柄。如果失败,返回INVALID_HANDLE_VALUE(-1)。
- 接收值:
WriteConsoleA: 将一个ASCII字符串写入控制台。- 接收值:
rcx: 通过GetStdHandle获取的控制台输出句柄。rdx: 指向要写入的字符串缓冲区的指针。r8: 要写入的字符数。r9: 指向一个变量的指针,该变量用于接收实际写入的字符数(可以为0,表示不关心)。[rsp+32]: (栈上传递的第五个参数) 必须为0。
- 返回值:
rax- 如果函数成功,返回非零值。如果失败,返回零。
- 接收值:
1 | ; 示例:打印 "Hello, World!" |
一个脚本
我还写了一个Python脚本用来帮我快速编译asm文件:
1 | import sys |
用法:
python asmc.py <your_file.asm> [library1.lib library2.lib …]
例如:
python asmc.py hello.asm kernel32.lib