文章

gcc&g++的使用

gcc&g++的使用

GCC(GNU Compiler Collection) ,即 GNU 编译器套件,是 Linux 系统下最常用的 C/C++ 编译器,大部分 Linux 发行版中都会默认安装。GCC 编译器通常以gcc命令的形式在终端(Shell)中使用,它有很多选项可以选择使用。

组件部分及软件工具

GCC由很多部分组成,但它们也并不总是出现的。有些部分是和语言相关的,所以如果没有安装某种特定语言,系统中就不会出现相关的文件。

部分描述
c++gcc 的一个版木,默认语言设置为 C++,而且在连接的时候自动包含标准 C++ 库。这和 g++ 一样
ccl实际的C编译程序
cclplus实际的 C++ 编译程序
collect2在不使用 GNU 连接程序的系统上,有必要运行 collect2 来产生特定的全局初始化代码
(例如 C++ 的构造函数和析构函数)
configureGCC 源代码树根目录中的一个脚木。用于设置配置值和创建 GCC 编译程序
必需的 make 程序的描述文件
crt0.o这个初始化和结束代码是为每个系统定制的,而且也被编译进该文件,
该文件然后会被连接到每个可执行文件中来执行必要的启动和终止程序
cygwin1.dllWindows 的共享库提供的 API,模拟 UNIX 系统调用
f77该驱动程序可用于编译 Fortran
f771实际的 Fortran 编译程序
g++gcc 的一个版木,默认语言设置为 C++,而且在连接的时候自动包含标准 C++ 库。这和 c++ 一样
gcc该驱动程序等同于执行编译程序和连接程序以产生需要的输出
gcj该驱动程序用于编译 Java
gnat1实际的 Ada 编译程序
gnatbind一种工具,用于执行 Ada 语言绑定
gnatlink一种工具,用于执行 Ada 语言连接
jc1实际的 Java 编译程序
libgcc该库包含的例程被作为编译程序的一部分,是因为它们可被连接到实际的可执行程序中。
它们是特殊的例程,连接到可执行程序,来执行基本的任务,例如浮点运算。
这些库中的例程通常都是平台相关的
libgcj运行时库包含所有的核心 Java 类
libobjc对所有 Objective-C 程序都必须的运行时库
libstdc++运行时库,包括定义为标准语言一部分的所有的 C++ 类和函数

下面列出的软件和 GCC 协同工作,目的是实现编译过程。有些是很基本的(例如 as 和 Id),而其他一些则是非常有用但不是严格需要的。尽管这些工具中的很多都是各种 UNIX 系统的本地工具,但还是能够通过 GNU 包 binutils 得到大多数工具。

工具描述
addr2line给出一个可执行文件的内部地址,addr2line 使用文件中的调试信息将地址
翻译成源代码文件名和行号。该程序是 binutils 包的一部分
ar这是一个程序,可通过从文档中增加、删除和析取文件来维护库文件。
通常使用该工具是为了创建和管理连接程序使用的目标库文档。该程序是 binutils 包的一部分
asGNU 汇编器。实际上它是一个汇编器,因为它可以被编译或能够在各种不同平台上工作。
该程序是 binutils 包的一部分
autoconf产生的 shell 脚本自动配置源代码包去编译某个特定版木的 UNIX
c++filt程序接受被 C++ 编译程序转换过的名字(不是被重载的),而且将该名字翻译成初始形式。
该程序是 binutils 包的一部分
f2c是 Fortran 到C的翻译程序。不是 GCC 的一部分
gcovgprof 使用的配置工具,用来确定程序运行的时候哪一部分耗时最大
gdbGNU 调试器,可用于检查程序运行时的值和行为
GNATSGNU 的调试跟踪系统(GNU Bug Tracking System)。一个跟踪 GCC 和其他 GNU 软件问题的在线系统
gprof该程序会监督编译程序的执行过程,并报告程序中各个函数的运行时间,
可以根据所提供的配置文件来优化程序。该程序是 binutils 包的一部分
ldGNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部分
libtool一个基本库,支持 make 程序的描述文件使用的简化共享库用法的脚本
make一个工具程序,它会读 makefile 脚本来确定程序中的哪个部分需要编译和链接,
然后发布必要的命令。它读出的脚本(叫做 makefile 或 Makefile)定义了文件关系和依赖关系
nlmconv将可重定位的目标文件转换成 NetWare 可加载模块(NetWare Loadable Module, NLM)。
该程序是 binutils 的一部分
nm列出目标文件中定义的符号。该程序是 binutils 包的一部分
objcopy将目标文件从一种二进制格式复制和翻译到另外一种。该程序是 binutils 包的一部分
objdump显示一个或多个目标文件中保存的多种不同信息。该程序是 binutils 包的一部分
ranlib创建和添加到 ar 文档的索引。该索引被 Id 使用来定位库中的模块。该程序是 binutils 包的一部分
ratforRatfor 预处理程序可由 GCC 激活,但不是标准 GCC 发布版的一部分
readelf从 ELF 格式的目标文件显示信息。该程序是 binutils 包的一部分
size列出目标文件中每个部分的名字和尺寸。该程序是 binutils 包的一部分
strings浏览所有类型的文件,析取出用于显示的字符串。该程序是 binutils 包的一部分
strip从目标文件或文档库中去掉符号表,以及其他调试所需的信息。该程序是 binutils 包的一部
vcgRatfor 浏览器从文本文件中读取信息,并以图表形式显示它们。vcg 工具并不是GCC发布中的一部分,
但 -dv 选项可被用来产生 vcg 可以理解的优化数据的格式
windresWindow 资源文件编泽程序。该程序是 binutils 包的一部分

