文章

GCC编译运行C、C++代码及四个过程

GCC编译运行C、C++代码及四个过程

GCC

前言

或者说,编译运行任何语言代码都需要相应的运行环境,C也一样,假设你自己设计一门语言,定义一个操作符“左移”,很显然,需要特定的环境才能运行它。

一个简单的方法是安装一个IDE,现在IDE中都集合了很多运行环境,很方便。

但必要的是,我不想安装体积较大的IDE,只需要一个单纯的文本,然后命令行运行就行了,这也是可以的。

GNU编译器套装(英语:GNU Compiler Collection,缩写为GCC)是GNU计划制作的一种优化编译器,支持各种编程语言、操作系统、计算机系统结构。

原名为GNU C语言编译器(GNU C Compiler),因为它原本只能处理C语言。同年12月,新的GCC编译器可以编译C++语言。后来又为Fortran、Pascal、Objective-C、Java、Ada,Go等其他语言开发了前端。C和C++编译器也支持OpenMP和OpenACC规范。

GCC不仅是GNU操作系统的官方编译器,还是许多类UNIX系统和Linux发行版的标准编译器。

截至2022年9月,GCC 12.2版内含C(gcc)、C++(g++)、Objective-C、Fortran(gfortran)、Ada(GNAT)、Go (gccgo)以及D (gdc,从9.1版开始)编程语言的前端。OpenMP和OpenACC并行语言拓展从GCC 5.1开始支持。GCC 7之前的版本也支持Java(gcj),允许将java编译为机器语言。

有关C++和C的语言版本支持,从GCC 11.1开始默认为gnu++17,C++17的超集;以及gnu11,C11的超集,还提供严格的标准支持。GCC也对C++20和即将到来的C++23标准提供实验性部分支持。

所以在Linux系统上,输出gcc -v即可查看版本。因此,不同Linux上对C或C++支持的标准可能不同,gcc官网gcc的文档中有详细的各版本说明(例如4.8.5支持C90C++98标准)。这和你选择不同的Java版本运行Java项目是一样的。

GCC(g++)的基本用法

编译C程序:

1
gcc -o output_file input_file.c

这条命令将会编译名为input_file.c的C源代码文件,并生成一个名为output_file的可执行文件。

运行编译后的可执行文件:

1
./output_file

这条命令将会执行编译生成的可执行文件。

对于C++程序的编译和运行,可以按照以下步骤进行:

编译C++程序:

1
g++ -o output_file input_file.cpp

这条命令将会编译名为input_file.cpp的C++源代码文件,并生成一个名为output_file的可执行文件。

运行编译后的可执行文件:

1
./output_file

这条命令将会执行编译生成的C++可执行文件。

gcc包含很多命令参数,man gcc可以查看手册获取更多信息。

其次gcc也是可以编译C++代码的,只是链接过程不能自动链接C++的库,因此可以有两种方式(设有一个汇编后的test.o):

1
2
3
4
5
gcc test.cpp -o test2 -lstdc++
// or
gcc test.o -o test -lstdc++
// or
g++ test.o -o test

第一、二个都是一个意思,即使用-l参数加上C++库,第二个是链接过程用g++命令进行。

一些参数

Argument Description Example
-Wall Enables all compiler’s warning messages. gcc -Wall hello.c -o hello
-Werror Turns warnings into errors. gcc -Werror hello.c -o hello
-o Specifies the output file. gcc hello.c -o hello
-g Generates debug information for use with GDB. gcc -g hello.c -o hello
-O Enables optimization. gcc -O hello.c -o hello
-std Specifies the version of the C language standard. gcc -std=c99 hello.c -o hello
-c Compiles or assembles the source files, but does not link. gcc -c hello.c
-I Adds include directory of header files. gcc -I /path/to/include hello.c -o hello
-L Adds directory of library files. gcc -L /path/to/library hello.c -o hello
-l Links with the library. gcc hello.c -lmylib -o hello
-D Defines a preprocessor macro. gcc -DDEBUG hello.c -o hello

gcc对编译文件的默认后缀名

Options Controlling the Kind of Output

编译最多可以包括四个阶段:预处理、编译本身、汇编和链接,始终按此顺序进行。GCC能够预处理多个文件并将其编译为多个文件 汇编程序输入文件、或汇编成一个汇编器输入文件;然后,每个汇编程序输入文件都会生成一个对象文件,链接会组合所有的对象文件(新编译的和指定为输入的) 转换为可执行文件。

对于任何给定的输入文件,文件名后缀决定了要进行的编译类型,man gcc命令有文件类型说明:
文件.c
必须进行预处理的C源代码。

文件.i
不应进行预处理的C源代码。

文件.ii
不应进行预处理的C++源代码。

文件.m
Objective-C源代码。请注意,必须链接到libobjc库才能使Objective-C程序正常工作。

文件.mi
不应进行预处理的Objective-C源代码。

文件.mm
文件M
Objective-C++源代码。请注意,必须链接到libobjc库才能使Objective-C++程序正常工作。请注意。M指的是字面上的大写字母M。

