GCC Basic

GCC Basic

GCC 简介

GCC 是 Linux 下的编译工具集,是 GNU Compiler Collection 的缩写,包含 gcc 、 g++ 等编译器。这个工具集不仅包含编译器,还包含其他工具集,例如 ar 、 nm 等。

GCC 工具集不仅能编译 C/C++ 语言,其他例如 Objective-C 、 Pascal 、 Fortran 、 Java 、 Ada 等语言均能进行编译。

GCC 在可以根据不同的硬件平台进行编译,即能进行交叉编译,在 A 平台上编译 B 平台的程序,支持常见的 X86 、 ARM 、 PowerPC 、 mips 等,以及 Linux 、 Windows 等软件平台。

GCC 的 C 编译器 是 gcc,其命令格式为 :

Usage: gcc [options] file...

GCC 下默认文扩展名的含义:

文件扩展名 GCC 所理解的含义
*.c 该类文件为 C 语言的源文件
*.h 该类文件为 C 语言的头文件
*.i 该类文件为预处理后的 C 文件
*.C 该类文件为 C++ 语言的源文件
*.cc 该类文件为 C++ 语言的源文件
*.cxx 该类文件为 C++ 语言的源文件
*.m 该类文件为 Objective-C 语言的源文件
*.s 该类文件为汇编语言的源文件
*.o 该类文件为汇编后的目标文件
*.a 该类文件为静态库
*.so 该类文件为共享库
a.out 该类文件为链接后的输出文件

GCC 下有很多编译器,可以支持 C 语言,C++语言等多种语言

GCC 编译器命令 含义
cc 指的是 C 语言编译器
cpp 指的是预处理编译器
gcc 指的是 C 语言编译器
g++ 指的是 C++语言编译器

编译程序的基本知识

GCC 编译器对程序的编译分为 4 个阶段:

  1. 预编译
  2. 编译和优化
  3. 汇编
  4. 链接

GCC 的编译器可以将这 4 个步骤合并成一个。

源文件指存放可编辑代码的文件,如存放 C/C++ 和汇编语言的文件。

目标文件是指经过编译器的编译生成的 CPU 可识别的二进制代码,但是目标文件一般不能执行,因为其中的一些函数过程没有相关的指示和说明。

可执行文件就是目标文件与相关的库链接后的文件,它是可以执行的。

预编译过程将程序中引用的头文件包含进源代码中,并对一些宏进行替换。

编译过程将用户可识别的语言翻译成一组处理器可识别的操作码,生成目标文件,通常翻译成汇编语言,而汇编语言通常和机器操作码之间是一种一对一的关系。 GNU 中有 C/C++ 编译器 GCC 和汇编器 as。

所有的目标文件必须用某种方式组合起来才能运行,这就是链接的作用。目标文件中通常仅解析了文件内部的变量和函数,对于引用的函数和变量还没有解析,这需要将其他已经编写好的目标文件引用进来,将没有解析的变量和函数进行解析,通常引用的目标是库。链接完成后会生成可执行文件。

单个文件编译成执行文件

下面是一个示例程序的源代码:

// hello.c
#include <stdio.h>

int main()
{
    printf("Hello World!\n");

    return 0;
}

将其保存在 hello.c 文件中,运行如下命令将代码直接编译成可执行文件:

$ gcc hello.c

上述命令没有指定生成执行文件的名称, GCC 将生成默认的文件名 a.out。 运行结果如下:

$ ./a.out
Hello World!

如果希望生成指定的可执行文件名,选项 -o 可以使编译程序生成指定文件名,例如将上述程序编译输出一个名称为 test 的执行程序:

$ gcc -o test hello.c

上述命令把 hello.c 源文件编译成可执行文件 test。 运行可执行文件 test,向终端输出 "Hello World!" 字符串。运行结果如下:

$ ./test
Hello World!

编译生成目标文件

GCC 的 -c 选项可以用于生成目标文件,这一选项将源文件生成目标文件,而不是生成可执行文件。默认情况下生成的目标文件的文件名和源文件的名称一样,只是扩展名为 .o。例如,下面的命令会生成一个名字为 Hello.o 的目标文件:

$ gcc -c hello.c

如果需要生成指定的文件名,可以使用 -o 选项。下面的命令将源文件 hello.c 编译成目标文件,文件名为 test.o

$ gcc -c -o test.o hello.c

可以用一条命令编译多个源文件,生成目标文件,这通常用于编写库文件或者一个项目中包含多个源文件。例如一个项目包含 filel.cfile2.cfile3.c, 下面的命令可以将源文件生成 3 个目标文件 : filel.ofile2.ofile3.o

$ gcc -c file1.c file2.c file3.c

多文件编译

GCC 可以自动链接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中。例如一个项目包含俩个文件,文件 string.c 中有一个函数 StrLen 用于计算字符串的长度,而在 main.c 中调用这个函数将计算结果显示出来。

源文件 string.c

// string.c
#define ENDSTRING '\0'
int StrLen(char *string)
{
    int len = 0;

    while (*string++ != ENDSTRING) {
        len++;
    }
    return len;
}

源文件 main.c

// main.c
#include <stdio.h>
extern int StrLen(char *str);
int main()
{
    char src[] = "Hello AimerNeige!\n";
    printf("String length is: %d\n", StrLen(src));

    return 0;
}

编译运行

下面的命令将俩个源文件中的程序编译成一个执行文件,文件名为 test

$ gcc -o test strung.c main.c

执行编译出来的可执行文件 test,程序的运行结果如下:

$ ./test
String length is: 18

当然也可以先将源文件编成目标文件,然后进行链接。例如,下面的过程将 string.cmain.c 源文件编译成目标文件 string.omain.o,然后将 string.omain.o 链接生成 test

$ gcc -c string.c main.c
$ gcc -o test string.o main.o

预处理

在 C 语言程序中,通常需要包含头文件并会定义一些宏。 预处理过程将源文件中的头文件包含进源文件中,并且将文件中定义的宏进行扩展。

编译程序时选项 -E 告诉编译器进行预编译操作。例如如下命令将文件 string.c 的预处理结果显示在计算机屏幕上 :

$ gcc -E string.c

如果需要指定源文件编译后生成的中间结果文件名,需要使用选项 -o。例如,下面的代码将文件 string.c 进行预编译,生成文件 string.i

$ gcc -o string.i -E string.c

编译成汇编语言

生成汇编语言的 GCC 选项 是 -S,默认情况下生成的文件名和源文件一致,扩展名为 .s。例如,下面的命令将 C 语言源文件 string.c 编译成汇编语言,文件名为 string.s

$ gcc -S string.c

生成和使用静态链接库

静态库是 obj 文件的一个集合,通常静态库以 .a 为后缀。静态库由程序 ar 生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。

静态库的优点是可以在不用重新编译程序库代码的情况下,进行程序的重新链接,这种方法节省了编译过程的时间(在编译大型程序的时候,需要花费很长时间)。但是由于现在系统的强大,编译的时间已经不是问题。静态库的另一个优势是开发者可以提供库文件给使用的人员,不用开放源代码,这是库函数提供者经常采用的手段。当然这也是程序模块化开发的一种手段,使每个软件开发人员的精力集中在自己的部分。在理论上,静态库的执行速度比共享库和动态库要快(1% ~ 5% )。

生成静态链接库

生成静态库,或者将一个 obj 文件加到已经存在的静态库的命令为 ar 库文件 obj文件1 obj文件2。创建静态库的最基本步骤是生成目标文件,这点前面已经介绍过。然后使用工具 ar 对目标文件进行归档。工具 ar-r 选项,可以创建库,并把目标文件插入到指定库中。例如,将 string.o 打包为库文件 libstr.a 的命令为:

$ ar -rcs libstr.a string.o

使用静态链接库

在编译程序的时候经常需要使用函数库,例如经常使用的 C 标准库等。GCC 链接时使用库函数和一般的 obj 文件的形式是一致的,例如对 main.c 进行链接的时候,需要使用之前已经编译好的静态链接库 libstr.a, 命令格式如下:

$ gcc -o test main.c libstr.a

