AT&T 汇编 #
GNU Linux 使用是 AT&T 汇编语法(GAS),而 Windows、Intel 手册使用 Intel 语法:
特性 | AT&T 语法 | Intel 语法 |
---|---|---|
操作数顺序 | 源在左,目标在右 | 源在右,目标在左 |
立即数 | 带 $ 前缀 | 无前缀 |
寄存器 | 带 % 前缀 | 无前缀 |
内存寻址 | disp(base,index,scale) |
[base + index*scale + disp] |
操作数大小 | 指令带后缀 (l,w,b) | 用关键字 (BYTE PTR 等) |
示例:
# AT&T 语法
movl $42, %eax # 立即数到寄存器
movl $42, -4(%rbp) # 立即数到内存
movl -4(%rbp), %eax # 内存到寄存器
movl (%rbx,%rcx,4), %eax # 复杂寻址
数据定义:
# GAS 数据定义
.byte ; 定义字节
.word ; 定义字
.long ; 定义双字
.quad ; 定义四字
# GAS 段定义
.section .data
.section .text
# 举例
.section .data
message: .string "Hello"
.section .text
.global _start
_start:
movq $1, %rax # 源操作数在左,目标在右
movq $1, %rdi
movq $message, %rsi
movq $5, %rdx
syscall
gdb、objdump 和 linux kenel 默认都使用 AT&T 汇编语法,但是也可以通过配置参数来指定 Intel 语法风格。
gdb:
# 默认 AT&T 语法
(gdb) disas main
Dump of assembler code for function main:
0x0000555555555129 <+0>: push %rbp
0x000055555555512a <+1>: mov %rsp,%rbp
0x000055555555512d <+4>: sub $0x10,%rsp
# 切换到 Intel 语法
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x0000555555555129 <+0>: push rbp
0x000055555555512a <+1>: mov rbp,rsp
0x000055555555512d <+4>: sub rsp,0x10
objdump:
# 默认 AT&T 语法
$ objdump -d program
0000000000001129 <main>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: 48 83 ec 10 sub $0x10,%rsp
1131: 89 7d fc mov %edi,-0x4(%rbp)
# 切换到 Intel 语法
$ objdump -M intel -d program
0000000000001129 <main>:
1129: 55 push rbp
112a: 48 89 e5 mov rbp,rsp
112d: 48 83 ec 10 sub rsp,0x10
1131: 89 7d fc mov DWORD PTR [rbp-0x4],edi
寄存器 #
x86_64 提供了 16 个通用寄存器,每个寄存器为 64 位。
- 64位: %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rbp, %rsp, %r8-%r15
- 32位: %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp, %esp, %r8d-%r15d
- 16位: %ax, %bx, %cx, %dx, %si, %di, %bp, %sp, %r8w-%r15w
- 8位: %al, %bl, %cl, %dl, %sil, %dil, %bpl, %spl, %r8b-%r15b
特殊用途寄存器:
- RSP:栈指针,指向当前栈顶。
- RBP:基址指针,用于栈帧基址。
- RIP:指令指针,指向下一条将执行的指令。
- RFLAGS:状态标志寄存器,存储算术运算结果和 CPU 状态。
段寄存器:分为代码段、数据段、栈段等,段寄存器(如 CS、DS)已在现代架构中弱化。
- %cs - 代码段
- %ds - 数据段
- %ss - 栈段
- %es - 扩展段
- %fs - 额外段
- %gs - 额外段
数据传送指令 #
字节序:x86_64 使用小端序,低字节存储在低地址。
操作数大小:支持 8 位(字节)、16 位(字)、32 位(双字)、64 位(四字)。
# mov 指令
movq $60, %rax # 立即数到寄存器
movq %rax, %rbx # 寄存器到寄存器
movq (%rax), %rbx # 内存到寄存器
movq %rax, (%rbx) # 寄存器到内存
# 零扩展传送
movzbq %al, %rax # 字节到四字,零扩展
movzwq %ax, %rax # 字到四字,零扩展
# 符号扩展传送
movsbq %al, %rax # 字节到四字,符号扩展
movswq %ax, %rax # 字到四字,符号扩展
内存寻址 #
基本寻址格式:
AT&T 语法格式:
offset(base, index, scale)
等价于计算公式:
最终地址 = offset + base + (index * scale)
其中:
- offset: 偏移量(可选),常数
- base: 基址寄存器(可选)
- index: 索引寄存器(可选)
- scale: 比例因子(可选),必须是 1、2、4 或 8
直接寻址:
# 直接访问指定内存地址
movq value, %rax # value 是一个标签
movq $0x123456, %rax # 直接地址
寄存器间接寻址:
# 通过寄存器中的地址访问内存
movq (%rax), %rbx # 使用 rax 中的地址
movq (%rsp), %rax # 访问栈顶元素
基址寻址:
# base + offset
movq 8(%rbp), %rax # rbp + 8
movq -8(%rbp), %rax # rbp - 8
movq 16(%rsp), %rax # rsp + 16
变址寻址:
# (index * scale) + offset
movq array(, %rcx, 8), %rax # array + rcx * 8
movq (%rcx, %rdx, 4), %rax # rcx + rdx * 4
基址变址寻址:
# base + (index * scale) + offset
movq 8(%rbx, %rcx, 4), %rax # rbx + rcx * 4 + 8
movq data(%rip, %rcx, 8), %rax # RIP相对寻址
示例:
数组访问:
# int array[10];
# 访问 array[i],假设 i 在 %rcx 中
# 方式1:基址变址寻址
leaq array(%rip), %rax # 获取数组基址
movl (%rax, %rcx, 4), %edx # 访问 array[i]
# 方式2:直接使用标签
movl array(, %rcx, 4), %edx # 直接访问 array[i]
结构体访问:
# struct Point {
# int x; // offset 0
# int y; // offset 4
# long z; // offset 8
# };
# 假设结构体指针在 %rax
movl (%rax), %edx # 访问 p->x
movl 4(%rax), %edx # 访问 p->y
movq 8(%rax), %rdx # 访问 p->z
多维数组访问:
# int matrix[3][4];
# 访问 matrix[i][j]
# i 在 %rcx,j 在 %rdx
# 计算偏移:i * 4 * 4 + j * 4
movq %rcx, %rax
imulq $16, %rax # i * 16 (4 * 4)
leaq (%rax, %rdx, 4), %rax
movl matrix(%rax), %edx
栈帧访问:
# 访问局部变量和参数
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp # 分配栈空间
# 访问局部变量
movq $1, -8(%rbp) # 第一个局部变量
movq $2, -16(%rbp) # 第二个局部变量
# 访问参数(假设是通过栈传递)
movq 16(%rbp), %rax # 第一个栈参数
movq 24(%rbp), %rdx # 第二个栈参数
RIP和基址寻址 #
RIP 相对寻址是特殊的寻址模式:
# 格式:label(%rip)
# 计算方式:目标地址 = 下一条指令地址 + 偏移量
# 位置无关代码中常用
leaq message(%rip), %rdi # 加载字符串地址
movq value(%rip), %rax # 加载全局变量
基址寻址:
# 格式:offset(base_register)
# 计算方式:目标地址 = 基址寄存器内容 + 偏移量
movq 8(%rbp), %rax # 访问栈帧参数
movq -8(%rbp), %rax # 访问局部变量
movq 16(%rsp), %rax # 访问栈上数据
# 多重间接访问
movq (%rax), %rbx # 一级间接
movq (%rbx), %rcx # 二级间接
# 复杂偏移计算
movq 8(%rax, %rcx, 8), %rdx # base + index * scale + offset
RIP 相对寻址:
- 用于位置无关代码 (PIC)
- 基于当前指令位置
- 适合访问全局数据
- 支持 ASLR
- 偏移量在编译时确定
基址寻址:
- 用于局部数据访问
- 基于寄存器内容
- 适合访问栈数据
- 运行时动态计算
- 偏移量是固定的
内存布局示例:
- RIP 相对寻址
代码段:
0x1000: leaq message(%rip), %rdi
0x1007: ... # 下一条指令
数据段:
0x2000: message: "Hello"
计算: 0x1007 + (0x2000 - 0x1007) = 0x2000
- 基址寻址
栈:
高地址 参数 2 [%rbp + 24]
参数 1 [%rbp + 16]
返回地址 [%rbp + 8]
%rbp -> 旧 %rbp [%rbp]
局部变量 1 [%rbp - 8]
局部变量 2 [%rbp - 16]
%rsp -> ...
低地址
使用场景对比:
- 适合 RIP 相对寻址的场景
# 1. 访问全局变量
movq global_var(%rip), %rax
# 2. 加载字符串常量
leaq string_const(%rip), %rdi
# 3. 访问静态数组
leaq static_array(%rip), %rsi
# 4. 跳转表
leaq jump_table(%rip), %rax
- 适合基址寻址的场景
# 1. 函数参数访问
movq 16(%rbp), %rax # 第一个栈参数
# 2. 局部变量访问
movq -8(%rbp), %rax # 局部变量
# 3. 数组索引
movq (%rbx,%rcx,8), %rax # 访问数组元素
# 4. 结构体成员访问
movq 8(%rdi), %rax # 访问结构体字段
示例:
# RIP 相对寻址:全局数据访问
.section .data
global_var:
.quad 123
.section .text
func:
movq global_var(%rip), %rax # 访问全局变量
# 基址寻址:局部变量访问
func:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq $1, -8(%rbp) # 访问局部变量
movq 16(%rbp), %rax # 访问参数
LEAQ #
leaq(Load Effective Address Quadword) 指令计算地址但不访问内存,常用于:
- 地址计算
- 简单算术运算
- 指针操作
例子:
# 基本用法
leaq (%rdi), %rax # 相当于 move %rdi, %rax
leaq 8(%rdi), %rax # rax = rdi + 8
leaq (%rdi,%rsi), %rax # rax = rdi + rsi
leaq (%rdi,%rsi,4), %rax # rax = rdi + rsi * 4
# 用于算术运算
leaq (%rdi,%rdi,2), %rax # rax = rdi * 3
leaq (%rdi,%rdi,4), %rax # rax = rdi * 5
# 数组寻址
leaq array(,%rdi,4), %rax # rax = &array[rdi]
# RIP相对寻址
leaq message(%rip), %rdi # 加载字符串地址
算术运算指令 #
# 加法
addq $1, %rax # rax = rax + 1
addq %rbx, %rax # rax = rax + rbx
# 减法
subq $1, %rax # rax = rax - 1
subq %rbx, %rax # rax = rax - rbx
# 乘法
imulq $2, %rax # rax = rax * 2
imulq %rbx, %rax # rax = rax * rbx
# 除法
idivq %rbx # rdx:rax / rbx,商在rax,余数在rdx
PUSH/POP 栈指令 #
# 栈操作
pushq %rax # 压栈
popq %rbx # 出栈
# 多寄存器操作
pushq %rbp
pushq %rbx
# ...
popq %rbx
popq %rbp
另外,函数调用的 call
,leave
,ret
也对栈进行操作:
call
: 将当前 EIP 寄存器值压栈,然后跳转到被调用函数地址执行;leave
: 相当于 movq %rbp, %rsp;popq %rbp;ret
: 从栈弹出值保存到 EIP ,然后调整到对应函数地址处执行。
逻辑运算指令 #
# 与运算
andq $0xF, %rax # rax = rax & 0xF
# 或运算
orq $0xF, %rax # rax = rax | 0xF
# 异或运算
xorq %rax, %rax # 清零rax
# 移位
shlq $1, %rax # 左移1位
shrq $1, %rax # 右移1位
比较和跳转指令 #
# 比较
cmpq $10, %rax # 比较rax与10
cmpq %rbx, %rax # 比较rax与rbx
testq %rax, %rax # 测试 rax(常用于检查零)
# 设置条件码
setl %al # 如果小于则设置
setg %al # 如果大于则设置
sete %al # 如果相等则设置
# 无条件跳转
jmp label # 跳转到label
# 条件跳转
je label # 相等时跳转
jne label # 不相等时跳转
jg label # 大于时跳转
jge label # 大于等于时跳转
jl label # 小于时跳转
jle label # 小于等于时跳转
位运算 #
# 与运算
andq %rbx, %rax # rax &= rbx
# 或运算
orq %rbx, %rax # rax |= rbx
# 异或运算
xorq %rax, %rax # 清零 rax
xorq %rbx, %rax # rax ^= rbx
# 移位
shlq $1, %rax # 左移1位 (乘2)
shrq $1, %rax # 右移1位 (除2)
sarq $1, %rax # 算术右移
CALL/RET 函数调用和返回 #
CALL 指令执行两个主要操作:
- 将下一条指令的地址(返回地址)压入栈
- 跳转到目标函数的地址
执行前:
%rip -> 当前指令(call)
%rsp -> 栈顶
执行后:
%rip -> 目标函数地址
%rsp -> 栈顶 - 8
[%rsp] = 返回地址
调用前:
| |
|--------------|
%rsp ->| |
|--------------|
调用后:
| |
|--------------|
| 返回地址 | <-- 压入函数返回地址,及 call 指令下一条指令地址
%rsp ->| |
|--------------|
CALL 指令的类型:
# 1. 直接调用(使用标签)
call function_name
# 2. 间接调用(通过寄存器)
call *%rax
# 3. 间接调用(通过内存地址)
call *(%rax)
RET 指令执行两个主要操作:
- 从栈中弹出返回地址
- 跳转到返回地址
执行前:
%rip -> ret指令
%rsp -> 返回地址
执行后:
%rip -> 返回地址
%rsp -> 栈顶 + 8
返回前:
| |
|--------------|
%rsp ->| 返回地址 |
|--------------|
| |
返回后:
| |
|--------------|
%rsp ->| | <-- 弹出返回地址
|--------------|
| |
示例:
# 函数调用
call function # 调用函数,将当前 EIP 压栈
ret # 返回,从栈弹出压栈返回地址到 EIP
# 调用示例
call *%rax # 间接调用
call *(%rax) # 通过内存中的地址调用
示例:计算斐波那契数列 #
.section .text
.globl fib
fib:
pushq %rbp
movq %rsp, %rbp
cmpq $2, %rdi # 检查n是否小于2
jge .L2
movq %rdi, %rax # 返回n
jmp .L3
.L2:
pushq %rbx # 保存被调用者保存的寄存器
movq %rdi, %rbx # 保存n
subq $1, %rdi # 计算fib(n-1)
call fib
movq %rax, %rsi # 保存fib(n-1)的结果
movq %rbx, %rdi
subq $2, %rdi # 计算fib(n-2)
call fib
addq %rsi, %rax # fib(n-1) + fib(n-2)
popq %rbx # 恢复寄存器
.L3:
movq %rbp, %rsp
popq %rbp
ret
系统调用 #
参考:
- https://akaedu.github.io/book/ch19s01.html
- https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_Calling_Conventions
Linux x86_64 的系统调用号和参数传递约定:
- 系统调用号放在 %rax
- 参数依次放在 %rdi, %rsi, %rdx, %r10, %r8, %r9
- 使用 syscall 指令进行系统调用
常用系统调用:
sys_read = 0
sys_write = 1
sys_open = 2
sys_close = 3
sys_exit = 60
示例:
.section .data
msg:
.ascii "Hello, World!\n"
len = . - msg
.section .text
.globl _start
_start:
# write(1, msg, len)
movq $1, %rax # sys_write
movq $1, %rdi # stdout
movq $msg, %rsi # message
movq $len, %rdx # length
syscall
# exit(0)
movq $60, %rax # sys_exit
xorq %rdi, %rdi # status = 0
syscall
函数调用和栈帧 #
栈帧(Stack Frame)是为每个函数调用分配的一块连续内存区域,用于存储:
- 局部变量
- 保存的寄存器值
- 函数参数
- 返回地址
栈帧涉及两个寄存器:
- %rsp: 始终指向栈顶
- %rbp: 指向当前栈帧的基址,用于定位局部变量和参数
Intel 是小端模式,对于 32 位值,低地址保存低部分值。
栈帧布局 #
调用者栈帧: %rbp 寄存器指向的内存地址及以上的地址。
当前栈帧:%rsp 寄存器指向的内存地址到 %rbp 指向的内存内置。
高地址
| |
|--------------------------------|
| 栈参数 (第7个及以后) |
|--------------------------------|
| 函数返回地址 |
|--------------------------------|
| 保存的 %rbp | <-- %rbp
|--------------------------------|
| 局部变量 |
| ... |
|--------------------------------|
| 临时数据/spill区域 |
|--------------------------------|
| 被调用者保存的寄存器 | <-- %rsp
低地址
栈帧创建过程 #
# 函数序言(Function Prologue)
pushq %rbp # 保存栈基址
movq %rsp, %rbp # 设置新的栈基址
subq $16, %rsp # 分配栈空间,保存函数内变量
# ... 函数体 ...
# 函数结尾(Function Epilogue)
movq %rbp, %rsp # 恢复栈指针
popq %rbp # 恢复栈基址
ret # 返回
详细过程:
1. 初始状态:
%rbp -> | 旧帧指针 |
| ... |
%rsp -> | |
2. pushq %rbp 后:
%rbp -> | 旧帧指针 |
| ... |
| 旧 %rbp | <-- %rsp
3. movq %rsp, %rbp 后:
| 旧帧指针 |
| ... |
%rbp -> | 旧 %rbp | <-- %rsp
4. subq $16, %rsp 后:
| 旧帧指针 |
| ... |
%rbp -> | 旧 %rbp |
| 局部变量 |
| 局部变量 |
%rsp -> | |
调用者职责 #
调用者在调用函数前需要:
pushq %r10 # 保存调用者保存的寄存器
pushq %r11
设置参数,有两种方式 …
- 通过寄存器传参
- 寄存器+压栈传参:当 6 个寄存器用完时,从第 7 个参数开始使用压栈传参。
调用函数:
call function
函数返回后:
addq $16, %rsp # 可选:清理压栈传递的参数,$16 的值取决于压栈传参的大小
popq %r11 # 可选:恢复调用者保存的寄存器, 如 r11 和 r10
popq %r10
被调用者职责 #
function:
# 函数序言
pushq %rbp
movq %rsp, %rbp
# 保存被调用者保存的寄存器
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
# ... 函数体 ...
# 恢复寄存器和返回
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
# 函数尾声
movq %rbp, %rsp
popq %rbp
ret
寄存器传参 #
System V AMD64 ABI 调用约定,参数按顺序使用以下寄存器:
第1个参数:%rdi
第2个参数:%rsi
第3个参数:%rdx
第4个参数:%rcx
第5个参数:%r8
第6个参数:%r9
更多参数:通过栈传递(从右向左压栈)
浮点数参数:按照顺序使用 XMM 寄存器:
第1个参数:%xmm0
第2个参数:%xmm1
第3个参数:%xmm2
第4个参数:%xmm3
第5个参数:%xmm4
第6个参数:%xmm5
第7个参数:%xmm6
第8个参数:%xmm7
额外参数:通过栈传递
返回值存放在 %rax
调用者保存: %rcx, %rdx, %rsi, %rdi, %r8-r11
被调用者保存: %rbx, %rbp, %r12-r15
示例:
# 调用函数 func(1, 2, 3, 4, 5, 6, 7, 8)
movq $1, %rdi # 第1个参数
movq $2, %rsi # 第2个参数
movq $3, %rdx # 第3个参数
movq $4, %rcx # 第4个参数
movq $5, %r8 # 第5个参数
movq $6, %r9 # 第6个参数
pushq $8 # 第8个参数(先压栈,压栈的顺序是从右至左)
pushq $7 # 第7个参数
call func # 调用函数
addq $16, %rsp # 清理压栈传递的参数
# 另一个例子:
# void func(int a, double b, long c, float d)
# a in %rdi
# b in %xmm0
# c in %rdx
# d in %xmm1
.globl func
func:
pushq %rbp
movq %rsp, %rbp
# 使用参数
movq %rdi, -8(%rbp) # 保存整数参数 a
movsd %xmm0, -16(%rbp) # 保存双精度浮点数 b
movq %rdx, -24(%rbp) # 保存长整型 c
movss %xmm1, -28(%rbp) # 保存单精度浮点数 d
结构体传递示例:
# struct Point { int x; int y; };
# void func(struct Point p)
func:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
# 结构体通过寄存器传递(如果大小<=16字节)
movq %rdi, -16(%rbp) # 保存整个结构体
# 访问结构体成员
movl -16(%rbp), %eax # 访问 x
movl -12(%rbp), %edx # 访问 y
参数访问:一般通过相对于 %rbp 的偏移量来访问(基址寻址):
- (%rbp): 保存旧 %rbp 寄存器值;
- 8(%rbp): 保存函数返回地址
# 访问第一个参数
movq 16(%rbp), %rax # 从 rbp+16 的位置读取
# 访问第二个参数
movq 24(%rbp), %rax # 从 rbp+24 的位置读取
可变参数:
# int sum(int count, ...)
sum:
pushq %rbp
movq %rsp, %rbp
# 可变参数通过寄存器和栈传递
# 需要保存 %al 中的 XMM 寄存器数量
# 访问可变参数
movq %rsi, -8(%rbp) # 第一个可变参数
movq %rdx, -16(%rbp) # 第二个可变参数
# ...
Red Zone:
- 函数可以使用返回地址之下的128字节而无需调整栈指针
- 不适用于信号处理程序
func:
# 可以直接使用 red zone
movq %rdi, -8(%rsp) # 安全
movq %rsi, -16(%rsp) # 安全
# ... 最多使用到 -128(%rsp)
局部变量 #
参考:https://akaedu.github.io/book/ch19s03.html
局部变量在栈上分配,使用 %rbp 相对寻址:
局部变量分配示例:
.globl function
function:
pushq %rbp
movq %rsp, %rbp
# 分配局部变量空间
subq $32, %rsp # 分配32字节的局部变量空间
# 初始化局部变量
movq $0, -8(%rbp) # 第1个8字节变量
movq $0, -16(%rbp) # 第2个8字节变量
movl $0, -20(%rbp) # 第3个4字节变量
movb $0, -21(%rbp) # 第4个1字节变量
# 对齐到16字节
andq $-16, %rsp
一般使用相对于 %rbp 寄存器的偏移来访问:
# 访问第一个局部变量(假设是8字节)
movq -8(%rbp), %rax # 从 rbp-8 的位置读取
# 访问第二个局部变量
movq -16(%rbp), %rax # 从 rbp-16 的位置读取
返回值 #
返回值约定:
- 整数/指针返回值:%rax
- 浮点数返回值: %xmm0
- 大型结构体(超过 16 Bytes): 通过栈或指针返回
示例:
# 返回两个值的函数
.globl get_two_values
get_two_values:
movq $1, %rax # 第一个返回值
movq $2, %rdx # 第二个返回值(约定)
ret
小于等于16字节的结构体,返回值通过 rax:rdx 传递:
# struct SmallStruct { long a; long b; };
func:
movq $1, %rax # a
movq $2, %rdx # b
ret
大于16字节的结构体,通过隐藏参数(指针)返回,caller 分配空间,地址作为第一个参数传入
# long calc(int a, int b)
.globl calc
calc:
pushq %rbp
movq %rsp, %rbp
# 计算 a + b
movl %edi, %eax # 第一个参数
addl %esi, %eax # 加上第二个参数
# 返回值已在 %eax 中
movq %rbp, %rsp
popq %rbp
ret
# 返回结构体示例
# struct Point { int x, y; } get_point()
get_point:
pushq %rbp
movq %rsp, %rbp
# 返回 {1, 2}
movq $0, %rax # 清零 rax
movl $1, %eax # 设置 x
movl $2, %edx # 设置 y
movq %rbp, %rsp
popq %rbp
ret
示例 #
.globl example_function
example_function:
# 函数序言
pushq %rbp # 保存旧的帧指针
movq %rsp, %rbp # 设置新的帧指针
subq $16, %rsp # 分配16字节的局部变量空间(不含后续保存的寄存器)
# 保存被调用者保存的寄存器
pushq %rbx
pushq %r12
# 函数主体
# ... 函数代码 ...
# 恢复寄存器
popq %r12
popq %rbx
# 函数尾声
movq %rbp, %rsp # 恢复栈指针
popq %rbp # 恢复帧指针
ret # 返回
递归函数示例(计算斐波那契数):
.globl fib
fib:
# 函数序言
pushq %rbp
movq %rsp, %rbp
pushq %rbx # 保存被调用者保存的寄存器
# 检查基础情况
cmpq $2, %rdi
jge .L2
# n < 2 的情况,直接返回n
movq %rdi, %rax
jmp .L3
.L2:
# 保存n
movq %rdi, %rbx
# 计算fib(n-1)
decq %rdi
call fib
movq %rax, %r12 # 保存fib(n-1)的结果
# 计算fib(n-2)
movq %rbx, %rdi
subq $2, %rdi
call fib
# 计算结果
addq %r12, %rax
.L3:
# 函数尾声
popq %rbx
movq %rbp, %rsp
popq %rbp
ret
带局部变量的函数示例:
.globl calculate
calculate:
# 函数序言
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp # 分配两个局部变量的栈空间
# 保存通过寄存器传递的参数到栈局部变量
movq %rdi, -8(%rbp) # 第一个局部变量
movq %rsi, -16(%rbp) # 第二个局部变量
# 计算过程
movq -8(%rbp), %rax
addq -16(%rbp), %rax # 函数返回值保存在 rax 中
# 函数尾声
movq %rbp, %rsp
popq %rbp
ret # 此时 %rsp 指向的地址保存有函数返回地址
简单函数调用:
.section .text
.globl main
main:
# 调用前的准备
pushq %rbp # 保存旧的帧指针
movq %rsp, %rbp # 设置新的帧指针
call function # 调用函数,此时返回地址被压入栈
# 函数返回后继续执行
movq %rbp, %rsp
popq %rbp
ret
function:
pushq %rbp
movq %rsp, %rbp
# 函数体
movq %rbp, %rsp
popq %rbp
ret # 返回到调用点
带参数的函数调用:
# 函数调用示例:func(1, 2)
movq $1, %rdi # 第一个参数,前六个参数通过寄存器传参
movq $2, %rsi # 第二个参数
call func # 调用函数
# 返回值在 %rax 中
func:
pushq %rbp
movq %rsp, %rbp
# 使用 %rdi 和 %rsi 中的参数
movq %rbp, %rsp
popq %rbp
ret
多层函数调用示例:
调用链:main -> func1 -> func2
栈的状态:
高地址
| main参数 |
|------------------|
| main返回地址 |
| 保存的rbp |
|------------------|
| func1返回地址 |
| 保存的rbp |
|------------------|
| func2返回地址 |
| 保存的rbp |
%rsp ->| |
低地址
多函数调用示例:
# 定义结构体 (仅作参考)
# struct LargeStruct {
# long a[8]; # 64字节
# int b; # 4字节
# };
#
# struct Point {
# int x, y; # 8字节
# };
#
# 函数原型:
# int main();
# int func1(int a, char b, const char* str);
# long func2(int a, struct LargeStruct big, struct Point* p,
# char c, double d, const char* str);
.section .data
str1:
.string "Hello"
str2:
.string "World"
.section .text
.globl main
main:
# 函数序言
pushq %rbp
movq %rsp, %rbp
subq $144, %rsp # 分配栈空间,包括对齐和调用 func2 时压栈传递的参数
# 保存被调用者保存的寄存器
pushq %rbx
pushq %r12
pushq %r13
# 为 func1 准备参数
movl $42, %edi # int a = 42
movb $'A', %sil # char b = 'A'
leaq str1(%rip), %rdx # const char* str = "Hello"
# 调用 func1
call func1
movl %eax, %r12d # 保存 func1 的返回值
# 为 func2 准备参数
# 首先在栈上构建 LargeStruct
movq $1, -72(%rbp) # big.a[0]
movq $2, -64(%rbp) # big.a[1]
movq $3, -56(%rbp) # big.a[2]
movq $4, -48(%rbp) # big.a[3]
movq $5, -40(%rbp) # big.a[4]
movq $6, -32(%rbp) # big.a[5]
movq $7, -24(%rbp) # big.a[6]
movq $8, -16(%rbp) # big.a[7]
movl $9, -12(%rbp) # big.b
# 构建 Point 结构体
subq $16, %rsp # 为 Point 分配栈空间
movl $10, (%rsp) # p->x = 10
movl $20, 4(%rsp) # p->y = 20
movq %rsp, %r13 # 保存 Point 指针
# 准备 func2 的参数
movl $100, %edi # int a = 100
# LargeStruct 通过引用传递(隐式)
leaq -72(%rbp), %rsi # struct LargeStruct*
movq %r13, %rdx # struct Point* p
movb $'B', %cl # char c = 'B'
movsd .LC0(%rip), %xmm0 # double d = 3.14
leaq str2(%rip), %r9 # const char* str = "World"
# 调用 func2
call func2
# 恢复栈和寄存器
addq $16, %rsp # 清理 Point 结构体空间
popq %r13
popq %r12
popq %rbx
# 函数尾声
movq %rbp, %rsp
popq %rbp
ret
# 第一个函数
.globl func1
func1:
pushq %rbp
movq %rsp, %rbp
# 函数体 (简单示例)
movl %edi, %eax # 返回第一个参数
movq %rbp, %rsp
popq %rbp
ret
# 第二个函数
.globl func2
func2:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp # 分配本地变量空间
# 访问参数示例
# %edi 已经包含 int a
# %rsi 包含 LargeStruct 的指针
# %rdx 包含 Point 的指针
# %cl 包含 char c
# %xmm0 包含 double d
# %r9 包含 string 指针
# 函数体 (简单示例)
movq (%rdx), %rax # 返回 Point 的 x 值
movq %rbp, %rsp
popq %rbp
ret
.section .rodata
.LC0:
.double 3.14159 # 双精度浮点常量
详细说明:
-
参数传递说明:
- func1 使用常规寄存器传递
- func2 的参数较复杂:
- int a 通过 %edi 传递
- LargeStruct 通过引用传递(指针在 %rsi)
- Point* 通过 %rdx 传递
- char c 通过 %cl 传递
- double d 通过 %xmm0 传递
- const char* 通过 %r9 传递
-
结构体处理:
- LargeStruct 因为超过 64 字节,所以隐式通过引用传递
- Point 结构体指针直接通过寄存器传递
-
栈帧管理:
- main 函数分配足够空间用于本地变量和临时结构体
- 保持16字节对齐
- 正确保存和恢复被调用者保存的寄存器
-
内存布局:
- 字符串常量在 .data 段
- 浮点常量在 .rodata 段
- 临时结构体在栈上构建
-
寄存器使用:
- 遵循 System V AMD64 ABI 调用约定
- 正确处理参数传递
- 适当保存和恢复寄存器
-
安全性考虑:
- 正确的栈对齐
- 适当的栈空间分配
- 寄存器的保存和恢复
调用过程图解:
- 初始状态(程序刚启动)
高地址
|-------------------|
| 环境变量等 |
|-------------------|
| 命令行参数 |
|-------------------|
| 返回地址 |
%rsp ->|-------------------|
低地址
- main 函数开始
高地址
|-------------------|
| 返回地址 |
|-------------------|
| 旧 %rbp | <-- %rbp
|-------------------|
| %rbx |
| %r12 |
| %r13 |
|-------------------|
| |
| LargeStruct |
| (72字节) |
| |
|-------------------|
| 对齐填充 |
%rsp ->|-------------------|
低地址
- 调用 func1 前的准备
|-------------------|
| main栈帧 |
|-------------------|
| 参数1: %edi=42 |
| 参数2: %sil='A' |
| 参数3: %rdx=str1 |
%rsp ->|-------------------|
- func1 执行时
高地址
|-------------------|
| main栈帧 | # 包含通过 stack 为 fun1 传递的参数
|-------------------|
| 返回地址 |
|-------------------|
| 旧 %rbp | <-- %rbp
%rsp ->|-------------------|
低地址
寄存器状态:
%edi = 42
%sil = 'A'
%rdx = str1的地址
- func1 返回后,准备调用 func2
高地址
|-------------------|
| main栈帧 |
|-------------------|
| LargeStruct |
| a[0] = 1 |
| a[1] = 2 |
| a[2] = 3 |
| a[3] = 4 |
| a[4] = 5 |
| a[5] = 6 |
| a[6] = 7 |
| a[7] = 8 |
| b = 9 |
|------------------|
| Point struct |
| x = 10 |
| y = 20 |
%rsp ->|------------------|
低地址
寄存器状态:
%edi = 100 # int a
%rsi = LargeStruct指针 # struct LargeStruct
%rdx = Point指针 # struct Point*
%cl = 'B' # char c
%xmm0 = 3.14159 # double d
%r9 = str2的地址 # const char*
- func2 执行时
高地址
|-------------------|
| main栈帧 |
|-------------------|
| 返回地址 |
|-------------------|
| 旧 %rbp | <-- %rbp
|-------------------|
| 本地变量空间 |
%rsp ->| (16字节) |
|-------------------|
低地址
- 完整的内存布局
高地址
|-------------------|
| 环境变量等 |
|-------------------|
| 命令行参数 |
|-------------------|
| main返回地址 |
|-------------------|
| main旧%rbp |
|-------------------|
| 保存的寄存器 |
| %rbx |
| %r12 |
| %r13 |
|-------------------|
| LargeStruct |
| (72字节) |
|-------------------|
| Point struct |
| (8字节) |
|-------------------|
| func2返回地址 |
|-------------------|
| func2旧%rbp |
|-------------------|
| func2本地变量 |
%rsp ->|-------------------|
低地址
.section .data:
str1: "Hello\0"
str2: "World\0"
.section .rodata:
LC0: 3.14159 (双精度浮点数)
- 数据流图
main()
|
|---> func1(42, 'A', "Hello")
| 返回值在 %eax
|
|---> func2(100, largeStruct, &point, 'B', 3.14, "World")
返回值在 %rax
参数传递流程:
main -> func1:
%edi <-- 42
%sil <-- 'A'
%rdx <-- str1地址
main -> func2:
%edi <-- 100
%rsi <-- LargeStruct指针
%rdx <-- Point指针
%cl <-- 'B'
%xmm0 <-- 3.14159
%r9 <-- str2地址