x86 汇编入门:用 GDB 看懂你的 C 程序在干什么

1980 字
10 分钟
x86 汇编入门:用 GDB 看懂你的 C 程序在干什么

你写了一段 C 代码,编译运行一气呵成,结果也完全正确——但你对它到底”怎么跑的”其实一无所知。

不是说理解逻辑层面的流程就够了——你知道变量存在内存的哪个位置吗?函数调用时参数是怎么传过去的?return 背后发生了什么?如果你对这些问题的回答还是”大概是用栈吧”这种模糊答案,那你其实一直隔着一层雾在编程。

GDB 不只是用来 debug 的。它的反汇编功能可以把 C 代码踩碎了给你看——每一条语句对应几条汇编指令、寄存器的值怎么变、栈怎么推怎么弹,全都能一步步跟。今天就用一个最简单的例子,带你走进 C 程序的底层世界。

适合人群: 学过 C 基本语法,想深入了解底层运行机制的人
前置知识: 知道 C 语言的函数、指针概念即可
你需要: gcc、gdb(没有的话 apt install gdb 装一个)


一、准备一个目标程序#

创建一个文件 add.c

#include <stdio.h>
int add(int a, int b) {
int sum = a + b;
return sum;
}
int main() {
int x = 10;
int y = 20;
int result = add(x, y);
printf("Result: %d\n", result);
return 0;
}

然后编译,注意两个关键参数:

Terminal window
gcc -g -O0 -o add add.c

-g 把调试符号打包进二进制,这样 GDB 才能把地址和源码行号对应起来。-O0 关掉优化——优化后的汇编跟你写的 C 代码基本对不上号,初学阶段先关了。


二、disassemble:把 C 函数翻译成汇编#

启动 GDB 加载程序:

Terminal window
gdb ./add

在 GDB 里输入:

(gdb) break main
(gdb) run

程序停在 main 函数入口。现在输入:

(gdb) disassemble

你看到的输出大概是这样(地址偏移可能略有不同):

Dump of assembler code for function main:
0x0000000000001169 <+0>: push %rbp
0x000000000000116a <+1>: mov %rsp,%rbp
0x000000000000116d <+4>: sub $0x20,%rsp
0x0000000000001171 <+8>: movl $0xa,-0x4(%rbp)
0x0000000000001178 <+15>: movl $0x14,-0x8(%rbp)
0x000000000000117f <+22>: mov -0x8(%rbp),%edx
0x0000000000001182 <+25>: mov -0x4(%rbp),%eax
0x0000000000001185 <+28>: mov %edx,%esi
0x0000000000001187 <+30>: mov %eax,%edi
0x0000000000001189 <+32>: call 0x1139 <add>
0x000000000000118e <+37>: mov %eax,-0xc(%rbp)
0x0000000000001191 <+40>: mov -0xc(%rbp),%eax
0x0000000000001194 <+43>: mov %eax,%esi
0x0000000000001196 <+45>: lea 0xe67(%rip),%rdi
0x000000000000119d <+52>: mov $0x0,%eax
0x00000000000011a2 <+57>: call 0x1030 <printf@plt>
0x00000000000011a7 <+62>: mov $0x0,%eax
0x00000000000011ac <+67>: leave
0x00000000000011ad <+68>: ret

看着眼晕很正常。一行一行拆开看,其实规律非常清晰。


三、函数开头三件套#

每个函数的开头几乎都是这三条:

push %rbp # 保存调用者的 rbp
mov %rsp,%rbp # 设置当前函数的栈帧基址
sub $0x20,%rsp # 为局部变量腾出 32 字节空间

rbp 是栈基址寄存器,指向当前函数栈帧的底部。rsp 是栈顶指针,始终指向栈的当前位置。

函数进来第一件事就是把上一个函数的 rbp 存起来,然后把自己的栈帧搭好。sub $0x20, %rsp 就是在栈上挖一块 32 字节的空间——main 函数有 3 个局部变量(x, y, result),32 字节绰绰有余。


四、局部变量在栈上怎么存的#

movl $0xa,-0x4(%rbp) # rbp-4 = 10 ← x
movl $0x14,-0x8(%rbp) # rbp-8 = 20 ← y

0xa 是 10 的十六进制,0x14 是 20。这两条指令就是把 10 和 20 写到栈上,分别放在 rbp 往下 4 字节和 8 字节的位置。

局部变量在栈上,地址从 rbp 往下分配。 这就是为什么局部变量的地址比你想象的低——往下是栈的生长方向。

mov -0x8(%rbp),%edx # edx = y (20)
mov -0x4(%rbp),%eax # eax = x (10)
mov %edx,%esi # 第二个参数 = y
mov %eax,%edi # 第一个参数 = x

这是在准备调用 add 函数的参数。在 Linux x86-64 上,函数调用的前两个参数放在 rdi 和 rsi 寄存器里(第三个放 rdx,第四个放 rcx,以此类推)。这就是 System V AMD64 ABI 调用约定。


