详解常用gcc编译参数

Last Updated: 2024-02-21 03:00:21 Wednesday

-- TOC --

编译C或C++代码,都可以用gcc(c++使用g++,或者gcc xxx.cpp -lstdc++)。本文总结gcc/g++的常用参数(大部分参数是一样的)。

GNU的工具链,不同工具的很多参数,含义相同...

编译C++代码

以下两种命令行,都可以编译C++代码:

$ g++ xxx.cpp ...
$ gcc xxx.cpp -lstdc++ ...

命令行参数

查看gcc的版本号。

将整个编译链接过程详细显示出来,可以看到对cc1,as和collect2(ld的一个封装)的调用,头文件的搜索路径等。使用gcc -v直接查看当前gcc版本的编译选项!

指定C或C++语言标准版本。比如 -std=c89,-std=c99等是C语言的标准版本号。-std=c++11,-std=c++14等就是C++语言的标准版本号。

查看gcc/g++默认编译标准,查看两个预定义的macro的值,__STDC_VERSION____cplusplus

In C mode, this is equivalent to -std=c90. In C++ mode, it is equivalent to -std=c++98. (c89和c90是一样的)

Do not use the standard system startup files or libraries when linking.

Do not search the standard system directories for header files.

指定输出文件,如果没有-o,输出有可能直接到stdout,有可能到默认的a.out文件;

只做pre-process,预编译,默认向stdout输出,使用-o指定输出文件,一般都用.i结尾。

Inhibit generation of linemarkers in the output from the preprocessor. 在-E预编译时,-P用来抑制linemarker的生成。如果你需要肉眼check预编译的结果,这个参数也许有点用。

指定程序入口点,默认是运行库里的_start

编译到assembly,汇编,默认输出相同文件名,但后缀为.s的文件。

输出intel格式汇编

$ gcc -S -masm=intel test.c  # get test.s

汇编语言学习资料

去掉可执行文件的symtab和strtab,相当于链接后执行strip命令

只编译,不链接,only compile,no link。

生成debug相关的section。在输出的.o和可执行文件中,有很多.debug段。-g3是最高级别,但测试发现,只有-g3不能有效调试macro,需要配合-gdwarf-2(还只能-2)。注意,-g0是个错误,它等于没有-g参数!

直接编译汇编.s代码,如果想调试,也需要-g参数。

链接选项,采用静态链接的方式编译,默认是动态链接

很可能Linux默认并没有安装libc.a,自己安装:

$ sudo dnf install glibc-static

如果没有-fPIC配合,则生成装载时重定位(Load Time Relocation)的动态链接库,这类动态链接库中的指令地址,在装载时会执行重定位,最后生成的还是绝对地址引用,因此这类库无法在不同进程间共享,没有起到节省内存的作用。

没有此参数,就是采用链接时重定位(Link Time Relocation)

创建.so动态链接库的时候,基本都要加上-fPIC参数。

-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址(相对寻址指令,或通过巧妙的计算等到相对位置),故而代码可以被加载到内存的任意位置,被所有需要的进程共享,并都可以正确执行。这正是共享库所要求的,共享库被加载时,在内存中的位置不固定,并能被所有需要的进程共享。

如果共享库不使用-fPIC选项,它的代码就不是地址无关,它就不能被多个进程共享,也就失去了节省内存的优点,但这种装载时重定位的模块,运行速度要比使用地址无关的代码要快一丢丢,因为对外部模块全局对方的访问,采用绝对地址,不用经过GOT和PLT。

-fpic,据说产生的地址无关的代码相对较小,而且执行更快。但此参数的使用在某些平台下有一些限制,主要是GOT的size。按照man gcc中的说明,如果在使用-fpic时出现错误,切换到-fPIC。所以,就直接用-fPIC了!

生成动态链接库

$ gcc <...> -fPIC -shared x1.c x2.c x3.c -o libxxx.so

注意库的文件名:libxxx.so,前有lib,后有.so!!

判断动态库是否为PIC代码

如果下面的readelf命令没有任何输出,就是PIC代码:(TEXTREL表示代码段重定位地址)

$ readelf -d xx. so | grep TEXTREL  # no output means PIC

Position Independent Executable,代码地址无关的可执行文件,或-fpie。大小写的区别,与-fPIC一样。

PIE executables are sort of a hack using a shared object with an entry-point. The dynamic linker already supported this. This was the easiest way to implement ASLR for executables.

