ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从hello.c到进程回收:C程序编译链接装载全链路拆解

从hello.c到进程回收:C程序编译链接装载全链路拆解

简介:面向计算机系统课程学习者与备考 CSAPP(深入理解计算机系统)大作业的学生,这份哈工大课程报告以经典 hello 程序为主线,串联程序从源码到运行的完整生命周期,可帮助你理清课程大作业的写作框架与知识脉络,适合正在完成同类报告或复习底层机制的人群参考。压缩包共 1 个 doc 文件,约 6.44MB,正文按章节组织,涵盖计算机系统概述、预处理、编译、汇编、链接,以及进程管理、信号与异常、内存管理、I/O 管理等模块,并配有 Ubuntu 下的命令示例与中间结果解析,如 hello.i 展开行数、头文件与宏替换过程等,便于对照实验环境复现验证。该资源已有 358 人学习,可作为课程报告的参考范文与知识点索引,帮助读者理解各阶段工具链的作用及相互衔接,提升对程序加载、执行与系统资源管理的整体认识。

1. 从一行 C 语句到进程被回收:这份 Hello 生命周期报告能补上哪块底层拼图

很多人学完 CSAPP 的链接和异常控制流,概念能背,真要把hello.c从磁盘上的 ASCII 文本一路追到进程被 shell 回收,脑子里还是糊的。这份某高校计算机学院的《程序人生》大作业干的就是这件事:它以 hello 的 P2P(From Program to Process)和 020(From Zero-0 to Zero-0)两条主线,把预处理、编译、汇编、链接、进程、存储、IO 七个阶段串成一条能动手复现的链路,每一环都配了命令、中间文件和逐行解读。刚学完 CSAPP、想找一份完整观察记录照着跑一遍的人适合看;带课的助教也能拿它当验收清单,对着目录一条条问学生「这步产物在哪、里面长什么样」。

2. 环境基线与中间产物链:五份文件把整条复现路径固定下来

2.1 虚拟机与工具链的最小组合

这份报告的实验环境门槛很低,一台四核 x64 的笔记本、8G 内存、256G 磁盘就够,Windows 上装个虚拟机跑 64 位 Linux 发行版,编译工具链用系统自带的 gcc 和 binutils,编辑器 vim、gedit、VS Code 都行。真正需要留意的不是硬件,而是工具链版本:hello.i展开成多少行、hello.o里重定位条目叫什么名字、main里某个变量落在%rbp偏移多少,全都跟 gcc 和 binutils 的版本强相关。原文那份记录里hello.i是 3065 行、自己的代码从 3046 行开始,你换一台机器跑出来大概率是两千多行或者四千多行,这不是你错了,是头文件版本不同。

我在动手前会先把版本信息落一份档,后面写报告、对差异都用得上:

# 记录工具链版本,后面所有"行数对不上""重定位类型不一样"都要回来查这里 gcc --version | head -1 ld --version | head -1 uname -a # 确认调试与观察工具齐全 which gcc readelf objdump nm ldd strace gdb

readelf和objdump都来自 binutils,跟 gcc 是两套东西,前者读 ELF 结构,后者反汇编,两个都要有。strace用来观察系统调用,gdb用来在execve前后打点,缺一个后面就会卡在「看得见结果但说不清过程」。

2.2 中间产物清单与每份文件对应的验证点

这份报告最有价值的地方,是把「每一步产出什么文件、这个文件用来验证什么」列成了一张表。整理成下面这样更好用,因为写报告时你会反复回来查「我这一步该拿什么去证明」:

产物生成阶段格式主要验证点
hello.i预处理ASCII 文本头文件展开、宏替换、条件编译裁剪、注释消失
hello.s编译ASCII 汇编数据段布局、寄存器传参、控制转移、PLT 调用
hello.o汇编可重定位 ELF节区结构、符号表、.rela.text重定位条目
hello.asm反汇编文本未重定位的地址全是 0 占位
hello.elfELF 转储文本.symtab、.rela.text、.debug_*全量信息
hello链接可执行 ELF入口地址、LOAD段、动态链接依赖
hello_elf/hello_asm链接后观察文本重定位后的真实地址、PLT桩代码

表里hello.asm和hello_asm是最容易被忽略的一对:前者是未链接的反汇编,call后面跟的偏移是00 00 00 00;后者是链接后的反汇编,同样的call后面已经是真实目标。这两份文件放在一起对比,重定位这件事就不用再靠想象了。