gcc和g++的区别

只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。

可以这样理解,gccGCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别,比如:

  • xxx.c:默认以编译 C 语言程序的方式编译此文件;
  • xxx.cpp:默认以编译 C++ 程序的方式编译此文件。
  • xxx.m:默认以编译 Objective-C 程序的方式编译此文件;
  • xxx.go:默认以编译 Go 语言程序的方式编译此文件;

但是如果使用g++指令,无论文件的后缀名是什么,都会当成C++程序进行编译。

除此之外对于编译执行 C++ 程序,使用 gcc 和 g++ 也是有区别的。很多 C++ 程序都会调用某些标准库中现有的函数或者类对象,而单纯的 gcc 命令是无法自动链接这些标准库文件的。这时候就要手动为其添加 -lstdc++ -shared-libgcc 选项,表示 gcc 在编译 C++ 程序时可以链接必要的 C++ 标准库。

指定编译标准

对于编译 C、C++ 程序来说,借助 -std 选项即可手动控制 GCC 编译程序时所使用的编译标准。也就是说,当使用 gcc 指令编译 C 语言程序时,我们可以借助 -std 选项指定要使用的编译标准;同样,当使用 g++ 指令编译 C++ 程序时,也可以借助 -std 选项指定要使用的编译标准。

-std 选项的使用方式很简单,其基本格式如下:

gcc/g++ -std=编译标准

例如:

1
2
gcc -std=c99 main.c
g++ -std=c++17 main.cpp

编译指令

精简版

1
2
3
4
5
6
7
8
gcc main.c
gcc -xc++ -lstdc++ -shared-libgcc main.cpp
g++ main.cpp

# 指定生成名称
gcc main.c -o main
gcc -xc++ -lstdc++ -shared-libgcc main.cpp -o main
g++ main.cpp -o main

这时候编译器会“一步编译”指定的 C(C++)程序,底层会依次执行预处理、编译、汇编、链接的过程,将程序编译成可执行程序。而编译过程中的中间文件默认是不会生成,如果想保留中间文件,可以在编译时添加-save-temps编译选项。

另外,编译命令还有以下常见选项:

gcc/g++指令选项功 能
-E(大写)预处理指定的源文件,不进行编译。
-S(大写)编译指定的源文件,但是不进行汇编。
-c编译、汇编指定的源文件,但是不进行链接。
-o指定生成文件的文件名。
-llibrary(-I library)其中 library 表示要搜索的库文件的名称。该选项用于手动指定链接环节中程序
可以调用的库文件。建议 -l 和库文件名之间不使用空格,比如 -lstdc++。
-ansi对于 C 语言程序来说,其等价于 -std=c90;对于 C++ 程序来说,其等价于 -std=c++98。
-std=手动指令编程语言所遵循的标准,例如 c89、c90、c++98、c++11 等。
-O[num]使用编译优化级别1编译程序。级别为1~3,如O1,O2,O3
级别越大优化效果越好,但编译时间越长。
-v打印gcc编译时的详细步骤信息
-L共享库的路径添加到搜索的范围
-I将头文件的路径添加到搜索的范围
-shared生成共享库
-static生成静态库
-Wl,-Bstatic-Bstatic选项用于对指定的库静态连接
-Wl,-BdynamicBdynamic搜索共享库(默认)
-Wa,option此选项传递option给汇编程序;
如果option中间有逗号,就将option分成多个选项,然后传递给会汇编程序
-Wl,option此选项传递option给连接程序;
如果option中间有逗号,就将option分成多个选项,然后传递给会连接程序

预处理(-E)

