从 `hello.c` 到 ELF:亲手走一遍 GCC 编译、汇编与链接全过程

发布时间:2026/11/5 14:03:45

从 `hello.c` 到 ELF:亲手走一遍 GCC 编译、汇编与链接全过程 平时编译 C 程序我们往往只写一条命令gcc hello.c-ohello几秒之后可执行文件就生成了。但这条命令背后到底发生了什么头文件去了哪里.o为什么不能直接运行printf的实现什么时候进入程序动态链接和静态链接又差在哪为了把这些问题串起来我用一个简单的hello.c做了完整实验分别保留预处理文件、汇编文件和目标文件再借助objdump、readelf、ldd观察符号、重定位和 ELF 布局。这套过程不只适用于本机 GCC。以后换成 ARM 交叉编译器编译链路仍然相同只是目标架构和使用的库发生了变化。一条命令背后的四个阶段从 C 源文件到可执行程序通常会经过四个阶段hello.c --预处理-- hello.i --编译-- hello.s --汇编-- hello.o --链接-- hello阶段GCC 参数输出主要工作预处理-Ehello.i展开头文件和宏处理条件编译编译-Shello.s语法、语义分析生成汇编代码汇编-chello.o生成机器码和可重定位 ELF链接无停止参数hello解析符号、重定位并生成可执行文件这里有个很容易写错的地方汇编后停止使用的是小写-c。大写-C是预处理选项通常和-E搭配用来保留注释。准备一段测试代码为了方便观察库函数符号测试程序保留了printf、atoi和exit等调用#includestdio.h#includestdlib.hintmain(intargc,char*argv[]){if(argc!2){printf(Usage: %s number\n,argv[0]);exit(EXIT_FAILURE);}printf(program: %s, input: %s\n,argv[0],argv[1]);intnumberatoi(argv[1]);switch(number){case0:printf(h\n);break;case1:printf(e\n);break;case2:printf(l\n);break;case3:printf(l\n);break;case4:printf(o\n);break;default:printf(hello\n);break;}returnEXIT_SUCCESS;}atoi()无法可靠判断转换失败这里只是为了观察编译和链接过程。正式项目更适合使用strtol()并检查非法字符、溢出和数值范围。把四个阶段分别执行gcc-Ehello.c-ohello.i gcc-S-g-O0hello.i-ohello.s gcc-chello.s-ohello.o gcc hello.o-ohello-g用来保留调试信息方便后面混合显示源代码和汇编-O0关闭优化让生成结果更接近源代码结构。第一站预处理文件为什么这么大gcc-Ehello.c-ohello.i预处理器主要完成下面几件事展开#include引入的头文件替换宏定义处理#if、#ifdef等条件编译默认删除注释插入行标记保留内容对应的源文件和行号例如#12hello.c它告诉后续编译阶段接下来的内容与hello.c第 12 行相关。实际行标记还可能包含其他标志用来表示进入或离开头文件等状态。比较文件大小和行数stathello.c hello.iwc-lhello.c hello.i实验中几十行的源文件经过预处理后增长到近两千行。真正让文件膨胀的不是业务代码而是stdio.h、stdlib.h以及它们递归包含的类型、声明和宏。这也是hello.i最实用的用途当宏展开或条件编译不符合预期时直接查看预处理结果比盯着源文件猜更有效。第二站C 代码如何变成汇编gcc-S-g-O0hello.i-ohello.s编译阶段会完成词法分析、语法分析、语义检查、中间表示生成以及目标代码生成。开启优化后编译器还会对中间表示进行优化。这一阶段输出的hello.s已经是面向目标 CPU 的汇编代码但还不是机器码。外部函数也不会简单地在这个阶段“复制进来”。对于printf、atoi这类普通外部调用编译器主要生成调用指令和符号引用真正的实现由链接阶段处理。想观察优化的影响可以生成两份汇编文件gcc-S-O0hello.c-ohello_O0.s gcc-S-O2hello.c-ohello_O2.sdiff-uhello_O0.s hello_O2.s有些系统上编译器甚至会把简单的printf(h\n)优化成puts(h)。因此符号表里出现puts并不一定意味着源代码直接调用了它。第三站hello.o为什么还不能运行gcc-chello.s-ohello.o汇编器会把汇编助记符编码成 CPU 机器指令同时生成符号表和重定位信息。这时的hello.o已经包含机器码但它属于“可重定位目标文件”还不能直接执行。原因主要有两个printf、atoi等外部符号的最终位置还没有确定文件还没有形成操作系统可直接装载的完整程序映像hello.o也不是一段没有结构的二进制数据而是一个 ELF 文件通常包含ELF 文件头.text代码节.rodata只读数据节.data和.bss数据节符号表重定位节使用-g时生成的调试信息这里需要区分两个概念section通常译为“节”主要服务于编译和链接segment通常译为“段”主要描述程序如何装载到内存目标文件主要关注节可执行文件被内核加载时程序头表中的段更关键。用objdump拆开目标文件先看反汇编objdump-dhello.o同时显示源代码和汇编objdump-Shello.oobjdump -S需要目标文件包含调试信息否则通常只能看到反汇编无法正确对应源代码。继续查看符号表和重定位objdump-thello.o objdump-rhello.o如果想看更完整的 ELF 元数据可以使用readelf-hhello.o readelf-Shello.o readelf-sWhello.o readelf-rWhello.o第四站链接器如何把零散内容拼成程序gcc hello.o-ohello链接阶段主要完成四件事符号解析把未定义符号和目标文件或库中的定义对应起来。合并输入文件中的同类节并确定整体布局。根据最终布局修正代码和数据里的地址引用。生成 ELF 头和程序头表让内核与动态加载器知道如何装载程序。使用gcc链接时输入不只有hello.o和 C 标准库。GCC 还会自动加入启动文件、运行时支持库和默认链接选项。想看它实际调用了哪些工具和文件可以执行gcc-vhello.o-ohello输出里常见的crt1.o、crti.o、crtn.o属于 C 运行时启动文件。它们负责在进入main前后完成必要的初始化和收尾工作。动态链接和静态链接体积差多少普通命令默认通常生成动态链接程序gcc hello.o-ohello ldd helloldd可以看到libc.so.6和动态加载器等依赖。括号中的十六进制数是本次检查时观察到的映射地址启用 ASLR 后可能在不同运行间变化。再生成一个静态链接版本gcc hello.o-static-ohello_staticls-lhhello hello_static ldd hello_static实验中的动态版本只有几 KB静态版本接近 1 MB体积差距非常明显。原因在于动态链接程序主要记录共享库依赖运行时再由加载器映射库静态链接会把需要的库代码复制进可执行文件不过静态链接不等于“复制到任何同架构 Linux 都能运行”。程序仍然受到 CPU 指令集、内核 ABI、系统调用、设备文件和运行配置等条件约束。符号表里那些0到底代表什么objdump-thello.o objdump-thello在hello.o中可以看到两类典型符号printf、exit、atoi等显示为*UND*说明它们是未定义外部符号值通常为 0main已经定义在.text中它的值表示相对于所在节起点的偏移因此不能简单地说“.o里的符号地址全是 0”。更准确的理解是可重定位文件中的已定义符号通常保存节内偏移未定义外部符号等待链接器解析链接完成后已定义符号才会获得最终链接布局中的虚拟地址使用下面的命令可以更清楚地查看符号类型、绑定方式和节索引readelf-sWhello.oobjdump -r没输出就没有重定位了吗objdump-rhello.o objdump-rhellohello.o中能看到针对外部函数和只读数据的重定位条目。链接完成后objdump -r hello可能没有输出这表示普通静态重定位已经被链接器处理不能据此断言可执行文件完全没有重定位。动态链接程序通常仍然包含 GOT、PLT 和共享库相关的动态重定位。应该继续检查readelf-rWhello objdump-Rhello现代 Linux 发行版还可能默认生成 PIE也就是位置无关可执行文件。它在装载时会根据实际基址完成相应的动态重定位filehello readelf-hhello|grepType不同 GCC 和发行版的默认设置可能不同因此实验结果不必和其他机器逐字一致重点是看清 ELF 类型和工具输出。readelf常用命令速查观察目标命令ELF 类型、架构和入口地址readelf -h hello节头表观察链接视图readelf -S hello程序头表观察装载视图readelf -l hello符号表readelf -sW hello重定位表readelf -rW hello动态段和共享库依赖readelf -d hello反汇编代码objdump -d hello.o动态重定位objdump -R hello动态库依赖ldd helloreadelf -h会输出整个 ELF 文件头其中包含入口地址而不是只显示入口地址。这次实验真正看懂了什么走完这套流程后gcc hello.c -o hello就不再是一条黑盒命令了hello.i告诉我们头文件和宏在预处理阶段被展开hello.s展示编译器为目标 CPU 生成的汇编代码hello.o已经包含机器码但仍保留未解析符号和重定位信息链接器负责解析符号、安排地址并生成可装载的 ELF动态链接和静态链接的差别不只是文件大小还涉及部署和运行环境objdump更适合观察指令readelf更适合查看 ELF 元数据这也是学习交叉编译前很值得做的一次实验。把gcc换成 ARM 交叉编译器后四个阶段没有变但生成的机器码、ELF 架构、动态加载器和链接库都会换成目标板对应的版本。以后再遇到undefined reference、Exec format error、动态库找不到或者程序明明存在却无法运行就可以先判断问题发生在编译、链接还是装载阶段而不是只盯着最后一条报错。
延伸阅读