原文里的 hello.c 没有完整贴出来,但根据汇编章节的描述可以还原出大致结构:带argc != 3的判断、一段getchar()循环、一次sleep、两次格式化输出。照着写一份功能等价的就能跑通全流程:

#include <stdio.h> #include <unistd.h> #include <stdlib.h> int sleepsecs = 2.5; /* 隐式强转,实际存 2,编译会有 warning,是故意的 */ int main(int argc, char *argv[]) { int i; if (argc != 3) { printf("Usage: Hello student_id name!\n"); exit(1); } for (i = 0; i < 10; i++) /* 循环体里只调 getchar,用来观察控制转移 */ getchar(); sleep(sleepsecs); printf("Hello %s %s\n", argv[1], argv[2]); return 0; }

int sleepsecs = 2.5;这行是报告里刻意保留的,2.5被截断成2,正好用来讲浮点到整型的隐式转换在汇编里是什么样——你在.data段里看到的是.long 2,浮点那部分根本没进目标文件。自己写的时候如果开了-Wall会报一条 warning,别急着改,这条 warning 本身就是观察素材。

2.3 一次性跑通全流程的命令序列

把这五步连起来敲一遍,整条链路就活了。命令本身很简单,关键是每次只做一件事,产物分开命名,别用gcc hello.c一把梭,那样中间过程全被临时文件吃掉了:

mkdir -p ~/hello-p2p && cd ~/hello-p2p # 把上面的 hello.c 放进来 # 1) 预处理:只做文本级替换,不做任何语法检查 gcc -E hello.c -o hello.i # 2) 编译:C 源码 -> 汇编文本 gcc -S hello.i -o hello.s # 3) 汇编:汇编文本 -> 可重定位目标文件(二进制) gcc -c hello.s -o hello.o # 4) 链接:可重定位目标文件 -> 可执行文件 gcc hello.o -o hello # 5) 观察:ELF 结构与反汇编,全部落成文本方便 diff readelf -a hello.o > hello.elf objdump -d hello.o > hello.asm readelf -a hello > hello_elf objdump -d hello > hello_asm

参数含义值得逐个说清:-E表示「只预处理」,-S表示「只编译到汇编」,-c表示「编译汇编但不链接」,三个开关刚好卡在三个阶段的分界线上;-o指定输出文件名,不写的话 gcc 会按阶段给默认名(a.out或者hello.o),容易覆盖。objdump -d只反汇编有代码的节,-s是把所有节的内容按十六进制打出来,看.rodata里的字符串用-s更方便。第 5 步把结果全导成文本是血泪经验:调汇编的时候你会反复在hello.o和hello之间跳,落成文件之后直接diff hello.asm hello_asm就能看出链接改了什么。

3. 预处理到汇编:3065 行的 hello.i 怎么变成 hello.o

3.1 预处理:头文件搬运、宏替换与条件编译

预处理的核心就三件事:把#include的头文件原样复制进来、把#define的宏换成实际值、按#if的条件裁剪代码,顺便把注释全部吃掉。报告里那组数字很直观——原来几十行的hello.c,预处理之后膨胀到 3065 行,自己的代码被挤到 3046 行之后,前面三千行全是stdio.h、unistd.h、stdlib.h三个头文件带进来的类型声明、函数原型和一堆条件编译分支。

# 两种写法等价,cpp 是独立的预处理器,gcc -E 是驱动它 cpp hello.c > hello.i gcc -E hello.c -o hello.i # 看一下膨胀了多少,以及自己的代码从哪一行开始 wc -l hello.i grep -n 'int main' hello.i sed -n '3040,3065p' hello.i

wc -l给出总行数,grep -n 'int main'直接定位自己代码的起点行号,sed截取尾部几十行看展开后的样子。这里有个细节:hello.i里会出现大量以#开头的行编号标记和文件名标记,这是预处理器给编译器留的位置信息,用来在报错时把行号映射回原始.c文件。这些标记你在读hello.i的时候可以直接跳过,但它们解释了「为什么hello.i里明明没写几行,编译器报错却能精确到hello.c第 12 行」。

提示:gcc -E默认会保留#行标记,加-P参数可以去掉。写报告截图的时候加-P更干净,但分析行号映射的时候别加。

