NOTE
3.1 汇编语言
x86-64 AT&T 汇编、寄存器、寻址、函数调用与 C/Go 反汇编的历史学习笔记。
这是历史学习笔记,可能存在过时或不完整的理解。
2026 注:本文主要按 x86-64 + AT&T 语法理解汇编,但调用约定取决于 ABI。文中既出现了 System V AMD64 的参数寄存器,也包含 Windows x64 的反汇编示例,阅读时应区分平台;Go 的 ABI 也已随版本演进。
1. 汇编语言是什么
计算机能理解和执行的是二进制的机器语言,但是机器语言对人类来说是不可理解的 为了解决这个问题,汇编语言出来了。它与机器指令一一对应
2. 寄存器
2.1. 寄存器名
需要加%作为前缀
2.2. 分类
- %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
- %rbp 是栈帧指针,用于标识当前栈帧的起始位置
- %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数(如果有6个或6个以上参数的话)
- %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中
2.3. caller save / callee save
函数调用子函数需要保存寄存器的值,以便从子函数返回之后恢复
- caller save:由调用者保存寄存器的值到栈上
- callee save:由被调用者保存寄存器的值到栈上
3. 寻址方式
- 寄存器间接寻址的格式为 offset(%register)
4. 汇编指令格式
以AT&T格式的汇编指令为例
操作码 [操作数]- 操作码:指示CPU执行什么操作
- 操作数:操作数是操作的对象
- 操作数的个数一般是0个,1个或2个
- 有2个操作数的指令中,第一个操作数是源操作数,第二个是目的操作数
- 立即操作数需要加上$符号做前缀
- “mov $0x1 %rdi” 这条指令中第一个操作数不是寄存器,也不是内存地址,而是直接写在指令中的一个常数
5. 常用指令
5.1. mov指令
- 令复制源操作数到目的操作数
- 格式
mov 源操作数 目的操作数
- 举例
mov%rsp,%rbp # 直接寻址,把rsp的值拷贝给rbp,相当于 rbp = rsp
mov-0x8(%rbp),%edx# 源操作数间接寻址,目的操作数直接寻址。从内存中读取4个字节到edx寄存器
mov%rsi,-0x8(%rbp) # 源操作数直接寻址,目的操作数间接寻址。把rsi寄存器中的8字节值写入内存
5.2. add/sub指令
- 加减运算指令
- 格式
add 源操作数 目的操作数
sub 源操作数 目的操作数
- 举例
sub$0x350,%rsp # 源操作数是立即操作数,目的操作数直接寻址。rsp = rsp - 0x350
add%rdx,%rax # 直接寻址。rax = rax + rdx
addl$0x1,-0x8(%rbp) # 源操作数是立即操作数,目的操作数间接寻址。内存中的值加1(addl后缀字母l表示操作内存中
5.3. call/ret指令
call:保存返回地址和跳转到子函数处执行
负责把执行call指令时的rip寄存器(函数返回地址)入栈

ret: 负责把call指令入栈的返回地址出栈给rip,从而实现从被调用函数返回到调用函数继续执行;
- 格式
call 目标地址
ret
- 举例
# 2. 调用函数片段
0x0000000000400559: callq 0x400526 <sum>
0x000000000040055e: mov %eax,-0x4(%rbp)
--------------------------------------------------
# 3. 被调用函数片段
0x0000000000400526: push %rbp
......
0x000000000040053f: retq
5.4. jmp/je/jle/jg/jge等等j开头的指令
- 属于跳转指令,操作码后面直接跟要跳转到的地址或存有地址的寄存器,这些指令与高级编程语言中的 goto 和 if 等语句对应
- 举例
jmp 0x4005f2
jle 0x4005ee
jl 0x4005b8
5.5. push/pop指令
- 专用于函数调用栈的入栈出栈指令,这两个指令都会自动修改rsp寄存器。
- 格式
push 源操作数
pop 目的操作数
5.6. leave指令