更多相关文章

2026/10/24 15:09:53

深度解析Grammarly Premium Cookie自动化采集架构与三级验证机制

深度解析Grammarly Premium Cookie自动化采集架构与三级验证机制 【免费下载链接】autosearch-grammarly-premium-cookie 免费白嫖使用Grammarly Premium高级版 项目地址: https://gitcode.com/gh_mirrors/au/autosearch-grammarly-premium-cookie Grammarly Premium高级…

2026/11/1 19:12:42

从嘉立创 EDA 导出的芯片原理图和封装,再导入 AD 的时候其 Top Paste 层芯片引脚没有,开钢网的时候没有芯片引脚的位置,这种情况该怎么办,如何解决

问题根源先说清楚嘉立创 EDA(LCEDA)导出 .PcbDoc 到 AD,两个软件 Mask(助焊 / 阻焊)底层机制不一样: 立创 EDA 贴片焊盘自动根据焊盘尺寸实时生成 Paste 开窗; 导出 AD 时不会把 “自动生成开窗…

2026/11/5 13:55:19

GD32E10x官方例程入门:从GPIO点灯到串口调试

简介:这份GD32E10x系列官方例程面向嵌入式硬件与单片机开发者,基于ARM Cortex-M3内核,汇集兆易创新GD32E10x的示例代码,适合工业控制、消费电子、通信设备等场景的学习、验证与二次开发。压缩包共935个文件,约11.16MB&…