宏替换的观察点更好找。hello.c里只要写了#define N 10,hello.i里所有N都会变成10,包括循环条件里的那个。报告里提到的「根据#if决定需要编译的代码」,在hello.i里表现为整段整段的代码直接消失——不是被注释掉,是根本没出现在文件里。这就是为什么预处理阶段不报语法错误:它压根不懂语法,只认#开头的行。

3.2 编译:hello.s 里的汇编指示、数据段与寄存器传参

从hello.i到hello.s是整条链路里信息密度最高的一步。词法分析、语法分析、语义检查、中间代码生成、优化、目标代码生成,六个阶段压在一个gcc -S里,输出的是人还能读懂的 x86-64 汇编文本。报告花了最长的一章拆这一步,抓的点很准:先认汇编指示,再看数据类型怎么落段,最后看操作和控制转移怎么实现。

gcc -S hello.i -o hello.s

汇编指示(也叫伪操作)都以.开头,不会被翻译成机器指令,只是给汇编器下指令。报告里列了一张表,最常用的几个是:

指示作用
.file声明源文件名,给调试信息用
.text以下内容进代码段
.data以下内容进已初始化数据段
.section .rodata切换到只读数据段,放字符串和常量
.globl声明符号为全局,链接时别的模块可见
.align地址对齐,x86-64上常见 4 或 8
.long/.string按 4 字节整数 / 字符串写入数据
.size/.type声明符号大小、是函数还是对象

这些指示直接决定了 ELF 里节的划分。hello.s里能观察到的变量分布大致是这样:

.file "hello.c" .section .rodata .LC0: .string "Usage: Hello student_id name!\n" .LC1: .string "Hello %s %s\n" .globl sleepsecs .data .align 4 .type sleepsecs, @object .size sleepsecs, 4 sleepsecs: .long 2 # 2.5 被截断成 2,浮点部分没进目标文件 .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $32, %rsp # 开栈帧,32 字节装 argc/argv/i movl %edi, -20(%rbp) # 第 1 个参数 argc 从 edi 落到栈 movq %rsi, -32(%rbp) # 第 2 个参数 argv 从 rsi 落到栈 cmpl $3, -20(%rbp) # argc 与 3 比较 je .L2 # 相等则跳过 if 块

字符串统一进.rodata、已初始化的全局变量进.data、函数体进.text,这是 C 语言内存模型在汇编层面的第一次落地。要注意原文正文里写「sleepsecs存在rodata段」,但从hello.s实际的.globl、.data、.long三段来看,它落在.data段——.rodata只装了两个字符串。这类前后不一致在整理笔记时最好保留判断痕迹,别照着抄,因为评审或者自己半年后回看,认的是汇编,不是正文措辞。

寄存器传参是另一个必须吃透的点。x86-64 的调用约定里,前六个整型参数依次走%rdi、%rsi、%rdx、%rcx、%r8、%r9,所以main拿到的是%edi里的argc和%rsi里的argv。函数一进来就把这两个寄存器写进栈帧,是因为后面要调用printf、sleep,这些寄存器会被被调用者覆盖,只能先存起来。局部变量i落在(%rbp-4),argv的首地址落在(%rbp-32),中间那 28 个字节是编译器给对齐和临时值留的余量。这些偏移量就是你后面用gdb打断点看内存时的坐标。

控制转移的实现方式也值得单独看一眼。C 里的if和for到汇编层被压成「比较 + 条件跳转」两件事:cmpl $3, -20(%rbp)做减法并设置条件码,je .L2检查 ZF 标志位,相等就跳走。for (i = 0; i < 10; i++)更典型,编译器会把i < 10优化成i <= 9,于是cmpl $9, -4(%rbp)配jle .L4,少做一次比较。这种「源码写的和汇编做的不是一回事」的地方,恰恰是读汇编最有收获的地方。

函数调用部分,main自己是被__libc_start_main调起来的,call指令先把下一条指令的地址压栈,再跳到目标;函数结束时leave一条指令等价于mov %rbp,%rsp加pop %rbp,把栈帧还回去,ret再从栈顶弹出返回地址写进%rip。这套push/sub/leave/ret的组合,在hello.s里每个函数都能看到一模一样的骨架。

外部函数的调用形式是@PLT后缀,puts@PLT、printf@PLT、sleep@PLT、getchar@PLT、exit@PLT。PLT 是过程链接表,动态链接的可执行文件里,对共享库函数的调用先跳到 PLT 桩,再由桩跳到 GOT 里存的真实地址。这里还有个容易被忽略的优化:第一次printf只输出一个不带格式符的固定字符串,gcc 会直接把它换成puts@PLT,因为puts更快。所以你在hello.s里找不到第一次printf,别以为自己漏抄了。

