首页 > 应用开发 > C/C++ > gcc c语言编译流程
1 前言
最近群里讨论个 C 语言的小程序,看起来都不是很难,但是大家对答案有争论,所以想讨论编译原理,做嵌入式要对编译原理有一定的了解,所以转了这篇文章。
我们之前讨论的问题如下代码
#include
#include
#define WEIQIFA 0;
int main ( void )
{
int i = WEIQIFA;
i = i ++ ;
i ++ ;
printf( "%d\n" ,i);
return ;
}
原来是没有那个宏 WEIQIFA 的,但是我为了举例编译原理,特意加上去,编译的第一步就是做宏替换
预编译后变成下面这样
int main(void)
{
int i = 0;;
i = i++;
i++;
printf("%d\n",i);
return 0;
}
用 g++ -g -Wstrict-prototypes -Wall -Wunused -o test test001.c 编译
然后用 objdump -j .text -Sl test | more 查看代码可以看到汇编代码如下
main():
/data/weiqifa/c/bianyiyuanli/test001.c:7
#include
#define WEIQIFA 0;
int main(void)
{
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 48 83 ec 10 sub $0x10,%rsp
/data/weiqifa/c/bianyiyuanli/test001.c:9
int i = WEIQIFA;
40052e: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
/data/weiqifa/c/bianyiyuanli/test001.c:11
i = i++;
400535: 8b 45 fc mov -0x4(%rbp),%eax
400538: 8d 50 01 lea 0x1(%rax),%edx
40053b: 89 55 fc mov %edx,-0x4(%rbp)
40053e: 89 45 fc mov %eax,-0x4(%rbp)
/data/weiqifa/c/bianyiyuanli/test001.c:12
i++;
400541: 83 45 fc 01 addl $0x1,-0x4(%rbp)
/data/weiqifa/c/bianyiyuanli/test001.c:14
printf("%d\n",i);
400545: 8b 45 fc mov -0x4(%rbp),%eax
400548: 89 c6 mov %eax,%esi
40054a: bf e4 05 40 00 mov $0x4005e4,%edi
40054f: b8 00 00 00 00 mov $0x0,%eax
400554: e8 a7 fe ff ff callq 400400 <printf@plt>
/data/weiqifa/c/bianyiyuanli/test001.c:16
return 0;
400559: b8 00 00 00 00 mov $0x0,%eax
/data/weiqifa/c/bianyiyuanli/test001.c:17
}
当时大家讨论很激烈
2 正文
大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如 C , C++ , Java ,另一种是解释型语言,例如 Python 、 Ruby 、 MATLAB 、 JavaScript 。
本文将介绍如何将高层的 C/C++ 语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤:
预处理( Preprocessing )
编译( Compilation )
汇编( Assembly )
链接( Linking )
1 、 GCC 工具链介绍
GCC ( GNU Compiler Collection , GNU 编译器套件),是由 GNU 开发的编程语言编译器。它是以 GPL 许可证所发行的自由软件,也是 GNU 计划的关键部分。 GCC 原本作为 GNU 操作系统的官方编译器,现已被大多数类 Unix 操作系统(如 Linux 、 BSD 、 Mac OS X 等)采纳为标准的编译器, GCC 同样适用于微软的 Windows 。 GCC 是自由软件过程发展中的著名例子,由自由软件基金会以 GPL 协议发布。
C 运行库
C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。 C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。
C 语言标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此, C 语言编译器通常需要一个 C 运行时库( C Run Time Libray , CRT )的支持。 C 运行时库又常简称为 C 运行库。与 C 语言类似, C++ 也定义了自己的标准,同时提供相关支持库,称为 C++ 运行时库。
2 、举个例子
由于 GCC 工具链主要是在 Linux 环境中进行使用,因此本文也将以 Linux 系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个 C 语言编写的简单 Hello 程序作为示例,其源代码如下所示:
#include
#include
#define WEIQIFA 0;
int main(void)
{
int i = WEIQIFA;
i = i++;
i++;
printf("%d\n",i);
return 0;
}
3 、编译过程
3.1 预处理
预处理的过程主要包括以下过程:
1 、将所有的 #define 删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如 #if #ifdef #elif #else #endif 等。
2 、处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。
3 、删除所有注释 “//” 和 “/* */” 。
4 、添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
5 、保留所有的 #pragma 编译器指令,后续编译过程需要使用它们。
使用 gcc 进行预处理的命令如下:
$gcc -E test001.c -o test001.i // 将源文件 test001.c 文件预处理生成 test001.i
// GCC 的选项 -E 使 GCC 在进行完预处理后即停止
test001.i 文件可以作为普通文本文件打开进行查看,其代码片段如下所示:
extern char *__stpncpy (char *__restrict __dest,
const char *__restrict __src, size_t __n)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2)));
extern char *stpncpy (char *__restrict __dest,
const char *__restrict __src, size_t __n)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2)));
# 658 "/usr/include/string.h" 3 4
# 3 "test001.c" 2
# 6 "test001.c"
int main(void)
{
int i = 0;;
i = i++;
i++;
printf("%d\n",i);
return 0;
}
3.2 编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
使用 gcc 进行编译的命令如下:
$ gcc -S test001.i -o test001.s // 将预处理生成的 test001.i 文件编译生成汇编程序 test001.s
// GCC 的选项 -S 使 GCC 在执行完编译后停止,生成汇编程序
上述命令生成的汇编程序 test001.s 的代码片段如下所示,其全部为汇编代码。
weiqifa@ubuntu:~/c/bianyiyuanli$ cat test001.s
.file "test001.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $0, -4(%rbp)
movl -4(%rbp), %eax
leal 1(%rax), %edx
movl %edx, -4(%rbp)
movl %eax, -4(%rbp)
addl $1, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
weiqifa@ubuntu:~/c/bianyiyuanli$
3.3 汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为 .o 的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成 .o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。
使用 gcc 进行汇编的命令如下:
$ gcc -c test001.s -o test001.o // 将编译生成的 test001.s 文件汇编生成目标文件 test001.o
// GCC 的选项 -c 使 GCC 在执行完汇编后停止,生成目标文件
// 或者直接调用 as 进行汇编
$ as -c test001.s -o test001.o // 使用 as 将 hello.s 文件汇编生成目标文件
注意: test001.o 目标文件为 ELF ( Executable and Linkable Format )格式的可重定向文件。
3.4 链接
链接也分为静态链接和动态链接,其要点如下:
1 、静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
2 、动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
2.1 、在 Linux 系统中, gcc 编译链接时的动态库搜索路径的顺序通常为:首先从 gcc 命令的参数 -L 指定的路径寻找;再从环境变量 LIBRARY_PATH 指定的路径寻址;再从默认路径 /lib 、 /usr/lib 、 /usr/local/lib 寻找。
2.2 、在 Linux 系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量 LD_LIBRARY_PATH 指定的路径寻址;再从配置文件 /etc/ld.so.conf 中指定的动态库搜索路径;再从默认路径 /lib 、 /usr/lib 寻找。
3 、在 Linux 系统中,可以用 ldd 命令查看一个可执行程序依赖的共享库。
由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如 libtest.a 和 libtest.so , gcc 链接时默认优先选择动态库,会链接 libtest.so ,如果要让 gcc 选择链接 libtest.a 则可以指定 gcc 选项 -static ,该选项会强制使用静态库进行链接。以 Hello World 为例: 4 、如果使用命令 “gcc hello.c -o hello” 则会使用动态库进行链接,生成的 ELF 可执行文件的大小(使用 size 命令查看)和链接的动态库(使用 ldd 命令查看)如下所示:
weiqifa@ubuntu:~/c/bianyiyuanli$ gcc test001.c -o test001
weiqifa@ubuntu:~/c/bianyiyuanli$ size test001
text data bss dec hex filename
1208 552 8 1768 6e8 test001
weiqifa@ubuntu:~/c/bianyiyuanli$ ldd test001// 可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc 动态库
linux-vdso.so.1 => (0x00007ffe90dbd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f930be92000)
/lib64/ld-linux-x86-64.so.2 (0x00007f930c25c000)
weiqifa@ubuntu:~/c/bianyiyuanli$
5 、如果使用命令 “gcc -static hello.c -o hello” 则会使用静态库进行链接,生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库(使用 Binutils 的 ldd 命令查看)如下所示:
$ gcc -static test001.c -o test001
$ size test001 // 使用 size 查看大小
text data bss dec hex filename
823382 7284 6360 837026 cc5a2 test001 // 可以看出 text 的代码尺寸变得极大
$ ldd test001
not a dynamic executable // 说明没有链接动态库
链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常被链接为不同的段,常见的段譬如 .text 、 .data 、 .rodata 、 .bss 等段。
4 、 ELF 文件分析
4.1 ELF 文件的段
ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都是段( Section )。一个典型的 ELF 文件包含下面几个段:
text :已编译程序的指令代码段。
rodata : ro 代表 read only ,即只读数据(譬如常数 const )。
data :已初始化的 C 程序全局变量和静态局部变量。
bss :未初始化的 C 程序全局变量和静态局部变量。
debug :调试符号表,调试器用此段的信息帮助调试。
如果对 ELF 文件特别感兴趣的,可以看看这个网站讲的非常详细非常好
可以使用 readelf -S 查看其各个 section 的信息如下:
weiqifa@ubuntu:~/c/bianyiyuanli$ readelf -S test001
There are 33 section headers, starting at offset 0xde500:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 00000000004001d8 000001d8
00000000000000f0 0000000000000018 AI 0 24 8
[ 4] .init PROGBITS 00000000004002c8 000002c8
000000000000001a 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 00000000004002f0 000002f0
00000000000000a0 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS 0000000000400390 00000390
000000000009e5b4 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000049e950 0009e950
0000000000002529 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 00000000004a0e80 000a0e80
00000000000000de 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 00000000004a0f60 000a0f60
0000000000000009 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 00000000004a0f80 000a0f80
000000000001d244 0000000000000000 A 0 0 32
[11] __libc_subfreeres PROGBITS 00000000004be1c8 000be1c8
0000000000000050 0000000000000000 A 0 0 8
[12] __libc_atexit PROGBITS 00000000004be218 000be218
0000000000000008 0000000000000000 A 0 0 8
[13] .stapsdt.base PROGBITS 00000000004be220 000be220
0000000000000001 0000000000000000 A 0 0 1
[14] __libc_thread_sub PROGBITS 00000000004be228 000be228
0000000000000008 0000000000000000 A 0 0 8
[15] .eh_frame PROGBITS 00000000004be230 000be230
000000000000af5c 0000000000000000 A 0 0 8
[16] .gcc_except_table PROGBITS 00000000004c918c 000c918c
00000000000000a3 0000000000000000 A 0 0 1
[17] .tdata PROGBITS 00000000006c9eb8 000c9eb8
0000000000000020 0000000000000000 WAT 0 0 8
[18] .tbss NOBITS 00000000006c9ed8 000c9ed8
0000000000000030 0000000000000000 WAT 0 0 8
[19] .init_array INIT_ARRAY 00000000006c9ed8 000c9ed8
0000000000000010 0000000000000000 WA 0 0 8
[20] .fini_array FINI_ARRAY 00000000006c9ee8 000c9ee8
0000000000000010 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 00000000006c9ef8 000c9ef8
0000000000000008 0000000000000000 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000006c9f00 000c9f00
00000000000000e4 0000000000000000 WA 0 0 32
[23] .got PROGBITS 00000000006c9fe8 000c9fe8
0000000000000010 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 00000000006ca000 000ca000
0000000000000068 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006ca080 000ca080
0000000000001ad0 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000006cbb60 000cbb50
0000000000001878 0000000000000000 WA 0 0 32
[27] __libc_freeres_pt NOBITS 00000000006cd3d8 000cbb50
0000000000000030 0000000000000000 WA 0 0 8
[28] .comment PROGBITS 0000000000000000 000cbb50
0000000000000035 0000000000000001 MS 0 0 1
[29] .note.stapsdt NOTE 0000000000000000 000cbb88
0000000000000f18 0000000000000000 0 0 4
[30] .shstrtab STRTAB 0000000000000000 000de392
0000000000000169 0000000000000000 0 0 1
[31] .symtab SYMTAB 0000000000000000 000ccaa0
000000000000b0a0 0000000000000018 32 710 8
[32] .strtab STRTAB 0000000000000000 000d7b40
0000000000006852 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
weiqifa@ubuntu:~/c/bianyiyuanli$
4.2 反汇编 ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。
使用 objdump -D 对其进行反汇编如下:
$ objdump -D test001
eeb: 00 6c 69 62 add %ch,0x62(%rcx,%rbp,2)
eef: 63 00 movslq (%rax),%eax
ef1: 6c insb (%dx),%es:(%rdi)
ef2: 6f outsl %ds:(%rsi),(%dx)
ef3: 6e outsb %ds:(%rsi),(%dx)
ef4: 67 6a 6d addr32 pushq $0x6d
ef7: 70 5f jo f58 <data.8386+0xf10>
ef9: 74 61 je f5c <data.8386+0xf14>
efb: 72 67 jb f64 <data.8386+0xf1c>
efd: 65 74 00 gs je f00 <data.8386+0xeb8>
f00: 38 40 25 cmp %al,0x25(%rax)
f03: 72 64 jb f69 <data.8386+0xf21>
f05: 69 20 2d 34 40 25 imul $0x2540342d,(%rax),%esp
f0b: 65 61 gs (bad)
f0d: 78 20 js f2f <data.8386+0xee7>
f0f: 38 40 25 cmp %al,0x25(%rax)
f12: 72 64 jb f78 <data.8386+0xf30>
f14: 78 00 js f16 <data.8386+0xece>
使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
weiqifa@ubuntu:~/c/bianyiyuanli$ gcc -o test001 -g test001.c
weiqifa@ubuntu:~/c/bianyiyuanli$ objdump -S test001
test001: file format elf64-x86-64
0000000000400526
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31561984/viewspace-2221194/ ,如需转载,请注明出处,否则将追究法律责任。