本文主要介绍gcc编译工具发展的背景故事以及常用命令,了解EFF文件格式。
目录
一、GCC简介
- GCC(GNU Compiler Collection,GNU编译器 套件)是由GNU开发的编程语言编译器。GCC 原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理C语言。但是GCC 很快地扩展,变得可处理 C++。后来又扩展能够支持更多编程语言,如 Fortran、Pascal、Objective-C、Java、Ada、Go 以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。
- GCC 原本作为 GNU 操作系统的官方编译器,现已被大多数类 Unix 操作系统(如 Linux、BSD、Mac OS X 等)采纳为标准的编译器,GCC 同样适用于微软的 Windows。另一方面,说到 GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。
二、GCC的伙伴
GCC 不是一个人在战斗,GCC 背后其实有一堆战友。
1、Binutils
Binutils 是一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size 等。这 一组工具是开发和调试不可缺少的工具 ,分别简介如下:
工具 | 作用 |
---|---|
addr2line | 将程序地址翻译成文件名和行号;给定地址和可执行文件名称,它使用其中的调试信息判断与此地址有关联的源文件和行号,该工具将帮助调试器在调试的过程中定位对应的源代码位置 |
ar | 创建、修改和提取归档,主要用于创建 静态库 |
as | 主要用于汇编,一个汇编器,将 gcc 的输出汇编为对象文件 into object files |
c++filt | 被链接器用于修复 C++ 和 Java 符号,防止重载的函数相互冲突 |
elfedit | 更新 ELF 文件的 ELF 头 |
gprof | 显示分析数据的调用图表 |
ldd | 可以用于查看一个可执行程序依赖的共享库 |
ld | 一个链接器,将几个对象和归档文件组合成一个文件,重新定位它们的数据并且捆绑符号索引 |
ld.bfd | 到 ld 的硬链接 |
libiberty | 包含多个 GNU 程序会使用的途径,包括 getopt、obstack、strerror、strtol 和 strtoul |
libbfd | 二进制文件描述器库 |
libopcodes | 一个库,用于处理 opcodes——处理器指令的 “可读文本” 版本;用于编制 objdump 这样的工具 |
nm | 列出给定对象文件中出现的符号 |
objcopy | 将一种对象文件翻译成另一种,如 .bin 转换成 .elf 、.elf 转换成 .bin 等。 |
objdump | 显示有关给定对象文件的信息,包含指定显示信息的选项;显示的信息对编译工具开发者很有用,最主要的作用是反汇编 |
ranlib | 创建一个归档的内容索引并存储在归档内;索引列出其成员中可重定位的对象文件定义的所有符号 |
readelf | 显示有关 ELF 二进制文件的信息, readelf -h* .exe 进行查看 |
size | 列出给定对象文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等 |
strings | 对每个给定的文件输出不短于指定长度 (默认为 4) 的所有可打印字符序列;对于对象文件默认只打印初始化和加载部分的字符串,否则扫描整个文件 |
strip | 移除对象文件中的符号,进行文件压缩,进行瘦身 |
2、C运行库
C语言 标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。
- C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义 在 stdio 头文件中。
- C 语言标准 仅仅定义了 C 标准库函数原型,并没有提供实现。因此,C 语言编译器通常需要一个 C 运行时库(C Run Time Libray,-CRT)的支持。
- C 运行时库又常简称为 C 运行库。与 C 语言类似,C++也定义了自己的标准,同时提供相关支持库,称为 C++运行时库。
三、GCC编译过程解析
1、GCC编译流程
编译过程分为四个阶段进行,即预处理(预编译)Preprocessing、编译 Compilation、汇编Assembly 和连接 Linking。
(1)过程:
第一步: 由 .c 文件到 .i 文件,这个过程叫 预处理。
第二步: 由 .i 文件到 .s 文件,这个过程叫编译。
第三步: 由 .s 文件到 .o 文件,这个过程叫 汇编。
第四步: 由 .o 文件到 可执行文件,这个过程叫 链接。
过程 | 作用 | 命令实例 |
---|---|---|
预处理 | 头文件展开,宏替换,去注释 | gcc -E hello.c -o hello.i |
编译 | C文件变为汇编文件 | gcc -S hello.i -o hello.s |
汇编 | 汇编文件变成二进制文件 | gcc -c hello.s -o hello.o |
链接 | 将函数库中对应的代码链接到目标文件中 | gcc hello.o -o hello |
注意:我们也可以直接使用 gcc test.c -o test
一步到位
(2)流程图:
2、实例演示
(1)hello.c的编写
1.首先,我们创建一个test3目录来存放此次的代码
mkdir test3 //创建test3
cd test3 //切换操作路径
2.接着在test3目录下通过vim文本编辑器编写hello.c
- 在命令行键入以下命令打开vim文本编辑器:
vim hello.c
- 通过vim创建并编写hello.c,进入vim后需要键入
【i】
或者【Insert】
才能开始编写代码。
//hello.c
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
- 编辑完成后,键入
【Esc】
,输入:wq
保存并退出vim文本编辑器。这样,我们就完成了 hello.c 的编写。
(2)预处理
- 预处理过程主要进行以下操作:
(1)将所有的 #define 删除,并且展开所有的宏定义;
(2)处理所有条件编译指令,如 #if,#ifdef 等;
(3)处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。
(4)删除所有的注释(//和/**/);
(5)添加行号和文件标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;
(6)保留所有的 #pragma 编译器指令,因为编译器须要使用它们;
- gcc预处理命令:
gcc -E hello.c -o hello.i
- 上述代码将 hello.c 文件经过预处理生成 hello.i 文件。
在该阶段,编译器将上述代码中的 stdio.h 编译进来,并且通过使用 gcc 的选项 -E 让 gcc 在预处理结束后停止编译过程。
(3)编译
- 在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
- gcc编译命令:
gcc -S hello.i -o hello.s
- 上述代码将 hello.i 文件经过预处理生成 hello.s 文件。
-S 选项表示只进行编译(这里编译为名词)而不进行汇编(这个汇编为动词),生成汇编代码。
(4)汇编
- 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 目标文件由段组成。通常一个目标文件中至少有两个段:
代码段(文本段):该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般不可写;
数据段:主要存放程序中要用到的各种常量、全局变量、静态的数据。一般数据段都是可读,可写,可执行的;
- gcc汇编命令:
gcc -c hello.s -o hello.o
- 汇编阶段是把编译阶段生成的 .s 文件转成 .o 目标文件(二进制文件)。
选项 -c 使 gcc 在执行完汇编后停止,生成目标文件。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成 .o 目标文件后,才能进入下一步的链接工作。目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。
(5)链接
- gcc 连接器负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
- 函数库一般分为静态库和动态库两种。
静态库:
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为 .a。
动态库:
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为 .so,gcc 在编译时默认使用动态库。
如果要让 gcc 选择链接静态库则可以指定 gcc 选项-static,该选项会强制使用静态库进行链接。
在 Linux 系统中,gcc 编译链接时的动态库搜索路径的顺序通常为:首先从 gcc 命令的。参数 -L 指定的路径寻找;再从环境变量 LIBRARY_PATH 指定的路径寻址;再从默认路径 /lib、/usr/lib、/usr/local/lib 寻找
在 Linux系统中,执行二进制文件时的动态库搜索路径的顺序通常为:
首先搜索编 译目标代码时指定的动态库搜索路径;
再从环境变量 LD_LIBRARY_PATH 指定的路径寻址;
再从配置文件/etc/ld.so.conf 中指定的动态库搜索路径;再从默认路径/lib、/usr/lib寻找
在 Linux 系统 中,可以用 ldd 命令查看一个可执行程序依赖的共享库
由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动 态库文件,比如 libtest.a 和 libtest.so,gcc 链接时默认优先选择动态库,会链接 libtest.so,如果要让 gcc 选择链接 libtest.a 则可以指定 gcc 选项-static,该选项会强制使用静态库进行链接。
- 让我们重新看看这个程序,在这个程序中并没有定义 printf 的函数实现,且在预编译中包含进的 stdio.h 中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现 printf 函数的呢?最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径 /usr/lib 下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数 printf 了,而这也就是链接的作用。
- gcc链接命令:
gcc hello.o -o hello
- 完成了链接之后,gcc 就可以生成可执行文件 hello。
(6)运行可执行文件
键入./hello
,输出“Hello World!”
四、多个程序文件的编译
通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理这些编译单元。
- 假设有一个由 main.c 和 sub1.c 两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序 test,可以使用下面这条命令:
gcc main.c sub.c -o test
- 如果同时处理的文件不止一个,GCC 仍然会按照 预处理、编译、汇编、链接 的过程依次进行。
- 上面这条命令大致相当于依次执行如下三条命令:
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc test1.o test2.o -o test
具体实现效果可以参考笔者的另外一篇帖子 嵌入式系统开发01——Ubuntu系统下C语言程序开发流程 的第九部分第二小节
五、检错
命令1:
gcc -pedantic illcode.c -o illcode -pedantic
-
编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。
-
-pedantic
选项能够帮助程序员发现一些不符合 ANSI/ISO C 标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的那些情况,才有可能被 GCC 发现并提出警告。 -
除了
-pedantic
之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以 -W 开头,其中最有价值的当数-Wall
了,使用它能够使 GCC 产生尽可能多的警告信息。
命令2:
gcc -Wall illcode.c -o illcode
- GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优秀的 Linux 程序员应该尽量避免产生警告信息,将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上
-Werror
选项,那 么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,上述代码就可以修改成下面这个样子:gcc -Werror test.c -o test
五、ELF文件
1、ELF文件的定义
可执行与可链接格式 (Executable and Linkable Format,ELF),常被称为 ELF格式,是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式,一般用于类Unix系统,比如Linux,Macox等。ELF 格式灵活性高、可扩展,并且跨平台。比如它支持不同的字节序和地址范围,所以它不会不兼容某一特别的 CPU 或指令架构。这也使得 ELF 格式能够被运行于众多不同平台的各种操作系统所广泛采纳。
2、ELF文件的分类
ELF文件主要用来表示 3 种类型的文件:
- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。比如编译的中间产物 .o文件;
- 可执行文件:一个可执行文件;
- 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。比如linux下的 .so文件。
3、ELF文件的结构
一个完整的ELF文件一般会包括如下几个内容:ELF头、Section头、Program头和Section。
4、ElF文件的段
ELF 文件格式如上图所示,位于 ELF 头 和 节头部表 之间的都是段(Section)。
一个典型的 ELF 文件包含下面几个段:
段名 | 内容 |
---|---|
.text: | 已编译程序的指令代码段。 |
.rodata: | ro 代表 read only,即只读数据(比如常数 const)。 |
.data: | 已初始化的 C 程序全局变量和静态局部变量。 |
.bss: | 未初始化的 C 程序全局变量和静态局部变量。 |
.debug: | 调试符号表,调试器用此段的信息帮助调试。 |
可以使用 readelf -S
查看其各个 section 的信息,如下:
readelf -S hello
5、反汇编ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。
- 使用 objdump -D 对其进行反汇编:
objdump -D hello
- 使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
gcc -o hello -g hello.c //要加上-g 选项
objdump -S hello
总结
通过本次实践,我了解到了gcc编译工具发展的背景故事以及工具集中各软件的用途,对gcc编译的整个过程有了进一步的理解,掌握了gcc的一些常用命令,了解了EFF文件格式的一些基本知识。也让我明白了自己掌握的知识不完善,需要多阅读帖子、多实际操作来提升自我。
参考列表:
1.Binutils工具集 解析
2.GCC编译详解
3.ELF文件详解
4.GCC背后的故事
5.GCC常用命令
6.https://blog.csdn.net/weixin_44316996/article/details/107396385?spm=1001.2014.3001.5506