如何阅读 Windows 下 C 程序的反汇编代码(使用 objdump -d)
🧠 如何阅读 Windows 下 C 程序的反汇编代码(使用 objdump -d)
—— 从 CRT 启动到你的 main() 函数
🎯 学习目标
- 掌握如何从
.exe的反汇编中定位main()函数。 - 理解
.exe文件的整体结构和 CRT 初始化流程。 - 学会快速识别关键函数、调用栈和导入表。
- 为后续学习逆向工程、内联汇编、PE 文件格式打下基础。
🔑 核心重点
一个
.exe文件不是从你的main()开始运行的,而是先执行 CRT 启动代码(CRT Startup),然后才跳转到你的main()。
✅ 整体结构概览
1. .exe 文件的主要组成部分(PE Format)
| 区段(Section) | 内容 |
|---|---|
.text |
可执行的机器码(包括 CRT 和 main) |
.data |
已初始化的数据(如字符串常量) |
.rdata |
只读数据(如 printf("Hello") 中的字符串) |
.idata |
导入表(Import Table)——列出使用的 DLL 和函数 |
.edata |
导出表(Export Table)——如果你是 DLL |
.reloc |
重定位信息(用于 ASLR) |
.rsrc |
资源(图标、版本信息等) |
📌 我们最关心的是 .text 段中的内容,特别是 main() 函数。
✅ 使用 objdump -d main.exe > hello_disassembly.txt 的输出结构
当你运行这条命令时,你会看到如下结构:
main.exe: file format pei-x86-64
Disassembly of section .text:
0000000140001000 <__mingw_invalidParameterHandler>:
140001000: c3 ret
...
0000000140001530 <main>:
140001530: 55 push %rbp
140001531: 48 89 e5 mov %rsp,%rbp
...
📌 这些地址是 RVA(Relative Virtual Address),在内存中会被加载到固定基址(通常是 0x140000000)。
✅ 如何找到 main() 函数?
方法一:直接查找关键字 main
在 hello_disassembly.txt 中搜索:
<main>:
你会看到类似:
0000000140001530 <main>:
这就是你的程序入口逻辑所在。
方法二:使用 nm 查看符号表(推荐)
nm main.exe | grep main
输出可能像这样:
0000000140001530 T main
说明 main() 函数位于地址 0x140001530。
✅ CRT 启动流程简要图示(MinGW-w64)
OS 加载 exe → CRT Startup (汇编) → pre_c_init() → pre_cpp_init() → __tmainCRTStartup() → call main()
这些函数都在 .text 段中定义,顺序如下:
| 地址 | 函数名 | 描述 |
|---|---|---|
140001000 |
__mingw_invalidParameterHandler |
异常处理回调 |
140001010 |
pre_c_init |
基本 C 库初始化 |
140001130 |
pre_cpp_init |
C++ 支持初始化 |
140001180 |
__tmainCRTStartup |
CRT 主启动函数 |
140001530 |
main |
用户写的主函数 |
📌 最终 __tmainCRTStartup 会调用 main(),并返回结果给操作系统。
✅ main 函数详解(以你的 Hello World 为例)
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
反汇编如下:
0000000140001530 <main>:
140001530: 55 push %rbp
140001531: 48 89 e5 mov %rsp,%rbp
140001534: 48 83 ec 20 sub $0x20,%rsp
140001538: 48 8d 05 a5 01 00 00 lea 0x1a5(%rip),%rax # 1400016e4 <_IO_stdin_used+0x4>
14000153f: 48 89 c1 mov %rax,%rcx
140001542: e8 d9 fc ff ff callq 140001220 <printf@plt>
140001547: b8 00 00 00 00 mov $0x0,%eax
14000154c: c9 leaveq
14000154d: c3 retq
📌 逐行解释:
| 指令 | 功能 |
|---|---|
push %rbp / mov %rsp, %rbp |
创建栈帧 |
sub $0x20, %rsp |
分配局部变量空间 |
lea ... (%rip), %rax |
获取 "Hello World" 字符串地址 |
callq printf@plt |
调用标准库函数 printf() |
mov $0x0, %eax |
设置返回值为 0 |
leaveq / retq |
返回到 CRT 启动函数 |
✅ 如何阅读调用链和函数调用?
你可以追踪调用栈,例如:
callq 140001220 <printf@plt>
查看这个地址的内容:
0000000140001220 <printf@plt>:
140001220: ff 25 8a 7d 00 00 jmpq *0x7d8a(%rip) # 140009fb0 <__imp_printf>
这表示它是一个 PLT(Procedure Linkage Table)跳转项,最终指向 msvcrt.dll 中的 printf() 函数。
📌 所以你在 .exe 文件里看不到 printf() 的实现,它是在运行时动态链接进来的。
✅ 实战建议:阅读反汇编的技巧
| 技巧 | 描述 |
|---|---|
只关注 .text 段 |
使用 objdump -j .text -d main.exe 缩小范围 |
使用 grep 或编辑器搜索 <main> |
快速定位用户逻辑 |
结合 nm 查看符号表 |
定位函数地址 |
| 使用调试器辅助分析(如 x64dbg) | 单步执行、观察寄存器变化 |
尝试静态链接 -static |
看清完整的 CRT 初始化过程 |
注意 RIP 相对寻址(如 lea) |
理解字符串和全局变量的访问方式 |
✅ 示例:完整流程总结(从 CRT 到 main)
Entry Point → _start (CRT startup)
→ pre_c_init()
→ pre_cpp_init()
→ __tmainCRTStartup()
→ call main()
→ return 0
→ exit(0)
📌 你的 main() 是整个流程中最短的一环,但却是你真正控制的部分。
📚 推荐练习
练习 1:找出你的 main 函数地址并反汇编该区域
objdump -d main.exe --start-address=0x140001530 --stop-address=0x140001550
练习 2:观察 printf() 的 PLT 表跳转
跟踪 callq printf@plt 到底调用了哪个 DLL 函数。
练习 3:尝试静态链接并比较反汇编差异
gcc -static hello.c -o hello_static.exe
objdump -d hello_static.exe > static_disassembly.txt
你会发现 CRT 代码大幅增加,甚至包含完整的 printf() 实现。
🧭 下一步建议
完成本章后,建议:
- 继续学习《函数调用栈与堆栈平衡》,理解函数调用机制。
- 研究《C 与汇编交互》,掌握内联汇编技巧。
- 下一章将进入《Windows API 编程基础》或《ELF/PE 文件格式详解》。
是否需要我为你生成配套的反汇编脚本模板、nm 符号解析工具,或是进入下一章《函数调用栈与堆栈平衡》?还是想让我出一份动手实践的反汇编作业?

浙公网安备 33010602011771号