文章目录
GCC编译器背后的故事及常用命令、了解ELF文件格式
前言
一、GCC简介
二、GCC背后的战友
1.Binutils
2.C运行库
三、GCC编译流程及对应命令
1.编译流程图及命令框图
2.预处理
3.编译
4.汇编
5.链接
四、GCC其他常用命令简介
1.多个程序文件的编译
2.检错
3.库文件连接
3.1 编译成可执行文件
3.2 链接
3.3 强制链接时使用静态链接库
五、ELF文件学习
1.ELF文件格式
2.反汇编ELF
总结
一、GCC简介
gcc简介
Linux系统下的gcc(GNU C Compiler)是GNU推出的功能强大、性能优越的多平台编译器,是GNU的代表作品之一。gcc是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高20%~30%。gcc编译器能将C、C++语言源程序、汇程式化序和目标程序编译、连接成可执行文件,如果没有给出可执行文件的名字,gcc将生成一个名为a.out的文件。在Linux系统中,可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件。而gcc则通过后缀来区别输入文件的类别,下面我们来介绍gcc所遵循的部分约定规则。
.c为后缀的文件,C语言源代码文件;
.a为后缀的文件,是由目标文件构成的档案库文件;
.C,.cc或.cxx 为后缀的文件,是C++源代码文件;
.h为后缀的文件,是程序所包含的头文件;
.i 为后缀的文件,是已经预处理过的C源代码文件;
.ii为后缀的文件,是已经预处理过的C++源代码文件;
.m为后缀的文件,是Objective-C源代码文件;
.o为后缀的文件,是编译后的目标文件;
.s为后缀的文件,是汇编语言源代码文件;
.S为后缀的文件,是经过预编译的汇编语言源代码文件。
二、GCC背后的战友
1.Binutils
binutils工具链包括很多的工具,下面仅列出常用的几个工具:
ld链接器 将多个目标文件,链接成一个可执行文件(或目标库文件)。
as汇编器 将汇编源代码,编译为(目标)机器代码
readelf 显示ELF格式的(目标)文件的信息
objdump 显示目标文件中的信息(反汇编)
objcopy 拷贝并翻译(转换)文件,可用于不同格式的二进制文件的转换
gprof 显示配置信息
nm 列出目标文件中的符号
ar 用来操作(.a)档案文件,比如创建,修改,提取内容等
addr2line 将地址转换为(文件名和)行号的工具,一般主要用于反汇编(带确认此点)
gold 一个新的,速度更快的,只针对于ELF的链接器,当前出于测试中,还不是很成熟稳定
在binutils中有一个BFD文件夹,BFD主要用来隔离可执行的二进制文件和具体的操作,它将各种工具和具体的文件格式分离开来,binutils中的所有工具都是通过BFD来操作二进制代码的,如果修改或者添加一种可执行文件的格式,我们只需要修改BFD的后端代码即可,不涉及到其他的工具的修改。
2.C运行库
现在可供选择的C运行库有glibc, uClibc以及newlib等。glibc是由GNU项目提供的标准C运行库,它针对PC应用设计,较庞大,但能提供最优的兼容性。如果一般的嵌入式开发可选用uClibc。uClibc原本是uCLinux开发过程中的一个C语言库,现已经独立于uCLinux项目并且进一步完善。它对glibc的大部分函数进行了重写,并且目标就定位于嵌入式,所以其相对glibc而言要小巧很多。此外由于它的函数与glibc保持一致,这样很多原本基于glibc开发的软件基本无需改动便可改用uClibc编译运行,使得在嵌入式系统上占用的内存和磁盘空间更少。但由于毕竟不是标准的C运行库,因此uClibc拥有着一定的兼容性问题。
Newlib是一个面向嵌入式系统的C运行库.最初是由Solutions收集组装的一个源代码集合,取
名为newlib,现在由Red Hat维护,目前的最新的版本是1.11.01'1.
对于与GNU兼容的嵌入式C运行库,Newlib并不是唯一的选择,但是从成熟度来讲,newlib是最优秀
的.newlib具有独特的体系结构,使得它能够非常好地满足深度嵌入式系统的要求.newlib可移植性强,
具有可重入特性,功能完备等特点,已广泛应用于各种嵌入式系统中.
1)运行时库就是 C run-time library,是 C 而非 C++ 语言世界的概念:取这个名字就是因为你的 C 程序运行时需要这些库中的函数.
2)C 语言是所谓的“小内核”语言,就其语言本身来说很小(不多的关键字,程序流程控制,数据类型等);所以,C 语言内核开发出来之后,Dennis Ritchie 和 Brian Kernighan 就用 C 本身重写了 90% 以上的 UNIX 系统函数,并且把其中最常用的部分独立出来,形成头文件和对应的 LIBRARY,C run-time library 就是这样形成的。
3)随后,随着 C 语言的流行,各个 C 编译器的生产商/个体/团体都遵循老的传统,在不同平台上都有相对应的 Standard Library,但大部分实现都是与各个平台有关的。由于各个 C 编译器对 C 的支持和理解有很多分歧和微妙的差别,所以就有了 ANSI C;ANSI C (主观意图上)详细的规定了 C 语言各个要素的具体含义和编译器实现要求,引进了新的函数声明方式,同时订立了 Standard Library 的标准形式。所以C运行时库由编译器生产商提供。至于由其他厂商/个人/团体提供的头文件和库函数,应当称为第三方 C 运行库(Third party C run-time libraries)。
4)C run-time library里面含有初始化代码,还有错误处理代码(例如divide by zero处理)。你写的程序可以没有math库,程序照样运行,只是不能处理复杂的数学运算,不过如果没有了C run-time库,main()就不会被调用,exit()也不能被响应。因为C run-time library包含了C程序运行的最基本和最常用的函数。
5)到了 C++ 世界里,有另外一个概念:Standard C++ Library,它包括了上面所说的 C run-time library 和 STL。包含 C run-time library 的原因很明显,C++ 是 C 的超集,没有理由再重新来一个 C++ run-time library. VC针对C++ 加入的Standard C++ Library主要包括:LIBCP.LIB, LIBCPMT.LIB和 MSVCPRT.LIB
三、GCC编译流程及对应命令
2.1编译过程
GCC即GNU Compiler Collection,编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编 (Assembly)和链接(Linking),gcc可以理解为编译管理工具,它会具体调用相关的工具进行执行,具体如下图:
2.2预处理
可以输出到test.i文件中存放着test.c经预处理之后的代码。打开test.i文件,看一看,就明白了。后面那条指令,是直接在命令行窗口中输出预处理后的代码.
gcc的-E选项,可以让编译器在预处理后停止,并输出预处理结果。在本例中,预处理结果就是将stdio.h 文件中的内容插入到test.c中了。
2.3编译为汇编代码(Compilation)
预处理之后,可直接对生成的test.i文件编译,生成汇编代码:
gcc的-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件。
2.4汇编(Assembly)
对于上一小节中生成的汇编代码文件test.s,as汇编器负责将其编译为目标文件,如下:
[root@localhost /]# gcc -c test.s -o test.o
1
2.5链接(Linking)
gcc连接器是gas提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
对于上一小节中生成的test.o,将其与C标准输入输出库进行连接,最终生成程序test
gcc参数详解
编译参数 详细说明
-o 指定编译的⽬标,否则会⽣成的⽬标⽂件名是a.out; gcc main.c -o main
-S 把源文件编译成汇编代码
-E 只执行预处理
-include 包含头文件,功能如同在源码的语句#include <xxx.h>
-I (大写i)指定程序包含头文件的路径,一般用于指定第三方库的头文件。
-L 编译时,用于指定程序第三方库的查找路径。
-l 链接时,指定程序需要进行链接的库。注:一般库文件名是libxxx.so,-I指定xxx即可。如-Ixxx
-rpath 程序执⾏需要指定动态库的路径,但是可以⽤-rpath参数在编译时指定程序运⾏时需要加载的库的路径。
-D 程序编译阶段可以定义一些宏,该方法可以让程序有选择性的运行代码。
-0n 这是程序的优化等级。n的范围是0-3。n越大优化等级越高,程序运行的越快。否则越慢,n==0时是关闭优化。利于程序的调试,一般程序调试阶段会关闭优化等级,发布程序会把优化等级设为-O2。-O0:不进行优化处理。-O 或 -O1:优化生成代码。-O2:进一步优化。-O3 比 -O2 更进一步优化,包括 inline 函数。
-g 打印程序的调试信息,如果需要使⽤gdb⼯具进⾏调试程序,程序编译的时候,需要加上该参数。
-share 编译的时候尽量使用动态库。(除非只有静态库,没有动态库)
-static 禁止使用动态库,编译的时候只加载静态库,这会导致执行件很大。
-w 不生成任何的警告信息。
-Wall 生成所有的警告信息。
-fpic 使输出的对象模块可重定位地址方式行成的。
-shared 把对应的源文件形成对应的动态链接库。
3 、多个程序文件
通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理 这些编译单元。假设有一个由 test1.c 和 test2.c 两个源文件组成的程序,为了对它们进行编译,并 最终生成可执行程序 test,可以使用下面这条命令:
如果同时处理的文件不止一个,GCC 仍然会按照预处理、编译和链接的过程依次进行。如果深究起 来,上面这条命令大致相当于依次执行如下三条命令:
4、 检错
编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。或者换句话说,-pedantic 选项能够帮助程序员发现一些不符合 ANSI/ISO C 标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的 那些情况,才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W 开头,其中最有价值的当数-Wall 了,使用它能够使 GCC 产生尽可能多的警告信息。
gcc -Wall illcode.c -o illcode
1
GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优 秀的 Linux 程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将 警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror 选项,那 么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:
gcc -Werror test.c -o test
1
5 、库文件连接
5.1 编译成可执行文件
首先我们要进行编译 test.c 为目标文件,这个时候需要执行
5.2 链接
最后我们把所有目标文件链接成可执行文件:
Linux 下的库文件分为两大类分别是动态链接库(通常以.so 结尾)和静态链接库(通常以.a 结尾), 二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的
5.3 强制链接时使用静态链接库
默认情况下, GCC 在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链 接库,如果需要的话可以在编译时加上-static 选项,强制使用静态链接库。
在/usr/dev/mysql/lib 目录下有链接时所需要的库文件 libmysqlclient.so 和 libmysqlclient.a,为了让 GCC 在链接时只用到静态链接库,可以使用下面的命令:
静态库链接时搜索路径顺序:
ld 会去找 GCC 命令中的参数-L
再找 gcc 的环境变量 LIBRARY_PATH
再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的
动态链接时、执行时搜索路径顺序:
1.编译目标代码时指定的动态库搜索路径
2.环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
3.配置文件/etc/ld.so.conf 中指定的动态库搜索路径
4.默认的动态库搜索路径/lib
5.默认的动态库搜索路径/usr/lib
有关环境变量:
LIBRARY_PATH 环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量:指定程序动态链接库文件搜索路
一、ELF文件格式
1.1 ELF文件介绍
ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。
首先我们需要知道对象文件有哪些:
类型 实例
可重定位的对象文件(Relocatable file) .o;.a;.ko
可执行的对象文件(Executable file) vi、gdb、及我们用链接器生成的可执行文件、bash shell 程序
可被共享的对象文件(Shared object file) .so
核心转储文件(Core Dump File) 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 core dump
想要知道一个对象文件属于以上类型的哪一种,我们可以使用file +对象文件名命令来查看,例如
那对于 file 命令来说,它又能如何知道这些信息?答案是在ELF对象文件的最前面有一个ELF文件头,里面记载了所适用的处理器、对象文件类型等各种信息。
首先,ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:
程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。
节区头部表(Section Header Table)包含了描述文件节区的信息,比如大小、偏移等。
2.2.1 ELF Header
我们打开/usr/include/elf.h文件,开始处是一个ELF头部(ELF Header),用来描述整个文件的组织,这些信息独立于处理器,也独立于文件中的其余内容。
一个 ELF 文件包含 0 到多个 segments,它描述了如何创建运行时进程/内存映像。当内核遇到这些 segments,它通过 mmap(2) 系统调用将它们映射到虚拟地址空间 。也就是说,它将预定义的指令转换到内存中。如果 ELF 文件是可执行文件,它必须包含 program headers.否则,它就无法运行。操作系统使用这些头,底层的数据结构,构建进程。shared libraries 也是这样处理的。
静态 vs 动态
处理 ELF 文件,有两种类型的链接方式。静态和动态指的是它们使用的库。为了优化,我们看到二进制一般是 ”动态“的,意味着它需要外部组件,才能运行。一般这些组件是通用的库,包含通用的函数,像打开文件或创建 socket。静态文件,包含了所有的库。体积更大,也更可移植(例如,在另一个系统使用)。
如果你想检查一个文件是静态还是动态,使用 file 命令。如下所示:
$ file /bin/ps
/bin/ps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=2053194ca4ee8754c695f5a7a7cff2fb8fdd297e, stripped
要看依赖哪些库,直接使用 ldd:
$ ldd /bin/ps
linux-vdso.so.1 => (0x00007ffe5ef0d000)
libprocps.so.3 => /lib/x86_64-linux-gnu/libprocps.so.3 (0x00007f8959711000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f895934c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8959935000)
提示:要看底层依赖关系,可以使用 lddtree。
- 由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法
- 使用 objdump -D 对其进行反汇编:
- objdump -D hello
总结
通过本篇文章的学习,希望能够帮助大家进一步学习和了解在Ubuntu18.04系统下,gcc编译的详细过程分析、gcc常用命令参数等等,以及对于ELF文件格式的初步了解与学习。同时也期待大家能够积极留言,指出我存在的问题,谢谢!