控制 GCC 编译器仅对源代码做预处理操作。

所谓预处理操作,主要是处理那些源文件和头文件中以 # 开头的命令(比如 #include#define#ifdef 等),并删除程序中所有的注释 ///* ... */。简单来说就是把程序展开成可单独一个文件执行的代码。

预处理阶段实际的工作:

  • 将头文件中的内容(源文件之外的文件)插入到源文件中
  • 进行了宏替换的过程,定义和替换了由#define指令定义的符号
  • 删除掉注释的过程,注释是不会带入到编译阶段
  • 条件编译

默认情况下 gcc -E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指定的文件中。比如:

1
2
3
4
gcc -E main.cpp -o main.i

# 可以添加一个 -C 指令,可以防止编译器在预处理时删除注释
gcc -E -C main.cpp -o main.i

另外预处理阶段还支持以下选项

选 项功 能
-D name[=definition]在处理源文件之前,先定义宏 name。宏 name 必须是在源文件和头文件中
都没有被定义过的。将该选项搭配源代码中的#ifdef name命令使用,
可以实现条件式编译
如果没有指定一个替换的值(即省略 =definition),该宏被定义为值 1。
-U name如果在命令行或 GCC 默认设置中定义过宏 name,则“取消”name 的定义。
-D-U 选项会依据在命令行中出现的先后顺序进行处理。
-include file如同在源代码中添加 #include "file" 一样。
-iquote dir对于以引号(#include "")导入的头文件中,
-iquote 指令可以指定该头文件的搜索路径。
当 GCC 在源程序所在目录下找不到此头文件时,就会去 -iquote 指令指定的目录中查找。
-I dir同时适用于以引号 ""<> 导入的头文件。当 GCC 在 -iquote 指令指定的目录下
搜索头文件失败时,会再自动去 -I 指定的目录中查找。
该选项在 GCC 10.1 版本中已被弃用,并建议用 -iquote 选项代替。
-isystem dir -idirafter dir都用于指定搜索头文件的目录,适用于以引号 ""<> 导入的头文件。
-Wall发出gcc提供的所有有用的报警信息
-Werror将警告升级为编译报错

编译非汇编文件(-S)

将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。

1
2
3
4
5
# 默认生成main.s
gcc -S main.i

# 生成文件名为test.i
gcc -S main.i -o test.i

需要注意的是,gcc -S 指令操作的文件并非必须是经过预处理后得到的 .i 文件,-S 选项的功能是令 GCC 编译器将指定文件处理至编译阶段结束。这也就意味着,gcc -S 指令可以操作预处理后的 .i 文件,也可以操作源代码文件:

  • 如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;
  • 如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行预处理和编译这 2 步操作。

如果想阅读汇编代码,可以加上-fverbose-asm,编译器在编译时会自动添加必要注释

1
gcc -S main.cpp -fverbose-asm -o main.s

生成目标文件(汇编阶段)(-c)

汇编其实就是将汇编代码转换成二进制格式的目标文件(不可执行,需要链接第三方库)

大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。

1
2
3
4
5
# 默认生成main.o
gcc -c main.s

# 生成文件名为test.o
gcc -c main.s -o test.o

跟编译阶段类似,汇编命令并非只能用于加工 .s 文件。事实上,-c 选项只是令 GCC 编译器将指定文件加工至汇编阶段,但不执行链接操作。这也就意味着:

  • 如果指定文件为源程序文件(例如 demo.c),则 gcc -c 指令会对 demo.c 文件执行预处理、编译以及汇编这 3 步操作;
  • 如果指定文件为刚刚经过预处理后的文件(例如 demo.i),则 gcc -c 指令对 demo.i 文件执行编译和汇编这 2 步操作;
  • 如果指定文件为刚刚经过编译后的文件(例如 demo.s),则 gcc -c 指令只对 demo.s 文件执行汇编这 1 步操作。

gcc链接

得到生成目标文件之后,接下来就可以直接使用 gcc 指令继续执行链接操作

1
2
3
gcc main.i -o main
gcc main.s -o main
gcc main.o -o main

以上三种方法都能生成名为main的执行文件

手动添加链接库(-l)

链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。

在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。

而且,链接器也必须将程序中所用到的所有C标准库函数加入其中。对于链接器而言,链接库不过是一个具有许多目标文件的集合,它们在一个文件中以方便处理。

标准库的大部分函数通常放在文件 libc.a 中(文件名后缀.a代表“achieve”,译为“获取”),或者放在用于共享的动态链接文件 libc.so 中(文件名后缀.so代表“share object”,译为“共享对象”)。这些链接库一般位于 /lib/ 或 /usr/lib/,或者位于 GCC 默认搜索的其他目录。

当使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加。

例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>      /* printf */
#include <math.h>       /* cos */
#define PI 3.14159265
int main ()
{
    double param, result;
    param = 60.0;
    result = cos ( param * PI / 180.0 );
    printf ("The cosine of %f degrees is %f.\n", param, result );
    return 0;
}

如果编译时需要手动链接math库但是没有添加进来,编译会发生函数未定义错误。新版本的gcc应该默认链接了math库所以不需要手动添加

当编译器无法找到函数时,需要用-l选项链接

1
gcc main.c -o main.out -lm

数学库的文件名是 libm.a。前缀lib和后缀.a是标准的,m是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀,本例中,基本名称为 m

  • 链接其它目录中的库

通常,GCC 会自动在标准库目录中搜索文件,例如 /usr/lib,如果想链接其它目录中的库,就得特别指明。有三种方式可以链接在 GCC 搜索路径以外的链接库

  1. 把链接库作为一般的目标文件,为 GCC 指定该链接库的完整路径与文件名。

    例如,如果链接库名为 libm.a,并且位于 /usr/lib 目录,那么下面的命令会让 GCC 编译 main.c,然后将 libm.a 链接到 main.o

    1
    
     gcc main.c -o main.out /usr/lib/libm.a
    
  2. 使用-L选项,为 GCC 增加另一个搜索链接库的目录:

    1
    
     gcc main.c -o main.out -L/usr/lib -lm
    

    可以使用多个-L选项,或者在一个-L选项内使用冒号分割的路径列表。

  3. 把包括所需链接库的目录加到环境变量 LIBRARY_PATH 中,用来告诉它 去哪里查找链接库.a.so 文件) 的路径。