也可以使用命令 -l 库名 进行,库名是不包含函数库和扩展名的字符串。例如编译 main.c 链接静态库 libstr.a 的命令可以修改为:

$ gcc -o test main.c -lstr

上面的命令将在系统默认的路径下查找 str 函数库,并把它链接到要生成的目标程序上。可能系统会提示无法找到库文件 str , 这是由于 str 库函数没有在系统默认的查找路径下,需要显示指定库函数的路径,例如库文件和当前编译文件在同一目录下:

$ gcc -o test main.c -L./ -lstr

生成动态链接库

动态链接库是程序运行时加载的库,当动态链接库正确安装后,所有的程序都可以使用动态库来运行程序。动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址是相对地址,不是绝对地址,其真实地址在调用动态库的程序加载时形成。

动态链接库的名称有别名(soname)、真名(realname)和链接名(linker name)。别名由一个前缀 lib,然后是库的名字,再加上一个后缀 .so 构成。真名是动态链接库的真实名称,一般总是在别名的基础上加上一个小版本号、发布版本等构成。除此之外,还有一个链接名,即程序链接时使用的库的名字。在动态链接库安装的时候,总是复制库文件到某个目录下,然后用一个软链接生成别名,在库文件进行更新的时候,仅仅更新软链接即可。

生成动态链接库

生成动态链接库的命令很简单,使用 -fPIC 选项或者 -fpic-fPIC-fpic 选项的作用是使得 gcc 生成的代码是位置无关的,例如下面的命令将 string.c 编译生成动态链接库:

$ gcc -shared -Wl,-soname,libstr.so -o libstr.so.1 string.c

其中,选项 -soname,libstr.so 表示生成动态库时的别名是 libstr.so-o libstr.so.l 选项则表示是生成名字为 libstr.so.1 的实际动态链接库文件; -shared 告诉编译器生成一个动态链接库。

生成动态链接库之后一个很重要的问题就是安装,一般情况下将生成的动态链接库复制到系统默认的动态链接库的搜索路径下,通常有/lib/usr/lib/usr/local/lib,放到以上任何一个目录下都可以。

动态链接库的配置

动态链接库不能随意使用,要在运行的程序中使用动态链接库,需要指定系统的动态链接库搜索的路径,让系统找到运行所需的动态链接库才可以。系统中的配置文件 /etc/ld.so.conf 是动态链接库的搜索路径配置文件。在这个文件内,存放着可被 Linux 共享的动态链接库所在目录的名字(系统目录/lib 、/usr/lib 除外),多个目录名间以空白字符(空格、换行等)或冒号或逗号分隔。查看系统中的动态链接库配置文件的内容:

$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf

查看这个文件夹

$ ls /etc/ld.so.conf.d
fakeroot-x86_64-linux-gnu.conf  x86_64-linux-gnu.conf
i386-linux-gnu.conf             zz_i386-biarch-compat.conf
libc.conf

从上面的配置文件可以看出, 在系统的动态链接库配置中,包含了该动态库 /lib/i386-linux-gnu/usr/lib/i386-linux-gnu/lib/i686- linux-gnu/usr/lib/i686-linux-gnu 四个目录。

动态链接库管理命令

为了让新增加的动态链接库能够被系统共享,需要运行动态链接库的管理命令 ldconfigldconfig 命令的作用是在系统的默认搜索路径,和动态链接库配置文件中所列出的目录里搜索动态链接库,创建动态链接装入程序需要的链接和缓存文件。搜索完毕后,将结果写入缓存文件 /etc/ld.so.cache 中,文件中保存的是已经排好序的动态链接库名字列表。ldconfig 命令行的用法如下:

ldconfig [-v|--verbose] [-n] [-N] [-X] [-f CONF] [-C CACHE] [-r ROOT] [-l] [-p|--print-cache] [-c FORMAT] [--format=FORMAT] [-V] [-?|--help|--usage] path...

ldconfig 的选项含义:

选项 含义
-v 此选项打印 ldconfig 的当前版本号,显示所扫描的每一个目录和动态链接库
-n 此选项处理命令行指定的目录,不对系统的默认目录 /lib/usr/lib 进行扫描,也不对配置文件 /etc/ld.so.conf 中所指定的目录进行扫描
-N 此选项 ldconfig 不会重建缓存文件
-X 此选项 ldconfig 不更新链接
-f CONF 此选项使用用户指定的配置文件代替默认文件 /etc/ld.so.conf
-C CACHE 此选项使用用户指定的缓存文件代替系统默认的缓存文件 /etc/ld.so.cache
-r ROOT 此选项改变当前应用程序的根目录
-l 此选项用于手动链接单个动态链接库
-p –print-cache 此选项用于打印出缓存文件中共享库的名字

如果想知道系统中有哪些动态链接库,可以使用 ldconfig-p 选项来列出缓存文件中的动态链接库列表。

使用 ldconfig 命令,默认情况下并不将扫描的结果输出。使用 -v 选项会将 ldconfig 在运行过程中扫描到的目录和共享库信息输出到终端,用户可以看到运行的结果和中间的信息。在执行 ldconfig 后,将刷新缓存文件 /etc/ld.so.cache

当用户的目录并不在系统动态链接库配置文件 etc/ld.so.conf 中指定的时候,可以使用 ldconfig 命令显示指定要扫描的目录,将用户指定目录中的动态链接库放入系统中进行共享。命令格式的形式为:

$ ldconfig 目录名

这个命令将 ldconfig 指定的目录名中的动态链接库放入系统的缓存 /etc/ld.so.cache 中,从而可以被系统共享使用。

下面的代码将扫描当前用户的 lib 目录,将其中的动态链接库加入系统:

$ ldconfig ~/lib

如果在运行上还命令后,再次运行 ldconfig 而没有加参数,系统会将 /lib/usr/libetc/ld.so.conf 中指定目录中的动态库加入缓存,这时候上述代码中的动态链接库可能不被系统共享了。

使用动态链接库

在编译程序时,使用动态链接库和静态链接库是一致的,使用 -l 库名 的方式,在生成可执行文件的时候会链接库文件。例如下面的命令将源文件 main.c 编译成可执行文件 test ,并链接库文件 libstr.a 或者 libstr.so

$ gcc -o test main.c -L./ -lstr

-L 指定链接动态链接库的路径, -lstr 链接库函数 str 。但是运行 test 一般会出现如下问题:

./test: error while loading shared libraries: libstr.so: cannot open shared object file: No such file or directory

这是由千程序运行时没有找到动态链接库造成的。程序编译时链接动态链接库和运行时使用动态链接库的概念是不同的,在运行时,程序链接的动态链接库需要在系统目录下才行。有以下几种办法可以解决此问题。

  • 将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量 LD_LIBRARY_PATH 中实现,例如:
$ export LD_LIBRARY_PATH=/example/ex02: $LD_LIBRARY_PATH

将存放库文件 libstr.so 的路径 /example/ex02 加入到搜索路径中,再运行程序就没有之前的警告了。

  • 另一种方法是使用 ld-Linux.so.2 来加载程序,命令格式为:
/lib/ld-Linux.so.2 --library-path 路径 程序名

加载 test 程序的命令为:

/lib/ld-Linux.so.2 --library-path /example/ex02 test

如果系统的搜索路径下同时存在静态链接库和动态链接库,默认情况下会链接动态链接库。如果需要强制链接静态链接库, 需要加上 -static 选项,即上述的编译方法改为如下的方式:

$ gcc -o test main.c -static -lstr

动态加载库

动态加载库和一般的动态链接库所不同的是,一般动态链接库在程序启动的时候就要寻找动态库,找到库函数;而动态加载库可以用程序的方法来控制什么时候加载。

动态加载库主要有函数 dlopen() dlerror() dlsym() dlclose()

打开动态库 dlopen() 函数

函数 dlopen() 按照用户指定的方式打开动态链接库,其中参数 filename 为动态链接库的文件名,flag为打开方式,一般为 RTLD_LASY, 函数的返回值为库的指针。其函数原型如下:

void *dlopen(const char *filename, int flag);

