Gcc不是一个人在战斗

GCC简介

GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C语言;它现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal 语言、COBOL语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C 语言编译器的意思了,而是变成了 GNU Compiler Collection 也即是 GNU 编译器家族的意思了。另一方面,说到 GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。

Binutils 工具包

一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as 、ld、ldd 、readelf 、size等。这一组工具是开发和调试不可缺少的工具

addr2line
来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置

as
主要用于汇编

ld
主要用于连接
ar
主要用于创建静态库
静态库与动态库
将多个 .o 目标文件生成一个库文件,存在两种类型的库:静态库,动态库

Windows 中,静态库是后缀为 .lib 的文件,共享库是后缀为 .dll 的文件;在 Linux 系统中,静态库后缀为 .a ,动态库后缀为 .so
静态库
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。
静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。
动态库
动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。
动态库的优点是,不需要拷贝到目标程序中,不会影响目标程序的体积,而且同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。同时,编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)。
ldd
可以用于查看一个可执行程序的依赖的共享库

objcopy
将一种对象文件翻译成另一种格式,比如将 .bin 转换为 .elf 等

objdump
主要作用是反汇编

readelf
显示有关 ELF 文件的信息

size
列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等

C运行库

C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。

C 语言的标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此,C 语言编译器通常需要一个 C 运行时库( C Run Time Libray , CRT )的支持。C 运行时库又常简称为 C 运行库。与 C 语言类似,c++ 也定义了自己的标准,同时提供相关支持库,成为 C++ 时运行库
实际上,上述的编译步骤分为四个阶段:

1.预处理(也叫预编译,Preprocessing)

2.编译(Compilation)

3.汇编(Assenbly)

4.连接(Linking)
1,预处理
主要包括一下过程:

1.将所有的 #define 删除,并展开所有宏定义,处理所有的条件预编译指令,例如 #if #ifdef 等

2.处理 #include 预编译命令,将被包含的文件插入到该预编译指令的位置

3.删除所有注释

4.添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告的行号

5.保留所有的 #pragma 编译器指令,后迅速编译过程需要使用
使用gcc预处理命令如下

 gcc -E main .c -o main.i
//可以直接输出预处理后存放在hello.c 文件预处理生产hello.i
gcc 的 -E 选项,可以让编译器在预处理后停止,-o 表示输出编译后的文件。并输出预处理结果。

2,编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及
优化后生成相应的汇编代码。
使用 gcc 进行编译的命令如下:

 gcc -S main.i -o main.s
// 将预处理生成的 main.i 文件编译生成汇编程序 main.s
// GCC 的选项-S 使 GCC 在

3汇编
汇编过程调用对汇编代码进行处理,生成机器可以识别的指令,由于每一个汇编语句几乎都对应一条处理器指令,所以,汇编想对于编译过程简单一些,调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可

当程序由多个源代码文件构成时,每个文件都要先完成汇编,生成 .o 目标文件之后,才能进行链接

对于上一小节中生成的汇编代码文件 main.s,gas 汇编器负责将其编译为目标文件

$ gcc -S main.i -o main.s
// 将预处理生成的 main.i 文件编译生成汇编程序 main.s
// GCC 的选项-S 使 GCC 在执行完编译后停止,生成汇编程序

连接
链接分为:动态链接、静态链接

静态链接
在编译阶段直接把静态库加入到可执行文件中,可执行文件较大。连接器将函数代码从所在地拷贝到最终执行程序中

动态链接
链接阶段,仅仅只加入一些描述信息,而程序执行是从系统中制定位置把相应动态库加载到内存中

注意: 关于动态库与静态库的进一步内容,可以看我的另一篇文章:GCC 入门之 静态库 与 动态库

连接过程:
为创建可执行文件,连接器必须要完成的任务是:符号解析、重定位

符号解析:把目标文件中符号的定义和引用联系起来

重定位:把符号定义和内存地址对应起来然后修改所有对符号的作用

gcc 连接器是 gas 提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。

对于上一小节中生成的 main.o,将其与C标准输入输出库进行连接,最终生成程序 main

gcc main.o -o main

在命令行窗口,可以直接执行 ./test,运行结果如下

通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理这些编译单元。假设有一个由 main.c 和 sub1.c 两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序 main,可以使用下面这条命令:

gcc main.c sub1.c -o main
如果同时处理的文件不止一个,GCC 仍然会按照预处理、编译和链接的过程依次进行。如果深究起来,上面这条命令大致相当于依次执行如下三条命令:

gcc -c main.c -o main.o
gcc -c sub1.c -o sub1.o
gcc main.o sub1.o -o main

检错

gcc -pedantic main.c -o main

-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。

或者换句话说,-pedantic 选项能够帮助程序员发现一些不符合 ANSI/ISO C 标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的那些情况,才有可能被 GCC 发现并提出警告。

除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以 -W 开头,其中最有价值的当数 -Wall 了,使用它能够使 GCC 产生尽可能多的警告信息。

gcc -Wall main.c -o main

GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优秀的 Linux 程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上 -Werror 选项,那么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:

gcc -Werror main.c -o main

ELF 文件解析

ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都
是段(Section)。一个典型的 ELF 文件包含下面几个段:
.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。

可以使用以下命令查看各个 Section 的信息:

readelf -S hello

显示

2.反汇编 ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包
含的指令和数据,需要使用反汇编的方法。
使用 objdump -D 对其进行反汇编如下:

objdump -D main

image

posted @ 2022-09-27 19:58  qgrwljzy  阅读(65)  评论(0)    收藏  举报