进入到了第二章的学习,第二章主要学习二进制文件是如何产生的,以及Linux中的二进制文件ELF文件。
这一小节我们先学习二进制文件是如何产生的,即从源代码到可执行文件的过程
1. 展示源代码
下面将采用一段Hello World程序来演示这个过程
#include <stdio.h>
int main() {
print("Hello, world!\n");
}
使用如下命令对以上C语言程序进行编译
gcc hello.c -o hello -save-temps --verbose
以上命令中,-save-temps表示将编译过程中生成的中间文件保存下来,--verbose表示查看GCC编译的详细工作流程。
2. 解读中间文件
众所周知,GCC编译共四个阶段:预处理、编译、汇编、链接。
2.1 预编译阶段
/usr/lib/gcc/x86_64-linux-gnu/11/cc1这是第一阶段,cc1是编译器,对应预处理和编译阶段。
何为预处理?
预处理就是处理源代码中以“#”开始的预处理指令,比如“#include”、“#define”等,将其转化后直接插入到程序文本中,得到另一个程序,通常以“.i”作为文件扩展名。
你也可以敲入以下命令得到一个预编译文件:
gcc -E hello.c -o hello.i
-E表示单独执行预处理过程。
经过预处理后的 hello.i 文件大概长这样👇(GCC版本不同,编译出来的东西也不同)
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
……
extern int printf(const char *__restrict __format, …);
……
int main() {
printf("Hello, world!\n");
}
通过以上预处理文件,我们可以观察发现,在文件进行预处理阶段时,主要进行了以下动作:
- 递归处理“#include”预处理指令,将对应文件的内容复制到该指令的位置;
- 删除所有“#define”指令,并且在其被引用的位置递归地展开所有的宏定义;
- 处理所有条件预处理指令:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等;
- 删除所有注释;
- 添加行号和文件名标识。
2.2 编译阶段
GCC编译的第二阶段即编译
在编译阶段主要进行了词法分析、语法分析、语义分析以及优化等一系列动作,最终生成汇编代码。
你也可以敲以下命令得到编译后的文件:
gcc -S hello.c -o hello.s
-S表示将预处理和编译合并处理,操作对象可以是源代码hello.o或者是预处理文件hello.i
hello.s文件如下所示:
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello, world!\n"
.text
.globl main
2.3 汇编阶段
as是汇编器,对应第三阶段——汇编。汇编器根据汇编指令与机器指令的对照表进行翻译,将hello.s汇编成目标文件hello.o。
你也可以使用如下命令直接生成目标文件:
gcc -c hello.c -o hello.o
此时生成的hello.o为可重定位文件,使用objdump -sd hello.o命令查看其内容:
先解释一下参数的意思,-s表示sections段,-d表示disassemble,即反汇编;sections段不理解没关系,这个以后会解释。
继续解释一下里面的汇编代码的意思。
由于尚未进行链接,对象文件中符号的虚拟地址无法确定,因此“Hello,world!”这个字符串的地址被设置为0x0000,看下图👇
“call puts”指令中函数puts()的地址被设置为下一条指令的地址0x1c
2.4 链接阶段
collect2是链接器,是对ld命令的封装,用于将C语言运行时库(CRT)中的目标文件(crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o)以及所需的动态链接库(libgcc.so、libgcc_s.so、ligc.so)链接到可执行hello中。
链接分为两种:静态链接和动态链接
GCC默认使用动态链接
若要使用静态链接应添加编译选项“-static”即可指定使用静态链接
链接阶段主要进行地址和空间分配、符号绑定、重定位等操作。