NOTE

3.1 汇编语言

x86-64 AT&T 汇编、寄存器、寻址、函数调用与 C/Go 反汇编的历史学习笔记。

Computer Architecture & Assembly创建于 更新于 historical

这是历史学习笔记,可能存在过时或不完整的理解。

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寄存器加偏移量的方式来访问它们;

7. 参考