五、call 指令背后发生了什么#

call 0x1139 <add>

call 指令做两件事:

  1. 返回地址(也就是 call 的下一条指令的地址 0x118e)压入栈
  2. 跳转到 add 函数的地址

你现在在 add 函数里做个断点,看看里面:

(gdb) break add
(gdb) continue
(gdb) disassemble
Dump of assembler code for function add:
0x0000000000001139 <+0>: push %rbp
0x000000000000113a <+1>: mov %rsp,%rbp
0x000000000000113d <+4>: mov %edi,-0x14(%rbp) # rbp-20 = a (10)
0x0000000000001140 <+7>: mov %esi,-0x18(%rbp) # rbp-24 = b (20)
0x0000000000001143 <+10>: mov -0x14(%rbp),%edx # edx = a
0x0000000000001146 <+13>: mov -0x18(%rbp),%eax # eax = b
0x0000000000001149 <+16>: add %edx,%eax # eax = a + b
0x000000000000114b <+18>: mov %eax,-0x4(%rbp) # rbp-4 = sum
0x000000000000114e <+21>: mov -0x4(%rbp),%eax # eax = sum ← 返回值
0x0000000000001151 <+24>: pop %rbp
0x0000000000001152 <+25>: ret

看看这里做了什么:

参数传递: edi 和 esi(rdi/rsi 的 32 位版本)里存着调用方传进来的 10 和 20。函数把它们存到自己的栈上,因为后面还要用寄存器做计算。

计算: add %edx, %eax 把两个数加起来,结果存在 eax 里。返回值永远放在 eax(或 rax)寄存器里。 这就是为什么你 return 之后调用方马上能从 eax 拿到结果。

ret 指令: 从栈顶弹出之前 call 指令压进去的返回地址,跳回去继续执行。

你看——函数调用本质就是一个”把参数塞进约定好的寄存器→跳过去执行→把结果塞进 eax→跳回来”的过程。没有魔法,全是约定。


六、用 si 逐条看汇编执行#

回到 main 函数,用 stepi(简写 si)一条一条执行汇编指令,观察每一步的变化:

(gdb) si
(gdb) info registers rbp rsp rip

每执行一条 si,处理器就往前走一条指令。info registers 看关键寄存器的变化——rbp 和 rsp 怎么变、eax 什么时候被写入。

试试在 add 函数里单步走,观察 eax 的值从 0 变成 a,加上 b,变成 a+b 的过程——比你想象中直观得多。


七、总结#

概念汇编层面的真相
局部变量存在栈上(rbp 往下偏移),不是”某个神秘空间”
函数参数前 6 个放寄存器(rdi, rsi, rdx, rcx, r8, r9),多的压栈
返回值放在 rax/eax 寄存器里
函数调用call 压入返回地址 → 跳转 → 执行 → ret 弹出地址跳回
栈帧每个函数有自己的栈帧区间,由 rbp 和 rsp 划定边界

常见问题

Q:为什么我的汇编跟文章里的地址不一样? 地址偏移取决于编译器和系统版本,这不重要。看指令本身,地址只是位置标记。

Q:-O0 和 -O2 编译出来的汇编差别大吗? 非常大。-O2 会把一些变量直接优化到寄存器里不写内存,甚至内联函数消除调用开销。这就是为什么用真实程序的汇编做逆向分析比教材例子难得多——编译器帮你做了大量优化,你看到的是优化后的结果,不是原始代码逐句翻译。

Q:怎么看内存里的具体值?x 命令。x/4xb 0x7fffffffdabc 以十六进制显示指定地址的 4 个字节。配合 &变量名 可以看变量的内存布局。比如 x/4xb &x 看变量 x 的 4 个字节内容。

Q:学这个对写 C 代码有什么实际帮助? 最大的帮助是让你不再对”函数调用有开销”这句话停留在感性认识——你亲眼看到 call/ret 做了多少事,push/pop 改变了什么。以后写性能敏感代码时,你会自然而然地想”这里少调一个函数就能省一次 call 和栈帧搭建”。


从今天开始,遇到想不通的 C 代码,不要只靠猜。用 GDB 加载它,disassemble 看一眼,si 走几步——你的 C 代码在你面前就不再是魔法了。

这篇文章是 GDB 看汇编的入门第一课。下次可以聊聊怎么用 GDB 看结构体的内存布局、数组在汇编里怎么寻址——感兴趣的话可以关注更新 🐾

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
x86 汇编入门:用 GDB 看懂你的 C 程序在干什么
https://www.kshare.top/posts/x86-gdb-disassembly-intro/
作者
Kshare
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Kshare
Hello, I'm Kshare.
公告
站点已开启广告(类型:弹窗广告),给您带来的不便敬请谅解。如有更好的广告建议,欢迎发送邮件至 t9uzrz6u@anonaddy.me,感谢您的支持!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
187
分类
9
标签
190
总字数
490,175
运行时长
0
最后活动
0 天前

文章目录