- leave指令没有操作数,它一般放在函数的尾部ret指令之前,用于调整rsp和rbp,这条指令相当于如下两条指令:
mov %rbp,%rsp # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
pop %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处
6. 举例
6.1. 函数调用
CPU是如何从调用者跳转到被调用函数执行的?
参数是如何从调用者传递给被调用函数的?
函数局部变量所占内存是怎么在栈上分配的?
返回值是如何从被调用函数返回给调用者的?
函数执行完成之后又需要做哪些清理工作?
6.2. C函数
在子函数调用时,执行的操作有:父函数将调用参数从后向前压栈 -> 将返回地址压栈保存 -> 跳转到子函数起始地址执行 -> 子函数将父函数栈帧起始地址(%rpb) 压栈 -> 将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址
6.2.1. hello world
#include <stdio.h>
// 对参数 a 和 b 求和
int sum(int a, int b)
{
int s=a+b;
return s;
}
// main函数:程序入口
int main(int argc, char*argv[])
{
int n=sum(1, 2); // 调用sum函数对求和
printf("n: %d\n", n); //在屏幕输出 n 的值
return 0;
}
6.2.2. gcc编译
gcc call.c -o call.exe
6.2.3. 使用gdb调试
gdb .\call.exe
6.2.4. 反汇编main
(gdb) disass main
Dump of assembler code for function main:
// 函数的调用
0x00000001004010a2 <+0>: push %rbp # 保存旧的帧指针,相当于创建新的栈帧
0x00000001004010a3 <+1>: mov %rsp,%rbp # 让 %rbp 指向新栈帧的起始位置
0x00000001004010a6 <+4>: sub $0x30,%rsp # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位
0x00000001004010aa <+8>: mov %ecx,0x10(%rbp)
0x00000001004010ad <+11>: mov %rdx,0x18(%rbp)
0x00000001004010b1 <+15>: callq 0x100401100 <__main>
0x00000001004010b6 <+20>: mov $0x2,%edx
0x00000001004010bb <+25>: mov $0x1,%ecx
0x00000001004010c0 <+30>: callq 0x100401080 <sum>
0x00000001004010c5 <+35>: mov %eax,-0x4(%rbp)
0x00000001004010c8 <+38>: mov -0x4(%rbp),%eax
0x00000001004010cb <+41>: mov %eax,%edx
// 函数的返回
0x00000001004010cd <+43>: lea 0x1f2c(%rip),%rcx # 0x100403000
0x00000001004010d4 <+50>: callq 0x100401110 <printf>
0x00000001004010d9 <+55>: mov $0x0,%eax
0x00000001004010de <+60>: add $0x30,%rsp
0x00000001004010e2 <+64>: pop %rbp
0x00000001004010e3 <+65>: retq
0x00000001004010e4 <+66>: nop
0x00000001004010e5 <+67>: nop
0x00000001004010e6 <+68>: nop
0x00000001004010e7 <+69>: nop
0x00000001004010e8 <+70>: nop
0x00000001004010e9 <+71>: nop
0x00000001004010ea <+72>: nop
0x00000001004010eb <+73>: nop
0x00000001004010ec <+74>: nop
0x00000001004010ed <+75>: nop
0x00000001004010ee <+76>: nop
0x00000001004010ef <+77>: nop
End of assembler dump.
- 格式如下:
- 指令地址
- 指令相对于当前函数起始地址以字节为单位的偏移
- 指令
6.2.5. 给main第一条指令打断点后运行
(gdb) b *0x00000001004010a2 //打断点
Breakpoint 1 at 0x1004010a2
(gdb) r //运行
Starting program: /c/Users/user/code/test/call.exe
[New Thread 12648.0x738]
[New Thread 12648.0x421c]
[New Thread 12648.0x20b0]
[New Thread 12648.0x123c]
[New Thread 12648.0x254]
[New Thread 12648.0x14d4]
Thread 1 "call" hit Breakpoint 1, 0x00000001004010a2 in main () //停在了第一条指令处
6.2.6. 再次反汇编main函数
(gdb) disass
Dump of assembler code for function main:
=> 0x00000001004010a2 <+0>: push %rbp
0x00000001004010a3 <+1>: mov %rsp,%rbp
0x00000001004010a6 <+4>: sub $0x30,%rsp
//...
- 前面的三条指令叫做函数序言,用于保存调用者的rbp寄存器以及为当前函数分配栈空间
=>表示CPU将要执行的下一条指令
6.2.7. 查看所有寄存器的值
(gdb) i r
(gdb) i r
rax 0x100000000 4294967296
rbx 0x180239780 6444783488
rcx 0x1 1
rdx 0xffffcc60 4294954080
rsi 0x0 0
rdi 0x20 32
rbp 0xffffcce0 0xffffcce0
rsp 0xffffcc18 0xffffcc18
r8 0x80003a790 34359977872
r9 0x0 0
r10 0x180239f20 6444785440
r11 0x0 0
r12 0xffffcc60 4294954080
r13 0xffffcc70 4294954096
r14 0xffffcc70 4294954096
r15 0xffffcc97 4294954135
rip 0x1004010a2 0x1004010a2 <main>
--Type <RET> for more, q to quit, c to continue without paging--
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x53 83
gs 0x2b 43
- rip的值是0x1004010a2,指向下一条执行的指令
- rsp的值是0xffffcc18,指向栈顶
- rbp的值是0xffffcce0,指向当前栈帧的起始地址
6.3. Go函数
6.3.1. hello world
package main
//计算a, b的平方和
func sum(a, b int) int {
a2 := a * a
b2 := b * b
c := a2 + b2
return c
}
func main() {
sum(1, 2)
}
- go build
go build -gcflags "-N -l" call2.go
6.3.2. 使用gdb调试
gdb .\call2.exe
6.3.3. 给main打断点
(gdb) break main.main
Breakpoint 1 at 0x455a96: file C:/Users/user/code/tmp/call2.go, line 12.
6.3.4. 运行
(gdb) r
Starting program: C:\Users\user\code\tmp\call2.exe
[New Thread 4416.0x27f4]
[New Thread 4416.0x2dc0]
[New Thread 4416.0x2cac]
[New Thread 4416.0x1bac]
[New Thread 4416.0x6c8]
[New Thread 4416.0x208]
[New Thread 4416.0x18e8]
[New Thread 4416.0xb68]
Thread 1 hit Breakpoint 1, main.main () at C:/Users/user/code/tmp/call2.go:12
12 func main() {
6.3.5. 反汇编
(gdb) disass main.main
Dump of assembler code for function main.main:
0x0000000000455a80 <+0>: mov %gs:0x28,%rcx
0x0000000000455a89 <+9>: mov 0x0(%rcx),%rcx
0x0000000000455a90 <+16>: cmp 0x10(%rcx),%rsp
0x0000000000455a94 <+20>: jbe 0x455ac4 <main.main+68>
=> 0x0000000000455a96 <+22>: sub $0x20,%rsp #为main函数预留32字节栈空间
0x0000000000455a9a <+26>: mov %rbp,0x18(%rsp) #保存调用者的rbp寄存器
0x0000000000455a9f <+31>: lea 0x18(%rsp),%rbp #调整rbp使其指向main函数栈帧开始地址
0x0000000000455aa4 <+36>: movq $0x1,(%rsp) #sum函数的第一个参数(1)入栈
0x0000000000455aac <+44>: movq $0x2,0x8(%rsp) #sum函数的第二个参数(2)入栈
0x0000000000455ab5 <+53>: callq 0x455a20 <main.sum> #调用sum函数
0x0000000000455aba <+58>: mov 0x18(%rsp),%rbp #恢复rbp寄存器的值为调用者的rbp
0x0000000000455abf <+63>: add $0x20,%rsp #调整rsp使其指向保存有调用者返回地址的栈单元
0x0000000000455ac3 <+67>: retq #返回到调用者
0x0000000000455ac4 <+68>: callq 0x44ae90 <runtime.morestack_noctxt>
0x0000000000455ac9 <+73>: jmp 0x455a80 <main.main>
End of assembler dump.
(gdb) disass main.sum
Dump of assembler code for function main.sum:
0x0000000000455a20 <+0>: sub $0x20,%rsp #为sum函数预留32字节的栈空间
0x0000000000455a24 <+4>: mov %rbp,0x18(%rsp) #保存main函数的rbp
0x0000000000455a29 <+9>: lea 0x18(%rsp),%rbp #设置sum函数的rbp
0x0000000000455a2e <+14>: movq $0x0,0x38(%rsp) #返回值初始化为0
0x0000000000455a37 <+23>: mov 0x28(%rsp),%rax #从内存中读取第一个参数a(1)到rax
0x0000000000455a3c <+28>: mov 0x28(%rsp),%rcx #从内存中读取第一个参数a(1)到rcx
0x0000000000455a41 <+33>: imul %rax,%rcx #计算a * a,并把结果放在rcx
0x0000000000455a45 <+37>: mov %rcx,0x10(%rsp) #把rcx的值(a * a)赋值给变量a2
0x0000000000455a4a <+42>: mov 0x30(%rsp),%rax #从内存中读取第二个参数a(2)到rax
0x0000000000455a4f <+47>: mov 0x30(%rsp),%rcx #从内存中读取第二个参数a(2)到rcx
0x0000000000455a54 <+52>: imul %rax,%rcx #计算b * b,并把结果放在rcx
0x0000000000455a58 <+56>: mov %rcx,0x8(%rsp) #把rcx的值(b * b)赋值给变量b2
0x0000000000455a5d <+61>: mov 0x10(%rsp),%rax #从内存中读取a2到寄存器rax
0x0000000000455a62 <+66>: add %rcx,%rax #计算a2 + b2,并把结果保存在rax
0x0000000000455a65 <+69>: mov %rax,(%rsp) #把rax赋值给变量c, c = a2 + b2
0x0000000000455a69 <+73>: mov %rax,0x38(%rsp) #将rax的值(a2 + b2)复制给返回值
0x0000000000455a6e <+78>: mov 0x18(%rsp),%rbp #恢复main函数的rbp
0x0000000000455a73 <+83>: add $0x20,%rsp #调整rsp使其指向保存有返回地址的栈单元
0x0000000000455a77 <+87>: retq #返回main函数
End of assembler dump.
6.4. 两者对比
6.4.1. 参数传递
- C语言
- 六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递;
- 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序依次存入栈中;
- go语言
- 参数是通过栈传递给被调用函数的,最后一个参数最先入栈,第一个参数最后入栈,参数在调用者的栈帧之中,被调用函数通过rsp加一定的偏移量来获取参数
6.4.2. 返回值
- C语言
- 使用rax寄存器返回函数调用的返回值
- go语言
- 使用栈返回函数调用的返回值。
6.4.3. 局部变量
- C语言
- 通过rbp加偏移量的方式来访问局部和临时变量
- go语言
- 使用rsp寄存器加偏移量的方式来访问它们;