-pie是gcc的linking option,如果只使用-fpie,只是将代码编译成使用相对寻址的方式(x64下RIP相对寻址),链接时才分配虚拟地址。因此,这两个参数一般都一起使用。

由于采用相对寻址,-fpie生成的代码会比使用绝对地址的代码慢一丢丢,但会比-fPIC要好一丢丢,因为后者还要通过GOT表访问,会更慢一点点。

指定输入内容是哪种编程语言,比如在命令行用管道给gcc输入一段代码时,要用-x指定语言。Example:

echo "int main(void){return 0;}" | gcc -x c - -o test

编译器的优化选项的级别,-O与-O1一样,默认-O0,-O3优化级别最高,打开了gcc所有的优化选项。

-O0 Reduce compilation time and make debugging produce the expected results. This is the default. (编译时间短,适合调试阶段)

inline关键字生效,至少要用-O1级别!

If you use multiple -O options, with or without level numbers, the last such option is the one that is effective.

如果不小心在命令行写出 -O4,-O5,-O6......gcc不会报错!

Optimized for size!

-O2的基础上,去掉那些可能增大 code size 的优化选项,但-finline-functions选项是enabled。

Optimize aggressively for size rather than speed. This may increase the number of instructions executed if those instructions require fewer bytes to encode. -Oz behaves similarly to -Os including enabling most -O2 optimizations.

更激进的size优化。

比-O3更激进的优化,Disregard strict standards compliance.

Optimize debugging experience。

-Og should be the optimization level of choice for the standard edit-compile-debug cycle, offering a reasonable level of optimization while maintaining fast compilation and a good debugging experience. It is a better choice than -O0 for producing debuggable code because some compiler passes that collect debug information are disabled at -O0.

后来在著名的《CSAPP》书中看到了对这个参数的另一个解释:使用-Og得到的汇编代码,保持了与原C代码一样的代码结构,便于学习和分析汇编。

显示大部分编译告警,强烈建议使用。(不是全部哦)

Output warnings about legal but questionable usage. 打印一些额外的告警信息,建议与-Wall一起使用!

可以用来打开或者关闭(抑制)某个告警,比如要打开一个告警 -Wsequence-point,或要抑制它就写成这样 -Wno-sequence-point。

打开VLA告警,防止代码使用VLA

关闭所有告警信息。

Make all warnings into errors. 有warning时也当成error来处理,此时编译会终止。

就是这个选项导致的这这样的错误显示:cc1plus: all warnings being treated as errors。如果想编译通过,把程序先跑起来,快速地解决方法是:

添加宏定义,不用修改代码,就可以控制条件编译开关。

比如:-DMARCO,效果相当于 #define MARCO 1,比如:-DMARCO=ABCDE,效果相当于 #define MARCO ABCDE,比如:-DMARCO=2,效果相当于 #define MARCO 2

取消宏定义。

-UMARCO,取消宏MARCO的定义,相当于#undef MARCO

Add the directory dir to the list of directories to be searched for header files during preprocessing.

提供头文件的搜索路径,用于在preprocessing阶段搜索头文件。

使用<>包含的头文件,会先搜索-I指定的路径,然后再搜索标准系统路径;使用""包含的头文件,会先搜索当前工作目录,然后再按照<>包含的头文件搜索路径搜索。

提供额外的lib搜索路径。

用于指明链接库的名称,建议将 -l 和 xxx 直接连用(即 -lxxx),中间不需有空格。比如:-lpthread

这里使用的是 Link Name。Linux系统还有个So-Name的概念,可用ldconfig命令维护。

参数顺序

-L-l参数,要放在命令行的后面部分:

$ gcc -L . -ll ...  # link error
$ gcc ... -L . -ltt

将options传递给linker,多个参数用comma分割。

示例:指定--dynamic-linker

-Wl,--subsystem,10,设置linker生成UEFI应用程序,使用x86_64-w64-mingw32-gcc编译生成64位EFI程序。

貌似与-Wl,option功能相同。

例如,设置当前路径为动态链接库的搜索路径(-rpath是ld的参数,rpath=run path):

$ gcc ... -Xlinker -rpath ./

默认编译出来的可执行elf文件,在其.dynamic section中,没有rpath字段,即按系统配置搜索动态链接库。如果有rpath,就只能在rpath指定的路径中搜索。有个小工具,chrpath,修改elf文件的rpath信息。

-f系列参数

貌似-f系列参数都是用来控制代码的生成!

