文章总结: 本文详细介绍了Windowsx64系统下汇编语言中的函数调用机制,包括CALL和RET指令的工作原理,以及Windowsx64调用约定。重点解释了参数传递规则(前4个参数通过寄存器RCX、RDX、R8、R9传递,第5个及以后通过栈传递),返回值放在RAX寄存器,以及特殊的32字节影子空间概念。文章提供了完整的代码示例演示这些概念,并介绍了LEA指令的特殊用法,对理解底层系统调用机制有重要价值。
综合评分: 89
文章分类: 二进制安全,代码审计,逆向分析
汇编系列第三篇-内存布局与函数01
原创
RabbitQ
安全研究站
2025年12月4日 00:04
北京
添加作者微信,加入交流群:
接昨天的汇编基础,我本地的教程本来是一章,但是弄到一半懒得排版所以拆分了一下,所以如果有序号问题请忽略~
在汇编中,并没有像高级语言那样天然的函数概念。所谓的函数调用,其实是跳转指令和栈内存操作的精妙配合。而在 Windows x64 系统下,为了让大家写的代码能互相通用,必须遵守一套严格的“交通规则”。
函数调用与 Windows x64 核心规则
函数调用的物理机制 (CALL & RET)
这部分是硬件层面的动作,不管你用什么操作系统,CPU 都是这样工作的。
1. CALL(调用)和 RET(返回)
你可以把函数调用想象成看书时被打断:你需要记下当前看到第几页(压栈返回地址),去处理别的事(跳转),处理完后再翻回那一页(弹栈并跳转回来)。
| 指令 | 它是谁 | 核心动作(机器内部做了什么) |
| — | — | — |
| call MyFunc | 这里的“去” | 1. Push RIP:把紧跟在 call 后面那条指令的地址压入栈顶(保存“回来的路”)。 2. Jmp MyFunc:跳到目标函数去执行。 |
| ret | 这里的“回” | 1. Pop RIP:从栈顶拿出一个地址。 2. Jmp RIP:跳到这个地址去。 |
2. 最小代码演示
_start:
; 1. 准备工作
mov rax, 10
; 2. 调用函数
call double_rax ; 机器操作:PUSH 下一行地址, JMP double_rax
; 3. 回来之后 (RET 会跳到这里)
add rax, 1 ; 此时 RAX 已经是 20 了,再加 1 变成 21
ret
; --- 定义函数 ---
double_rax:
add rax, rax ; 10 + 10 = 20
ret ; 机器操作:POP 返回地址, JMP 到上面的 "add rax, 1"
Windows x64 的“交通规则” (ABI:Application Binary Interface)
既然能跳过去了,那怎么把数据带过去?(传参)结果怎么带回来?(返回值)。Windows 规定了以下铁律:
规则 1:前 4 个参数用寄存器(VIP 通道)
为了快,前 4 个参数不用栈,直接放在指定的寄存器里。
| 参数位置 | 寄存器 |
| — | — |
| 第 1 个 | RCX |
| 第 2 个 | RDX |
| 第 3 个 | R8 |
| 第 4 个 | R9 |
规则 2:第 5 个及以后参数用栈(普通通道)
如果参数超过 4 个,第 5 个开始的数据必须扔到栈上(具体怎么放,看后面的例子)。
规则 3:返回值放在 RAX
不管函数里算了什么,离开前都要把结果塞进 RAX。
规则 4:必须分配“影子空间” (Shadow Space)
这是 Windows x64 最特殊的地方。**调用者(Caller)**在调用函数前,必须在栈上预留 32 字节 的空位。
什么是“影子空间”?
1. 它是啥?
它是在栈上预留的一块 32 字节(4 x 8字节) 的内存区域。
2. 谁负责分配?
你(调用者)。当你写 call 指令前,你得先 sub rsp, 32。
3. 它是干嘛用的?
它是给前 4 个寄存器参数(RCX, RDX, R8, R9) 预留的“备份仓库”。 虽然前 4 个参数通过寄存器传递,但被调用的函数(比如 printf 或调试器)有时候需要把这 4 个寄存器的值写回内存里保存。这个“写回的地方”就是你预留的影子空间。
如果不留这 32 字节会怎样?被调用的函数可能会直接往 [RSP+8] 等位置写数据,直接覆盖掉栈里其他重要的东西(比如你的返回地址),导致程序崩溃。
4.为啥用这么分配影子空间
根据前面的学习,我们知道 sub rsp, 32的意思就是rsp-32,rsp是栈指针,这块我们得从两个点来看,栈的工作方式和影子空间的设计目的
4.1栈的工作方式:高地址往低地址增长
在x64架构中:
- 栈指针(rsp)总是指向栈顶,也就是当前栈中已使用内存的最低地址
- 当栈上分配空间是,栈指针向低地址移动(数值减小)
- a当栈上释放空间时,栈指针向高地址移动(数值增大)
这么干的目的就是在栈顶留一块32个字节的空闲区域,区域范围就是rsp~rsp+31
包含所有规则的完整代码
我们来写一个包含 5 个参数 的函数调用,展示所有规则的结合。
任务:实现 result = sum_five(10, 20, 30, 40, 50)
1. 调用方 (main)
调用方要做三件事:
- 分配影子空间。
- 填寄存器(前4个)。
- 填栈(第5个)。
extern ExitProcess ; 声明外部函数,需要用到 Windows API
extern printf
global WinMain
section .data
format_output db "Sum is: %lld", 0Ah, 0
section .text
default rel
WinMain:
; --- 步骤 A: 建立栈空间 ---
; 我们需要预留 32字节(影子空间)
; 这里的栈操作通过 sub rsp, 32 完成
sub rsp, 32
; --- 步骤 B: 传递前 4 个参数 (寄存器) ---
mov rcx, 10 ; 第 1 个 -> RCX
mov rdx, 20 ; 第 2 个 -> RDX
mov r8, 30 ; 第 3 个 -> R8
mov r9, 40 ; 第 4 个 -> R9
; --- 步骤 C: 传递第 5 个参数 (栈) ---
; 第 5 个参数要放在影子空间也就是 32 字节的“上面”
; 注意:因为此时 RSP 指向影子空间的底部
; 所以位置是 [rsp + 32]
mov qword [rsp + 32], 50
; --- 步骤 D: 调用 ---
call sum_five
add rsp, 32 ; 释放 sum_five 的 shadow space
sub rsp, 32 ; 为 printf 分配 shadow space
lea rcx, [rel format_output] ; 第 1 个参数 (RCX): 格式字符串地址
mov rdx, rax ; 第 2 个参数 (RDX): RAX 中保存着 sum_five 的结果 150
; 2. 调用 printf
; 注意:因为我们已经为 sum_five 分配了 32 字节(即 `sub rsp, 32`),
; 并且在调用 sum_five 后没有释放,所以这 32 字节可以作为 printf 的影子空间!
call printf
; 释放 sum_five 和 printf 共用的 32 字节影子空间
add rsp, 32
; ExitProcess(UINT uExitCode)
mov rcx, 0
;分配 ExitProcess 的影子空间
sub rsp, 32
; 此时 RAX 里应该是 150
call ExitProcess ; 调用 Windows API 退出程序
2. 被调用方 (sum_five)
被调用方要做两件事:
- 建立栈帧(保护现场)。
- 取出参数计算(注意怎么取第 5 个参数)。
sum_five:
; --- 1. 标准序言 (Prologue) ---
push rbp ; 保存调用者的基址
mov rbp, rsp ; 建立自己的栈帧基址
; --- 2. 提取参数并计算 ---
; 此时栈的结构(从上往下):
; [RBP + 48] -> 第 5 个参数 (50) <-- 关键!
; [RBP + 16] -> 影子空间 (32字节)
; [RBP + 8] -> 返回地址
; [RBP] -> 旧 RBP
mov rax, rcx ; RAX = 10
add rax, rdx ; RAX = 30
add rax, r8 ; RAX = 60
add rax, r9 ; RAX = 100
; 加上栈里的第 5 个参数
; 为什么是 +48?(旧RBP 8字节 + 返回地址 8字节 + 影子空间 32字节)
add rax, [rbp + 48] ; RAX = 150
; --- 3. 标准尾声 (Epilogue) ---
pop rbp ; 恢复旧 RBP
ret ; 返回
程序运行结果:
7.LEA 指令(特殊的MOV)
LEA = Load Effective Address(加载地址)
section .data
msg db "Hello", 0
section .text
lea rcx, [msg] ; RCX = msg的地址
mov rcx, [msg] ; RCX = msg的内容(第一个字节'H')
区别:
lea取的是地址(指针)mov取的是值(内容)
比喻:
lea= 告诉你书在哪(地址)mov= 把书拿给你(内容)
这里有个小点可以注意一下,一般用来做快速算数运算,如:
lea rax, [rbx + 8] ;RAX = RBX + 8
再举个小例子:
lea rdx, [rcx*2 + 5]
;相当于:
mov rdx, rcx
add rdx, rcx
add rdx, 5
所以这玩意可以理解为lea = 加法计算器 + 轻量级乘法器,也就是管他是啥呢,咋好用咋用
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全研究站 RabbitQ《汇编系列第三篇-内存布局与函数01》