LIBRARY_PATH用于编译阶段寻找静态库或动态库,LD_LIBRARY_PATH用于目标程序执行时,寻找动态库的路径

GCC编译多文件项目

例如有以下两个源文件main.cfunc.c

1
2
3
4
5
#include <stdio.h>
int main(){
    display();
    return 0;
}
1
2
3
4
#include <stdio.h>
void display(){
    printf("Hello World");
}

可以使用如下方式进行编译

1
2
3
4
5
gcc -c main.c func.c
# main.o func.o

gcc main.o func.o -o main.exe
# main.exe

或者直接编译

1
2
gcc main.c func.c -o main.exe
# main.exe

当目录下只有两个c文件时,甚至可以使用通配符*

1
2
gcc *.c -o main.exe
# main.exe

静态链接库

静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。

创建静态库的时候,需要使用gcc/g++ -c先将源文件编译为目标文件*.o,然后用ar指令将*.o打包成静态库文件。目标程序与静态库链接时,目标程序代码调用的任何外部函数的代码都会从静态库中复制到最终的可执行文件中。

GCC在链接时优先使用动态库,只有当动态库不存在时才开始使用静态库,如果要强制使用静态库,编译时加上-static参数。使用-Wl,-Bstatic告诉链接器优先使用静态库。

使用静态库文件实现程序的链接操作,既有优势也有劣势:

  • 优势:生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强);
  • 劣势:如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。

和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。

在Linux系统下,静态链接库文件的后缀名通常用.a标识,在Windows下则通常为.lib

链接静态库时的搜索路径顺序

  1. ld会先去寻找gcc指令中的参数-L
  2. 环境变量LIBRARY_PATH
  3. /lib,/usr/lib,/usr/local/lib

动态链接库

动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。

显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。

创建动态库的时候,可以传-shared-fPIC参数,-fPIC参数用于编译阶段,用来生成位置无关的代码。使用gcc -shared -fPIC可以直接用xxx.c源文件生成动态库。

目标程序与动态库链接时,可执行文件仅包含它所需的一个小函数表,而不是来自库文件的完整机器代码。在可执行文件开始运行之前,动态库的代码被操作系统复制到内存中进行共享

使用-Wl,-Bdynamic告诉链接器优先使用动态库。

采用动态链接库实现程序的连接操作,其优势和劣势恰好和静态链接库相反:

  • 优势:由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
  • 劣势:此方式生成的可执行文件无法独立运行,必须借助相应的库文件(可移植性差)。

在Linux系统下,静态链接库文件的后缀名通常用.so标识,在Windows下则通常为.dll

链接静态库时的搜索路径顺序

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量LD_LIBRARY_PATH
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  4. /lib,/usr/lib

工具

  • ldd:列出依赖的动态库
1
ldd xxx
  • nm:查看静态/动态库中的函数
1
nm xx.so | grep main
本文由作者按照 CC BY 4.0 进行授权