例如,下面的代码使用 dlopen 打开当前目录下的动态库 libstr.so 。

void *phandle = dlopen("./libstr.so", RTLD_LAZY);

获得函数指针 dlsym()

使用动态链接库的目的是调用其中的函数,完成特定的功能。函数 dlsym() 可以获得动态链接库中指定函数的指针,然后可以使用这个函数指针进行操作。函数 dlsym() 的原型如下:

void *dlsym(void *handle, char *symbol);

其中参数 handledlopen() 打开动态库后返回的句柄,参数 symbol 为函数的名称,返回值为函数指针。

使用动态加载库的一个例子

下面是一个动态加载库使用的例子。首先使用函数 dlopen() 来打开动态链接库,判断是否正常打开,可以使用函数 dlerror() 判断错误。如果上面的过程正常,使用函数 dlsym() 来获得动态链接库中的某个函数,可以使用这个函数来完成某些功能。其代码如下:

#include <dlfcn.h>

int main(void)
{
    char src[] = "Hello AimerNeige!";
    int (*pStrLenFun) (char *str);
    void *phandle = NULL;
    char *perr = NULL;
    phandle = dlopen("./libstr.so", RTLD_LAZY);

    if (!phandle)
    {
        printf("Failed Load library!\n");
    }
    perr = dlerror();
    if (perr != NULL)
    {
        printf("%s\n", perr);
        return 0;
    }

    pStrLenFun = dlsym(phandle, "StrLen");
    perr = dlerror();
    if (perr != NULL)
    {
        printf("%s\n", perr);
        return 0;
    }

    printf("The string length is: %d\n", pStrLenFun(src));

    dlclose(phandle);
    return 0;
}

编译上述文件的时候需要链接动态库 libdl.so,使用如下的命令将上述代码编译成可执行文件 testdl。命令将 main.c 编译成可执行文件 testdl,并链接动态链接库 libdl.so

$ gcc -o testdl main.c libstr.so -ldl

执行文件 testdl 的结果为:

$ ./testdl
string length is: 18

GCC 常用选项

-DMACRO 选项

定义一个宏,在多种预定义的程序中会经常使用。如下代码根据系统是否定义 Linux 宏来执行不同代码。使用 -D 选项可以选择不同的代码段,例如 -DOS_LINUX 选项将执行代码段(1)。