Don't recognize built-in functions that do not begin with __builtin_ as prefix. 那些没有__builtin_前缀的接口符号名称,不会再被识别和替换。

GCC normally generates special code to handle certain built-in functions more efficiently; for instance, calls to alloca may become single instructions which adjust the stack directly, and calls to memcpy may become inline copy loops. (使用printf("string")会被替换成puts("stirng")). The resulting code is often both smaller and faster, but since the function calls no longer appear as such, you cannot set a breakpoint on those calls, nor can you change the behavior of the functions by linking with a different library.

builtin的另一个名称,叫做C intrinsics。GCC有1万多个builtin接口,有一部分是为了提供SIMD指令,比如Intel就定义了一大堆intrinsics接口用来提供其芯片的SIMD计算能力(符号名称没有builtin字样,gcc用有builtin字样的接口上封装了一层,并申明为inline,这些接口可以通过阅读对应的.h文件获知)。C代码可以通过inline assembly使用SIMD能力,也可以通过builtin接口,后者的好处是不用写汇编。使用builtin存在代码可移植性问题,要在编译时做好配置。有人说intrinsics是介于汇编和C语言之间的一层。由于C intrinsics与architecture直接相关,不同architecture之间没有可移植性,因此这部分接口无法通过C语言层面来提供,C intrinsics的存在,就是为了方便程序员可以用非汇编的方式发挥芯片的某些特定功能。

为了性能,程序员的世界正在变得越来越复杂...由于现代编译器还无法直接通过编译的方式,将sequencial code编译为vector code,以便充分利用architecture提供的SIMD加速能力。因此,只能由程序员自己手动去调用这些SIMD指令。也许未来编译器能够变得更强大,但现在还不行...(20231202)

很早以前,为了避免链接时的符号冲突,gcc编译C代码时,要在所有符号前增加_。现在gcc在Linux平台下默认已经不加了,但在windows平台下,默认还是要加。这两个参数,就是用来控制这个细节的。基本上是用不到的参数。

将未初始化的全局变量,当做强符号来处理。参考:common block

AddressSanitizer工具(ASAN),在编译时增加检测代码(instrumentation),并替换掉标准库中的内存申请和释放接口,以便在运行时检查内存访问的错误。不是所有的访问越界都会导致程序崩溃!那些不能让程序立即崩溃的越界,会导致非常难以发现的bug。

Enable AddressSanitizer, a fast memory error detector. Memory access instructions are instrumented to detect out-of-bounds and use-after-free bugs. The option enables -fsanitize-address-use-after-scope.

这个工具是Google开源的,在gcc的manpage中,能查阅到更多详细信息。使用-fsanitize=address的示例

检查内存泄漏的sanitizer工具。

用man gcc可以查询到很多sanitize选项!sanitizer工具会增大可执行文件的size!这些工具在测试时使用,生产环境重新编译时,去掉这些编译选项即可。有个术语,ASAN_CFLAGS

这个叫做UBSAN,可以在运行检测各种UB行为,integer计算的overflow也在它的检测范围内。

ASAN等工具需安装后使用,sudo dnf install libasan libubsan,使用ASAN编译选项,实际上就是gcc链接ASAN的库。

Control Flow protection,这是个增强代码安全的选项,我们看到的x64汇编中的endbr64指令,就是它加上的。

不生成栈帧指针,释放一个free register,属于-O1优化。

用于自动检查buffer overflow的一种手段,默认是开启的。原理是:在stack frame中插入一个特定的值,在function return之前,检查这个值是否还健在,以此来判断是否存在buffer overflow。

-M系列参数

输出编译单元依赖的头文件,仅仅是向stdout输出。

-M,只是过滤了系统头文件。

必须配合-M或-MM使用,将头文件输出到文件。

必须配合-M或-MM使用,改变输出中的object文件名。

$ gcc -MM test.c
test.o: test.c
$ gcc -MM test.c -MT abcd.o
abcd.o: test.c

等同于-M -MF,但可以不指定输出的保存依赖头文件的文件名,采用源文件名+.d后缀的方式。

$ gcc -c -MD test.c
$ ll test.*
-rw-r--r--. 1 xinlin xinlin  478 Feb 12 21:13 test.c
-rw-r--r--. 1 xinlin xinlin  858 Feb 13 17:10 test.d
-rw-r--r--. 1 xinlin xinlin 1.8K Feb 13 17:10 test.o