3.3 汇编:readelf 拆开 hello.o 的节区与重定位条目

汇编器把hello.s翻译成机器指令,打包成可重定位目标文件hello.o。「可重定位」这三个字是关键:这时候代码里的地址全是占位的,call printf后面跟的是00 00 00 00,因为汇编器不知道printf最终会被链接到哪个地址,只能先留空,等链接器来填。

gcc -c hello.s -o hello.o # 全量 ELF 信息,重点看节头、符号表、重定位表 readelf -a hello.o > hello.elf # 只看重定位条目,最常用 readelf -r hello.o # 反汇编,对比 call 后面的占位 objdump -d hello.o > hello.asm

hello.o里你会看到这些节:.text放机器指令,.data放已初始化的全局变量,.bss放未初始化的全局变量(在文件里不占空间,只记大小),.rodata放只读常量,.symtab是符号表,.rela.text是代码段的重定位表,.debug_*一堆调试节。readelf -a会把它们全打出来,第一次看会很晕,建议按「节头 → 符号表 → 重定位表」的顺序读。

.rela.text里的每一条重定位记录有四个字段,含义分别是:

  • Offset:这条重定位要改的位置,相对于所在节的偏移;
  • Type:重定位类型,R_X86_64_PC32表示相对寻址,R_X86_64_PLT32表示要跳 PLT;
  • Sym. Name:要引用的符号,比如printf、.rodata;
  • Addend:加数,跟符号地址相加后写回目标位置。

对照objdump -d hello.o看,call指令后面那 4 个字节正好对应Offset指向的位置。这时候的hello.o是一块「半成品」,能看结构不能运行,./hello.o会直接报格式错误——因为它没有入口地址、没有对齐到可加载的段、也没有动态链接器信息。

注意:新版 binutils 里printf的重定位类型可能显示成R_X86_64_PLT32,旧版显示R_X86_64_PC32。写报告时两个都提一句,比只写一个更稳妥,因为不同发行版差异很大。

4. 链接、装载与运行:从 hello 的虚拟地址空间到 fork/execve

4.1 链接:重定位表如何把符号填成真实地址

链接器干两件事:符号解析和重定位。符号解析是把每个.o里的未定义符号跟别的模块或库里的定义对上号,printf在hello.o里是UND(未定义),在标准 C 库里是T(代码段中的全局符号);重定位是拿着.rela.text里的记录,把call后面那 4 个占位字节改成算好的偏移。

# 用 gcc 驱动链接,它会自动带上 crt 启动文件、libc、动态链接器 gcc hello.o -o hello # 链接后的反汇编,对比 hello.asm objdump -d hello > hello_asm diff <(grep -A2 'call' hello.asm) <(grep -A2 'call' hello_asm)

这里有个几乎所有人都踩过的坑:报告摘要里写的是ld hello.o -o hello,这条命令直接敲会失败,报一堆undefined reference to _start、undefined reference to main之类的错。原因是ld只是个裸链接器,不知道启动文件在哪、不知道 C 库在哪、不知道动态链接器路径。gcc是驱动器,它在背后帮你补上crt1.o、crti.o、crtbegin.o、-lc、-lgcc、crtend.o、crtn.o这一长串参数。真要看gcc到底做了什么,加-v参数,它会把完整的ld调用打出来,那才是链接的真实样子。

链接完成后,hello.o里那些00 00 00 00全部变成了真实偏移:call后面不再是零,jmp也不再是零,.rela.text这一节直接消失(重定位做完就没用了)。objdump -d hello里能看到main的起始地址已经变成0x4011a5这类具体的值,而不是hello.o里的0x0。

4.2 装载:虚拟地址空间布局与动态链接

hello这个可执行文件在磁盘上是一堆节,被操作系统加载到内存时按「段」组织,段是节的集合,按访问权限合并。用readelf -l看段表,能看到若干个LOAD段,每个段记录虚拟地址VirtAddr、文件大小FileSiz、内存大小MemSiz、对齐Align。典型布局大致是:

区域起始位置内容权限
代码段0x400000附近.init.plt.text.rodata只读、可执行
数据段紧随代码段.data.bss.got.dynamic可读可写
堆数据段之后malloc分配区,向高地址增长可读可写
共享库映射区0x7f...附近libc.so等共享对象按段设置
栈高地址,向低地址增长局部变量、返回地址、环境变量可读可写
内核区最顶端用户态不可访问—