2026/11/5 13:50:18

基于Django的运维平台实战:资产、发布、巡检、告警一体化

简介:这份资源是一套基于Django框架开发的运维平台源码,面向具备Python基础、希望了解Web运维系统实现思路的开发者与运维人员,可用于学习主机管理、服务监控、日志查看、任务调度、权限控制、资源统计、脚本执行与故障告警等常见运维模块的落…

2026/11/5 13:25:11

基于ADS122U04IPWR与STM32F427ZI的工业测温方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 13:20:10

Dubbo服务下线实时告警:基于ZooKeeper Watch的轻量级监控方案

简介:这是一套面向Java分布式系统运维与开发人员的Dubbo服务高可用监控解决方案,聚焦于Provider异常下线的实时感知与告警响应。资源通过Zookeeper监听机制实现服务状态变更捕获,结合邮件通知模块,在不侵入原有Dubbo代码的前提下&…

2026/11/5 13:20:10

SSM超市管理系统源码解析:从分层架构到本地部署与业务修改

简介:这份资源是面向高校计算机专业学生与Java初学者的一套SSM框架课程设计完整项目,围绕超市管理场景,帮助读者完成从框架整合到业务模块落地的实战练习,适合作为毕业设计或课程作业的参考方案。压缩包共792个文件,约…

2026/11/5 13:20:10

SSM超市管理系统实战:从课设跑通到二次开发全解析

简介:这份资源是面向高校计算机相关专业学生与Java入门开发者的SSM课程设计参考项目,围绕超市管理场景,解决商品、库存、销售等业务模块从设计到落地的实践需求,适合作为课程作业、毕业设计原型或SSM框架练手案例。压缩包共792个文…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