从源码到可执行文件:hello.c 的二进制之旅

发布于:2025-08-16 ⋅ 阅读:(19) ⋅ 点赞:(0)

0.引言

你有没有想过为什么编译后才能运行?编译器在这其中起到什么作用?生成的可执行文件里有什么?操作系统又是如何加载它的呢?本系列文章会随着各个章节展开来让读者对这些问题有清晰的认知,整体章节内容规划如下:

编译流程的整体认识(hello.c到a.out)
编译器的翻译流程(词法分析、语法树与优化详解)
目标文件的格式(.o文件解析,符号表、重定位与链接报错解析)
静态链接原理(理解为什么有的程序“打包后会很大)
动态链接原理(理解共享库(.so)如何实现“一次编译,到处运行”)
程序装载流程(理解操作系统如何加载它,在什么位置运行)
编译链接实战(段错误、符号冲突与性能优化以及交叉编译和LLVM)

本文是第一篇,将会介绍基础的概念,从宏观视角来看hello.c如何变成a.out。

1.整体介绍

我们先从简单的hello.c开始:

#include <stdio.h>  
int main() {  
    printf("Hello World\n");  
    return 0;  
}

编写好代码后我们只需要执行gcc hello.c就会在当前目录下生成一个可执行的a.out,虽然我们只是执行了一条命令,但其实际是经历了四大阶段(预编译、编译、汇编、链接),其中每一个阶段都有其自己的规则。

在这里插入图片描述

2.预编译

预编译是将源代码文件(hello.c)和相关的头文件(stdio.h以及其对应包含的头文件)编译生成一个.i文件,这一阶段主要处理源代码文件中的"#"开头的预编译指令,主要的规则如下,我们可以通过查看.i文件来检查一些宏定义是否正确展开或者头文件是否正确包含。

1)处理条件编译指令(如#if、#ifdef、#elif、#else、#endif)。

2)展开头文件(处理#include,将include的头文件内容插入指令位置,这个过程会递归展开)。

3)处理define(将#define进行展开)。

4)删除注释(包括//或者/*注释)。

5)保留#pragma指令(编译器后续使用)。

6)增加行号和文件名标识(出问题后用来作为报错信息)。

#可以使用gcc -E查看预处理后的结果,打开hello.i就能看到被展开的内容
gcc -E hello.c -o hello.i

3.编译

预编译可以看成是进行简单的替换和规则处理,编译的话则是要进行词法分析、语法分析、语义分析、代码优化和生成。此处做简单介绍,下一篇文章会做更为具体说明。

1)词法分析:将代码拆分成一个个的词法单元(Token)。

2)语法分析:根据语法规则,将Token组成抽象语法树(AST)。

3)语义分析:检查语义的正确性,只能进行静态的检查(如类型是否匹配,变量是否定义等)。

4)代码优化和生成:对AST进行优化(如常量折叠、循环展开等),生成汇编代码。

#使用gcc -S生成汇编指令(完成了预编译,编译)
gcc -S hello.c -o hello.s

其部分代码如下:
在这里插入图片描述

4.汇编

汇编就是将汇编代码(编译生成的代码)转换为机器可以执行的指令的过程,几乎每一个汇编指令都有一个对应的机器指令,也就是说汇编过程其实只需要一一翻译就可以了,汇编过程可以通过两个语句来进行生成:

as hello.s -o hello.o
gcc -c hello.s -o hello.o

生成的.o文件为二进制文件,我们可以使用objdump来查看,这个工具我们后面会经常用到。

objdump -d hello.o

hello.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # f <main+0xf>
   f:   e8 00 00 00 00          callq  14 <main+0x14>
  14:   b8 00 00 00 00          mov    $0x0,%eax
  19:   5d                      pop    %rbp
  1a:   c3                      retq   

5.链接

到链接这一步时我们已经得到了机器码程序,但是其还是不能直接运行,因为其缺少一些外部的依赖(比如printf的实现)。链接器会将多个目标文件组合成一个完整的可执行文件(分为静态链接和动态链接),其过程主要是符号解析(找到外部符号位置)和重定位(重设代码中地址),这个过程较为复杂,我们用单独的文章进行说明。

#完成链接,生成可执行程序
gcc hello.o -o a.out
#查看类型
file a.out 
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=13c23451a252ce49af91ee79c8bd4fa7821d86de, for GNU/Linux 3.2.0, not stripped

6.总结

本文从宏观视角介绍了hello.c到二进制可执行文件的过程,其分为四个阶段(也是计算机分层抽象的体现),每个阶段有自己的职责,让其可以高效且方便维护。下一篇我们将去深入编译过程,去了解代码具体怎么翻译成机器码,词法分析语法分析做的事情和代码优化的具体内容。


网站公告

今日签到

点亮在社区的每一天
去签到