.bss段特殊:它在文件里不占空间(FileSiz为 0,MemSiz不为 0),加载时由内核映射一块「请求二进制零」的匿名页,读到的全是 0。这就是为什么未初始化的全局变量默认是 0 而不用在文件里写一堆零字节。

readelf -l hello # 段表:LOAD 段的地址、大小、权限 readelf -S hello # 节表:.text .data .bss .got .plt 各在哪 ldd hello # 依赖哪些共享库,动态链接器路径是什么 readelf -d hello # .dynamic 节:NEEDED、INIT、FINI 条目

ldd hello会列出linux-vdso.so.1、libc.so.6、ld-linux-x86-64.so.2这几项。动态链接的核心机制是延迟绑定:第一次调用printf@PLT时,PLT 桩跳到 GOT,GOT 里存的还是「解析器入口」而不是printf的真实地址,解析器找到printf后把真实地址写回 GOT,之后再调用就直接跳了。所以要观察动态链接的过程,得在第一次调用之前停下来,用gdb在main入口看 GOT 表内容,跑一步再看一次,能明显看到地址从解析器变成libc里的实际偏移。

4.3 运行:fork、execve、地址变换与 printf 落盘

在 shell 里敲./hello arg1 arg2,背后发生的事比想象中长。shell 本身是个进程,读到你敲的命令后调用fork()创建子进程,子进程调用execve("hello", argv, envp)把自己替换成 hello。execve做四件事:删除当前进程虚拟地址的用户部分里已有的区域结构、为新程序的代码/数据/bss/栈创建私有且写时复制的区域、把共享库映射到共享区域、把程序计数器设成代码段入口。做完这四步,bash的壳子还在,但里面跑的已经是 hello 了。

# 观察 fork/execve 与后续 IO strace -f -e trace=process,read,write ./hello arg1 arg2 # 观察地址空间变化,在 execve 前后各打一次 gdb -q ./hello (gdb) start (gdb) info proc mappings

strace的输出里能清楚看到clone(fork在 Linux 上的实际实现)、execve、read、write的调用顺序。getchar()循环十次,就是十次read(0, ...),每次读一个字节;sleep(2)对应的是一次nanosleep;printf("Hello %s %s\n", ...)对应的是一次write(1, "Hello ...", N),注意是write不是printf,因为printf是库函数,它把格式化结果攒在缓冲区里,真正落到终端靠的是write系统调用。

地址变换这条线也在这一步发生。程序里用的全是虚拟地址,CPU 里的 MMU 靠 CR3 寄存器指向的页表做四级遍历:PGD → PUD → PMD → PTE,每一级取 9 位索引,最后拼出物理页框号,加上页内偏移得到物理地址。为了不每次都走四级,TLB 缓存了最近的页表项;为了不每次都访问内存,三级 Cache(L1/L2/L3)缓存了最近的物理内存块。访问一个没映射过的页会触发缺页故障,内核的缺页处理程序负责分配物理页、建立映射、再重新执行那条指令。

进程结束的时候,main返回 0,exit被调用,进程进入僵尸态,shell 通过wait回收它,内核把它的页表、文件描述符、信号处理表全部清掉。这就是 020 里的那个「to Zero-0」——从零开始,回到零。

5. 避坑与排查:复现 Hello 全链路时最容易翻车的五处

5.1 编译与链接阶段的三处硬伤

**现象:**照着报告敲ld hello.o -o hello,报一堆undefined reference to _start,或者链接出来的文件一跑就段错误。原因:ld是裸链接器,不带启动文件、不带 C 库、不指定动态链接器,_start是crt1.o提供的,main的调用链也靠启动文件搭起来。 **解决:**统一用gcc hello.o -o hello驱动;确实想手写ld命令,就加-v把gcc的完整调用抄出来,照着改路径。

现象:hello.i的行数跟报告里的 3065 行差了几百上千行,怀疑自己预处理漏了东西。 **原因:**行数主要由头文件决定,不同发行版、不同 gcc 版本、不同glibc版本展开出来的行数完全不同,stdio.h里包含的头文件数量都能差出十几个。 **解决:**不要拿行数当正确性标准,改成校验「自己的代码从第几行开始」「三个头文件的路径出现在哪」「宏有没有被替换」这三条;行数只作为参考值记录在报告里,并注明工具链版本。

