从源码到可执行文件

本文最后更新于:2021-12-06 下午

从源码到可执行文件

我们平时接触到的都是高级语言,比如C语言,而当它真正在操作系统上执行的时候,每一条C语句都会被翻译成一系列的低级机器语言指令。最后,这些指令按照可执行文件的格式打包,并以二进制的形式存放起来。

编译原理

编译器的作用就是读入以某种语言(源语言)编写的程序,输出等价的用另一种语言(目标语言)编写的程序。编译器可以分为前端和后端。前段主要将源程序分解成组成要素和相应的语法结构,通过这个结构创建源程序的中间表示,同时收集和源程序相关的信息,存放到符号表之中;后端与机器相关,主要是根据中间表示和符号表信息构造目标程序。

编译过程可以大致分为以下五个步骤

  1. 词法分析:读取源程序的字符流,输出为有意义的词素;
  2. 语法分析:根据各个词法单元的第一个分量来创建树型的中间表示形式,通常是语法树;
  3. 语义分析:使用语法树和符号表中的信息,检测源程序是否满足语言定义的语义约束,同时收集类型信息,用于代码生成,类型检查和类型转换;
  4. 中间代码生成和优化:根据语义分析输出,生成类机器语言的中间表示,如三地址码,然后对生成的中间代码进行分析和优化;
  5. 代码生成和优化:把中间表示形式映射到目标机器语言。

GCC编译过程

在linux中,使用以下指令完成源程序到目标程序的转化

1
gcc hello.c -o hello

GCC编译器读取hello.c,经过预处理、编译、汇编、链接四个步骤,将其翻译成了可执行目标程序hello。

用这段代码来做示例

1
2
3
4
5
#include <stdio.h>
int main()
{
printf("hello world\n");
}

预处理阶段

GCC编译的第一个阶段是预处理阶段,主要是处理源代码中以#开始的预处理指令,比如

#include

#define

将其转换后直接插入程序文本之中,得到另一个C程序,通常以“.i”作为文件扩展名。在命令行中添加编译选项-E可以单独执行预处理

1
gcc -E hello.c -o hello.i

预处理的规则如下:

  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如#if、#ifdef、#elif、#else、#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释
  • 添加行号和文件名标识,比如# 2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生错误或警告时能够显示行号
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们

部分hello.i内容如下图所示

编译阶段

GCC的第二阶段是编译,该阶段将预处理文件进行一系列的词法分析、语法分析、语义分析以及优化,最终生成汇编代码。添加-S选项,是编译选项。操作对象可以是源代码hello.c也可以是预处理文件hello.i。实际上在GCC的实现中,已经将预处理和编译合并处理。

1
gcc -S hello.i -o hello.s

hello.s文件内容如下

其中生成的汇编代码,printf函数被替换成了puts函数,这是因为当printf只有单一参数时,与puts是十分类似,所以GCC的优化策略将其替换提高性能。

汇编阶段

GCC第三个阶段是汇编,汇编器根据汇编指令与机器指令的对照表进行翻译,将hello.s汇编成目标文件hello.o。在命令行中添加编译选项-c,操作对象可以是hello.c也可以是hello.s。

1
gcc -c hello.s -o hello.o

此时hello.o文件是一个可重定位文件

可以使用objdump命令查看内容

因为此时还没有进行链接,所以看到一些地址都是被设置为了0

链接阶段

GCC编译的第四个阶段是链接,可以分为静态链接和动态链接两种。GCC默认使用动态链接,添加编译选项-static可以指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配、符号绑定和重定位等操作。

1
gcc hello.o -o hello

链接操作由链接器(ld.so)完成,结果就会得到hello,这是一个可执行文件

参考

https://www.cnblogs.com/kele-dad/p/9490640.html

https://www.linuxidc.com/Linux/2016-09/135473.htm

《CTF竞赛权威指南(PWN篇)》


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!