文件.mii
不应进行预处理的Objective-C++源代码。

文件.h
C、 将C++、Objective-C或Objective-C++头文件转换为预编译头文件(默认),或将C、C++头文件转换成Ada规范(通过-fdump-Ada规范开关)。

文件.cc
文件.cp
文件.cxx
文件.cpp
文件CPP
文件.C++
文件C
必须进行预处理的C++源代码。请注意,在.cxx中,最后两个字母必须都是x。同样。C指的是字面上的大写字母C。

文件.mm
文件M
必须进行预处理的Objective-C++源代码。

文件.s Assembler code.(汇编程序代码)

gcc一些注意事项

man gcc中对一些情况已经进行了说明,如参数的顺序: You can mix options and other arguments. For the most part, the order you use doesn’t matter. Order does matter when you use several options of the same kind; for example, if you specify -L more than once, the directories are searched in the order specified.

多字母参数能否连用: Many options have multi-letter names; therefore multiple single-letter options may not be grouped: -dv is very different from -d -v.

预处理、编译、汇编和链接

总的来说,将代码生成可执行文件可以分为四个步骤: preprocessing, compilation proper, assembly and linking

对应的文件改变是从源码file.c->预处理(-E)->file.i->编译(-S)->file.s->汇编(-c)->file.o->链接->file

preprocessing

预处理步骤包括将#include的文件内容添加至test.c中,替换define宏等。 -E选项可以直接输出预处理后的代码到终端gcc -E test.c

1
2
3
4
5
//执行后,这里是stdio.h文件中的一堆内容,用cat命令cat stdio.h文件可以发现一致
int main() {
 printf("Hello World!");
 return 0;
}

值得注意的是:
这里并不是把所有的内容都加入进来,例如main函数中使用到了printf这个函数,而在stdio.h中,printf也仅仅只是一个引用而已,并不是完整的代码。

compilation proper

-S选项用于编译至汇编,例如gcc -S test.c -o test.s,此时查看test.s

1
//其中内容便是汇编代码

assembly

汇编代码还是需要被转换成处理器可执行代码的,这一步是转换汇编代码 -c选项用于转换汇编代码,gcc -c test.s -o test.o

事实上,man gcc手册中对-c的解释是:Compile or assemble the source files, but do not link.

运行上面的命令将创建一个名为hello_world.o的文件,其中包含程序的目标代码。此文件的内容为二进制格式,可以通过运行以下任一命令使用hexdump或od进行检查:

1
2
hexdump test.o
od -c test.o

hexdump:十六进制数据查看器

值得注意的是:
这里这个test.o文件也并不是真是可执行的代码,若你输入./test.o计算机也不会让你执行它,file命令可以实际查看这个文件格式、文件属性等,例如:

1
2
[root@test]# file test.o
test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

其中ELF是Executable and Linkable Format,注意输出信息中的relocatable,这个文件中的机器码也不是完整的,例如前面提到的printf函数,在这段代码中,其地址是00代替,printf实际的地址需要在链接阶段被动态链接或静态链接添加进来。
通常包括:

  • 文件头(File Header):包含了文件的基本信息,如文件类型、目标体系结构、入口地址等。
  • 程序头表(Program Header Table):描述了可执行文件的段(Segment)信息,包括代码段、数据段等,用于程序加载时确定各个段在内存中的位置和属性。
  • 节头表(Section Header Table):描述了目标文件的节(Section)信息,包括代码段、数据段、符号表等,用于链接器在链接时确定各个节的位置和属性。
  • 段(Segment):包含了可执行文件的代码段、数据段等信息,用于程序加载到内存中执行。
  • 节(Section):包含了目标文件的代码节、数据节、符号表等信息,用于链接器在链接时进行符号解析和重定位。

linking

在汇编阶段生成的目标代码由处理器理解的机器指令组成,但前面提到,并不是所有的信息都是完整的。因此为了生成一个可执行程序,必须重新排列、替换、填补一些内容。这个过程被称为链接。
链接器将排列对象代码片段,以便某些片段中的函数可以成功调用其他片段中的功能。它还将添加包含程序使用的库函数指令的部分。在“Hello,World!”程序的情况下,链接器将添加put函数的对象代码。

1
cc -o test test.c

最终终端输入./test便可直接运行。此时用file命令查看文件:

1
2
[root@test]# file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, 

值得注意的是:
这个步骤是为了填补汇编后的代码内容,因此可以想到如将缺失机器码全部填充进去,如实现printf的机器码,但这会造成一个问题,代码很多地方,很多文件都用到printf,如果生成的可执行文件都把完整的printf机器码填充进去,那可执行文件会很大,因此链接过程分动态链接和静态链接,动态链接可替换地址会被替换成一个符号表中的符号,而不是实际的函数地址,在运行时,操作系统的动态链接器会根据符号表中的符号解析出对应的函数地址,然后将程序中的地址引用指向实际的函数地址。

gcc、链接、静态链接、动态链接

首先,需要知道几个参数:

1
2
3
4
5
6
-L 添加库文件搜索路径
-l 链接指定的库文件
-I 添加头文件搜索路径
-shared 生成动态链接库
-static 生成静态可执行文件
-O 优化级别

此外还有linux的ar命令(此命令由GNU实现),此命令打包一些文件成一个集合(静态库),一般以.a结尾。

1
ar [选项] <归档文件> [文件...]

常用选项:-r将文件插入库中,-s,生成符号表,-c,创建库(The specified archive is always created if it did not exist, when you request an update. But a warning is issued unless you specify in advance that you expect to create it, by using this modifier. ),-t,显示一个表格列出库成员。

readelf命令查看共享库(elf)文件内容:

1
2
3
4
5
6
7
8
9
 # readelf --help
 Display information about the contents of ELF format files
 Options are:
  -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
  -h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
     --segments          An alias for --program-headers
  -S --section-headers   Display the sections' header
     --sections          An alias for --section-headers

linking 静态链接 示例

生成一个汇编后的文件:

gcc -c hello.c -o hello.o

hello.o插入静态库libhello.a

ar -crv libhello.a test.o

hello.o中有一个print_hello函数可以打印hello,此时用:

gcc test.c -o test -L. -lhello

命令,可以生成可执行的test文件,这个test文件和下面命令:

gcc -o test test.c hello.c

生成的是一样的,执行结果一样。

这里值得注意的点是:-l选项跟着hello,这里省略了lib(.*).a,只要了中间即可,其次.a是静态库后缀,-L跟着.,这意味着让编译器在.目录中搜索hello

值得注意的是:

我的环境是gcc4.8.5, 运行-static 参数报错

1
2
/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status

这个报错实际上是缺失libc.a,因为新版本Linux(至少我的是4.8.5)只有libc.so(动态链接库),需要安装glibc-static
此时运行gcc -static -o test test.c -L. -lhello不会报错,并且以静态库方式链接生成可执行文件:
此时查看ls -l加static和不加static生成的文件大小:

1
2
3
-rwxr-xr-x 1 root root 861352 Mar 29 11:03 test
==============
-rwxr-xr-x 1 root root 8416 Mar 29 11:08 test

一个是8KB,一个是860KB,这也是前面说的静态链接会替换一份可执行代码,所以文件内容大。

值得注意的是:

运行gcc -static -o test2 test.c会报错:

1
2
3
/tmp/ccneqwtQ.o: In function `main':
test.c:(.text+0x14): undefined reference to `hello'
collect2: error: ld returned 1 exit status

这因为没有链接libhello.amain函数中的hello()未定义,如果我们把链接库顺序换一下,也会出现这个问题:
运行gcc -static -L. -lhello -o test3 test.c,有:

1
2
3
/tmp/ccJTGdHQ.o: In function `main':
test.c:(.text+0x14): undefined reference to `hello'
collect2: error: ld returned 1 exit status

linking 动态链接 示例

使用命令gcc -fPIC -shared -o libhello.so hello.chello.c编译成动态链接库

然后使用gcc -o test4 test.c ./libhello.so生成test4可执行文件。

执行file libhello.so,查看文件类型可知:

1
libhello.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), 

这里-fPIC表示生成地址无关代码,-shared即共享文件(动态链接库),.so一般是动态库的后缀。

用ls -l 可以发现:

1
2
3
-rwxr-xr-x 1 root root   8416 Mar 29 11:08 test
-rwxr-xr-x 1 root root 861352 Mar 29 13:44 test2
-rwxr-xr-x 1 root root   8392 Mar 29 13:53 test4

其中test-o test test.c hello.c生成,test2静态生成文件大小最大,test4动态生成文件大小最小。

这里的地址无关代码是什么意思呢?

地址无关代码

共享库的代码是需要被多个程序共同使用的,例如hello()函数的地址被多个程序使用,但这个地址不方便固定,固定这个函数的地址不方便计算机的管理。

接着共享的也不仅仅是一个函数,所以地址无关代码针对函数、数据等有不同的解决方式:

1):共享库内部的函数调用:

内部的那好办了,例如一个班级不管在哪个房间,只要座位顺序一样,那每个人相对位置都一样,内部函数调用也同理,采用相对地址调用形式,基于调用处和被调函数的地址偏移量进行访问即可。

2):共享库内部的数据访问:

同理函数。

3):共享库对其它模块的数据访问:

共享库的地址需要被运行时才确定,所以共享库调用其他模块的数据也无法确定地址,怎么办呢?没关系,抽象出一个中间层,中间层存放实际地址,共享库中的地址是中间层的地址(即不关心实际地址是什么),只要运行时把共享库中的中间层相对地址替换成实际地址就行了,这个中间层由编译器生成,称为全局偏移量表GOT(global offset table)。

4):共享库对其它模块的函数调用:

同理数据。

共享库在计算机中的位置:

  • /lib 系统运行时需要的关键 基础共享库
  • /user/lib 系统运行时非关键性共享库
  • /user/local/lib 依赖的一些非系统性的第三方应用程序库等
本文由作者按照 CC BY 4.0 进行授权

热门标签