**现象:**在hello.s里找不到第一次printf的调用,只看到puts@PLT,以为编译器把代码优化坏了。原因:printf("Usage: ...\n")不带任何格式符,gcc 会把它替换成语义等价但更快的puts,这是标准允许的优化,不是 bug。 **解决:**想强制保留printf,编译时加-fno-builtin-printf,或者把字符串改成带%s的形式再传一个参数;写报告时把这条优化单独拎出来讲,比忽略过去更有说服力。

5.2 观察与记录阶段的两处误判

现象:readelf -a hello.o输出里翻不到.rela.text,或者重定位类型显示成R_X86_64_PLT32而报告里写的是R_X86_64_PC32,对不上。 **原因:**重定位节的命名和类型跟 binutils 版本、目标架构、编译器选项都有关;新版工具链对函数调用普遍用PLT32,旧版用PC32。 **解决:**先readelf -S hello.o确认重定位节叫什么名字,再用readelf -r单独看;报告里两种类型都写,并注明版本,别只写一种然后说别人错。

**现象:**报告正文说sleepsecs在.rodata段,但hello.s里明明写着.data,用objdump -s看.rodata里也只有两个字符串。 **原因:**已初始化为非零值的全局变量按 C 语义进.data,只有const或者字符串字面量才进.rodata;正文那句是笔误。 **解决:**以汇编和readelf -S为准,正文里保留一句「这里与初稿描述不一致,按实际产物修正」,这种自我纠错反而是加分项。

现象:strace抓到一堆read(0, ...),但printf的输出在终端上是一次性出来的,怀疑write被吞了。 **原因:**标准输出接终端时是行缓冲,接管道或文件时是全缓冲,printf的内容先进用户态缓冲区,缓冲区满或者遇到换行(行缓冲模式)才真正write。 **解决:**用strace -e trace=write单独看,或者在printf后面加fflush(stdout);写报告时把「库函数缓冲」和「系统调用」这两层分开讲,别混成一件事。

6. 二次开发:把这份报告改造成自己的实验脚手架

这份材料的骨架很好,但它是静态文档,而底层观察这件事最怕「一次性」。我的做法是把它变成一个可以反复跑的小脚手架,每次改一点代码,跑一遍脚本,diff 一次产物,看汇编和 ELF 哪里变了。

第一层是自动化产物。写一个Makefile,把五个阶段拆成独立目标,make all一次产出全部中间文件,make clean全部清掉:

CC := gcc TARGETS := hello.i hello.s hello.o hello hello.elf hello_elf hello.asm hello_asm all: $(TARGETS) hello.i: hello.c $(CC) -E hello.c -o $@ hello.s: hello.i $(CC) -S $< -o $@ hello.o: hello.s $(CC) -c $< -o $@ hello: hello.o $(CC) $< -o $@ # 观察目标:只依赖不产出,永远执行 hello.elf: hello.o readelf -a $< > $@ hello_elf: hello readelf -a $< > $@ hello.asm: hello.o objdump -d $< > $@ hello_asm: hello objdump -d $< > $@ clean: rm -f $(TARGETS)

第二层是对照实验。源码里每次只改一个变量,其余不动,然后diff上一次的hello.s。我常做的三组对比:

改造点改动方式观察目标
优化等级-O0换成-O2main的栈帧消失、循环被展开、i不再落栈
静态 vs 动态-static与默认对比hello.asm里@PLT全变成直接call,ldd报「not a dynamic executable」
多目标文件把printf拆到util.chello.o的.rela.text多出一条跨模块重定位

第三层是验证方法。改完之后别只看「能跑」,要拿三个指标卡:readelf -r hello.o的重定位条目数量变化、objdump -d里main的指令条数变化、strace的系统调用序列变化。三个都对得上,才算真正理解了这次改动在机器层面意味着什么。我一般还会在gdb里start之后打一次info registers rbp rsp,把栈帧边界记下来,跟hello.s里的subq $32, %rsp对一遍——对不上,说明我对这段汇编的理解有偏差。

这套脚手架跑顺之后,再去看别的程序的 ELF,思路是一样的:先readelf -h看入口,再readelf -l看段,再objdump -d看代码,最后strace看运行。从那以后我每次拆一个二进制,都会先把这四步强制走一遍再下结论,因为跳过去的那一步,往往就是后来返工的那一步。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表