当前位置: 首页 > news >正文

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

文章目录

    • 0.引言
    • 1.整体介绍
    • 2.预编译
    • 3.编译
    • 4.汇编
    • 5.链接
    • 6.总结

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.ohello.o:     file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0:   f3 0f 1e fa             endbr64 4:   55                      push   %rbp5:   48 89 e5                mov    %rsp,%rbp8:   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,%eax19:   5d                      pop    %rbp1a:   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到二进制可执行文件的过程,其分为四个阶段(也是计算机分层抽象的体现),每个阶段有自己的职责,让其可以高效且方便维护。下一篇我们将去深入编译过程,去了解代码具体怎么翻译成机器码,词法分析语法分析做的事情和代码优化的具体内容。

http://www.xdnf.cn/news/1301185.html

相关文章:

  • 【Golang】:数据类型
  • Wi-Fi 与蜂窝网络(手机网络)的核心区别,以及 Wi-Fi 技术未来的发展方向
  • Redisson分布式锁实战指南:原理、用法与项目案例
  • GPT 解码策略全解析:从 Beam Search 到 Top-p 采样
  • 流处理、实时分析与RAG驱动的Python ETL框架:构建智能数据管道(上)
  • CPU、内存、存储:生信分析任务的服务器配置精要
  • 第20章 LINQ 笔记
  • 8.15网络编程——UDP和TCP并发服务器
  • 【数据分享】上市公司创新韧性数据(2007-2023)
  • 数据驱动测试提升自动化效率
  • 终极手撸cpu系列-详解底层原理-CPU硬核解剖:从0和1到 看透CPU逻辑设计内部原理,弄清楚现代多线程cpu工作原理
  • Microsoft Visual Studio常用快捷键和Windows系统常用快捷键的整理
  • Linux-地址空间
  • 开发避坑指南(27):Vue3中高效安全修改列表元素属性的方法
  • 【学习笔记】NTP服务客户端配置
  • Go语言中安全停止Goroutine的三种方法及设计哲学
  • 前瞻性技术驱动,枫清科技助力制造企业借助大模型完成生产力转化
  • zabbix部署问题后常见问题
  • 新手入门Makefile:FPGA项目实战教程(二)
  • 【CV 目标检测】②R-CNN模型
  • 【Redis】分布式系统的演化过程
  • MyBatis的基本用法和配置方式
  • Highcharts Dashboards | 打造企业级数据仪表板:从图表到数据驾驶舱
  • 全球电商业财一体化:让出海品牌实现“看得见的增长“
  • demo 通讯录 + 城市选择器 (字母索引左右联动 ListItemGroup+AlphabetIndexer)笔记
  • Nginx反向代理与缓存实现
  • 人工智能与社会治理:从工具到生态的范式重构
  • Kafka生产者——提高生产者吞吐量
  • 切换VSCODE 中的默认 shell
  • GitHub 上 Star 数量前 18 的开源 AI Agent 项目