程序编译的基本流程

程序的编译过程,其实就是将我们编写的C源程序翻译成CPU能够 识别和运行的二进制机器指令的过程。关于C程序我们已经很熟悉了: 一个C程序主要由一行行C语言语句组成,不同的语句构成一个个代码块或函数,每个语句由C语言的关键字、运算符、预处理命令、用户定义的变量名、函数名等很多token构成。一个C语言项目通常由多个文件组成。

从C程序到可执行文件,整个编译过程并不是一气呵成、一步完成 的,而是环环相扣、多步执行的。如下图所示,程序的整个编译流程 主要分为以下几个阶段:预处理、编译、汇编、链接。每个阶段需要 调用不同的工具去完成,上一阶段的输出作为下一阶段的输入,步步推进。

在一个多文件的C项目中,编译器是以C源文件为单位进行编译的。在编译的不同阶段,编译程序(如gcc、arm-linux-gcc)会调用不 同的工具来完成不同阶段的任务。在编译器安装路径的bin目录下,你 会看到各种各样的编译工具,gcc在程序编译过程中会分别调用它们, 常见的工具有预处理器、编译器、汇编器、链接器。

● 预处理器:将源文件main.c经过预处理变为main.i。

● 编译器:将预处理后的main.i编译为汇编文件main.s。

● 汇编器:将汇编文件main.s编译为目标文件main.o。

● 链接器:将各个目标文件main.o、sub.o链接成可执行文件a.out。

最后生成的可执行文件a.out其实也是目标文件(object file),唯 一不同的是,a.out是一种可执行的目标文件。目标文件一般可以分为3 种。

● 可重定位的目标文件(relocatable files)。

● 可执行的目标文件(executable files)。

● 可被共享的目标文件(shared object files)。

汇编器生成的目标文件是可重定位的目标文件,是不可执行的, 需要链接器经过链接、重定位之后才能运行。可被共享的目标文件一 般以共享库的形式存在,在程序运行时需要动态加载到内存,跟应用 程序一起运行。

在 一 个 可 执 行 文 件 中 , 我 们 比 较 熟 悉 的 section 有.text、.data、.bss,就是我们常说的代码段、数据段、BSS段。C程序中定义的函数、变量、未初始化的全局变量经过编译后会放置在不同的段中:函数翻译成二进制指令放在代码段中,初始化的全局变量和 静态局部变量放在数据段中。BSS段比较特殊,一般来讲,未初始化的全局变量和静态变量会放置在BSS段中,但是因为它们未初始化,默认值全部是0,其实没有必要再单独开辟空间存储,为了节省存储空间, 所以在可执行文件中BSS段是不占用空间的。但是BSS段的大小、起始地址和各个变量的地址信息会分别保存在节头表section header table和 符号表.symtab里,当程序运行时,加载器会根据这些信息在内存中紧挨着数据段的后面为BSS段开辟一片存储空间,为各个变量分配存储单元。

如下图所示,就是将C程序中定义的函数、变量,挑挑拣拣、 加以分类,分别放置在可执行文件的代码段、数据段和BSS段中。程序中定义的一些字符串、printf函数打印的字符串常量则放置在只读数据段.rodata中。如果程序在编译时设置为debug模式,则可执行文件中还 会有一个专门的.debug section,用来保存可执行文件中每一条二进制指令对应的源码位置信息。根据这些信息,GDB调试器就可以支持源码级的单步调试,否则你单步执行的都是二进制指令,可读性不高,不方便调试。在最后环节,编译器还会在可执行文件中添加一些其他section,如.init section,这些代码来自C语言运行库的一些汇编代码, 用来初始化C程序运行所依赖的环境,如内存堆栈的初始化等。

你可能感兴趣的:(linux,windows,编辑器)