#ifdef OS_LINUX
...代码段(1#else
...代码段(2#endif
  • -Idir 将头文件的搜索路径扩大,包含 dir 目录。
  • -Ldir 将链接时使用的链接库搜索路径扩大,包含 dir 目录。gcc 都会优先使用共享程序库。
  • -static 仅选用静态程序库进行链接,如果一个目录中静态库和动态库都存在,则仅选用静态库。
  • -g 包括调试信息。
  • -On 优化程序,程序优化后执行速度会更快,程序的占用空间会更小。通常 gcc 会进行很小的优化,优化的级别可以选择,即 n。最常用的优化级别是 2。
  • -Wall 打开所有的 gcc 能够提供的、常用的警告信息。

GCC 常用选项及含义

-Wall 选项集合

GCC 的警告选项 含义
-Wchar-subscripts 这个选项针对数组的下标值,如果下标值是 char 类型的则给出警告。因为在一些平台上, char 类型的变量可能定义为 signed char, 是一个符号类型的整数,所以 char 类型的变量做下标时,当下标的值为负数时可能造成内存溢出。
-Wcomment 这个选项针对代码中的注释,如果出现不合适的注释格式的时候会出现警告。例如在 /*...*/ 中间出现 /*,或者在 //... 类型的注释末尾出现符号 \ 的时候, GCC 给出警告
-Wformat 这个选项针对输入输出的格式,检查 printfscanf 等格式化输入输出函数的格式字符串与参数类型的匹配情况,如果发现不匹配则发出警告
-Wimplicit 这个选项针对函数的声明,这个选项是选项 -Wimplicit-int 和选项 -Wimplicit-function-declaration 两个选项的集合。第一个选项在声明函数时如果没有指定返回值会给出警告;第二个参数如果在声明前调用函数会给出警告
-Wmissing-braces 这个选项针对结构类型或者数组初始化时的不合适格式。例如, int array[2][2] = {0, 0, 1, 1}; -Wm1ssmg-braces 由于初始化的表达式没有充分用 {} 括起来, GCC 会给出警告,应该采用下面的定义方法:int array[2][2] = { {0, 0}, {1, 1} };
-Wparentheses 这个选项针对多种优先级的操作符在一起或者代码结构难以看明白的操作,如果没有将操作进行明晰地分离, GCC 会给出警告。例如,`if (a && b
-Wsequenve-point 这个选项针对顺序点,如果在代码中使用了有可能造成顺序点变化的语句, GCC 会给出警告。例如,代码 i = i++ 在不同的平台上 i 值的结果是不同的,如果使用了这样的代码,GCC 会给出警告
-Wswitch 这个选项针对 switch 语句,如果一个 switch 语句中没有 default 条件,GCC 会给出警告信息
-Wunused 这个选项针对代码中没有用到的变量、函数、值、转跳点等, 它是 -Wunused-function-Wunused-label-Wunused-variable-Wunosed-value 选项的集合。 -Wunused-function 选项警告代码中存在没有使用的静态函数,或者只定义却没有实现的静态函数; -Wunused-label 选项警告代码中存在定义了却没有使用,或者使用了却没有定义的标签; -Wunused-variable 选项警告代码中存在定义了却没有使用的局部变量; -Wunused-value 选项警告代码中计算表达式的结果没有使用
-Wunused-parameter 这个选项针对函数参数,如果一个函数的参数在函数实现中没有用到, GCC 会给出警告信息
-Wuninitialized 这个选项针对没有初始化变量的使用,如果一个局部变量在使用之前没有初始化, GCC 会给出警告信息

-Wall 警告选项

GCC 的警告选项 含义
-Wflot-equal 这个选项针对浮点值相等的判定,由千浮点值的确切值难以得知,所以如果浮点值出现在相等判定的表达式中, GCC 会给出警告。浮点值的相等判定可以用浮点值的差与某个小值比较判定是否相等
-Wshadow 这个选项用千局部变量作用域内的同名变量,如果局部变量的作用域-Wshadow 范围内有其他同名变量时,局部变量会遮蔽全局变量,这时 GCC 会给出警告信息
-Wbad-functuin-cast 这个选项针对函数的返回值,当函数的返回值赋给不匹配的类型时,GCC 会给出警告信息
-Wsign-compare 这个选项针对有符号数和无符号数的比较,由于无符号数的优先级比有符号数的优先级高,二者进行比较运算的时候,会先将有符号数转换为无符号数。在负的有符号数和无符号数进行比较的时候,容易出现错误
-Waggregate-return 这个选项针对结构类型的函数返回值,如果函数的返回值为结构、联合等类型时, GCC 会给出警告信息
-Wmultichar 这个选项针对字符类型变量的错误赋值,当使用类似 char c = 'test' 这样的代码时, GCC 会给出警告
-Wunreachable-code 这个选项针对冗余代码,如果代码中有不能到达的代码时, GCC 会给出警告信息

其他

GCC 的警告选项 含义
-Wtraditional 选项 traditional 试图支待传统 C 编译器的某些方面

ANSI 兼容

GCC 的警告选项 含义
-ansi 与 ansi 的 C 语言兼容
-pedantic 允许发出 ANSI/ISO C 标准所列出的所有警告
-pedantic-errors 允许发出 ANSI/ISO C 标准所列出的所有错误

编译检查

GCC 的警告选项 含义
-fsynatax-only 仅进行编译检查而不实际编译程序

在编写代码的时候,不好的习惯会造成程序执行过程中发生错误。在一个比较大的项目中, 当程序运行起未后再查找这些错误是很困难的。因此一种好的习惯是使用编译选项将代码的警告信息显示出来,并对代码进行改正。例如,打开编译选项 -Wall-W 来显示所有的警告信息,甚至更严格一些,打开 -Werror 将编译时的警告信息作为错误信息未处理,中断编译。