等同于-MD,仅包含用户头文件,过滤系统头文件。

看看cmake对-M系参数的使用:

# 用make VERBOSE=1查看
-MD -MT test/CMakeFiles/utility_hpp_test.dir/utility_hpp_test.cpp.o -MF CMakeFiles/utility_hpp_test.dir/utility_hpp_test.cpp.o.d

用-MT改变输出中的object文件名,让这个文件名带上路径。用了-MD,但同时也用-MF指定了输出文件。

Machine Options

下面是machine options,使用-m开头:

设置汇编语法格式,默认att,可以-masm=intel,使gcc(gas)可以编译intel风格的汇编代码,包括inline asm。

在64位系统中,按32位模式编译代码。

很有可能64位系统中缺少编译32位代码的库,解决办法就是安装:

$ # Ubuntu
$ sudo apt install gcc-multilib g++-multilib
$ # Fedora
$ sudo dnf install glibc-devel.i686 libstdc++-devel.i686

这里有个扩展内联汇编的case

指定ISA(Instruction Set Architecture)。

下面是关于RISC-V这部分的说明:

-march=ISA-string

Generate code for given RISC-V ISA (e.g. rv64im). ISA strings must be lower-case. Examples include rv64i, rv32g, rv32e, and rv32imaf.

指定CPU支持的指令集合。

指定ABI(Application Binary Interface)接口类型。

下面是关于RISC-V这部分的说明:

-mabi=ABI-string

Specify integer and floating-point calling convention. ABI-string contains two parts: the size of integer types and the registers used for floating-point types. For example -march=rv64ifd -mabi=lp64d means that long and pointers are 64-bit (implicitly defining int to be 32-bit), and that floating-point values up to 64 bits wide are passed in F registers. Contrast this with -march=rv64ifd -mabi=lp64f, which still allows the compiler to generate code that uses the F and D extensions but only allows floating-point values up to 32 bits long to be passed in registers; or -march=rv64ifd -mabi=lp64, in which no floating-point arguments will be passed in registers.

The default for this argument is system dependent, users who want a specific calling convention should specify one explicitly. The valid calling conventions are: ilp32, ilp32f, ilp32d, lp64, lp64f, and lp64d. Some calling conventions are impossible to implement on some ISAs: for example, -march=rv32if -mabi=ilp32d is invalid because the ABI requires 64-bit values be passed in F registers, but F registers are only 32 bits wide. There is also the ilp32e ABI that can only be used with the rv32e architecture. This ABI is not well specified at present, and is subject to change.

对部分代码使用不同的编译参数

如下示例,为了测试慢速copy,不能有编译优化。

#pragma GCC push_options
#pragma GCC optimize ("O0")
void* memcpy_slow(void *__restrict dest,
                  const void *__restrict src, size_t n){
    if(n != 0){
        char *d = (char*)dest;
        char *s = (char*)src;
        for(size_t i=0; i<n; ++i)
            *d++ = *s++;
    }
    return dest;
}
#pragma GCC pop_options

申明C++代码要链接的C接口

使用extern "C" {}

#ifdef __cplusplus
#include <cstddef>
extern "C" {
#else
#include <stddef.h>
#endif
void* memcpy_slow(void *__restrict dest,
                         const void *__restrict src,
                         size_t n);
void* memcpy_fast(void *__restrict dest,
                  const void *__restrict src, size_t n);
#ifdef __cplusplus
}
#endif

字节对齐

字节对齐的数字,需要是2的幂次。

#pragma pack(1)   // 1字节对齐
typedef struct xyz{
    char a;
    int b;
    char c;
}xyz;
#pragma pack()    // 恢复默认
// sizeof(xyz) == 6

消除部分代码的特定warning

#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
    constexpr void deallocate(T *p, std::size_t n){
        ::operator delete(p);
    }
#pragma GCC diagnostic pop
#endif

查询或对比具体的优先选项

The output is sensitive to the effects of previous command-line options, so for example it is possible to find out which optimizations are enabled at -O2 by using:

$ gcc -Q -O2 --help=optimizers

Alternatively you can discover which binary optimizations are enabled by -O3 by using:

$ gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
$ gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
$ diff /tmp/O2-opts /tmp/O3-opts | grep enabled

编译AVX2代码

示例:LeetCode第1题,两数和,最后的SIMD版本。

本文链接:https://cs.pynote.net/sf/c/cdm/202111137/

-- EOF --

-- MORE --