谷动谷力

 找回密码
 立即注册
查看: 2152|回复: 2
打印 上一主题 下一主题
收起左侧

gcc 编译命令详解及最佳实践

[复制链接]
跳转到指定楼层
楼主
发表于 2023-8-12 09:51:00 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式



gcc 编译命令详解及最佳实践


GCC介绍
GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。

GCC 原名为 GNU C语言编译器,因为它原本只能处理 C 语言,但如今的 GCC 不仅可以编译 C、C++ 和 Objective-C,还可以通过不同的前端模块支持各种语言,包括 Java、Fortran、Ada、Pascal、Go 和 D 语言等等。

GCC 的编译过程可以划分为四个阶段:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)以及链接(Linking)。


Linux 程序员可以根据自己的需要控制 GCC 的编译阶段,以便检查或使用编译器在该阶段的输出信息,帮助调试和优化程序。以 C 语言为例,从源文件的编译到可执行文件的运行,整个过程大致如下。


各文件后缀说明如下:
后缀
描述
后缀
描述
.c
C 源文件
.s/.S
汇编语言源文件
.C/.cc/.cxx/.cpp
C++ 源文件
.o/.obj
目标文件
.h
C/C++ 头文件
.a/.lib
静态库
.i/.ii
经过预处理的 C/C++ 文件
.so/.dll
动态库
语法:
gcc [options] file...
选项:
  • -pass-exit-codes :从一个阶段以最高错误代码退出。
  • --target-help :显示特定于目标的命令行选项。
  • -help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...] :显示特定类型的命令行选项(使用 -v --help 显示子进程的命令行选项)。
  • -dumpspecs :显示所有内置规范字符串。
  • -dumpversion :显示编译器的版本。
  • -dumpmachine :显示编译器的目标处理器。
  • -print-search-dirs :显示编译器搜索路径中的目录。
  • -print-libgcc-file-name :显示编译器配套库的名称。
  • -print-file-name=<lib> :显示库 <lib> 的完整路径。
  • -print-prog-name=<prog> :显示编译器组件 [size=0.9em]<prog> 的完整路径。
  • -print-multiarch :显示目标的规范化 GNU 三元组,用作库路径中的一个组件。
  • -print-multi-directory :显示 libgcc 版本的根目录。
  • -print-multi-lib :显示命令行选项和多个库搜索目录之间的映射。
  • -print-multi-os-directory :显示操作系统库的相对路径。
  • -print-sysroot :显示目标库目录。
  • -print-sysroot-headers-suffix :显示用于查找标题的 sysroot 后缀。
  • -Wa,<options> :将逗号分隔的<options> 传递给汇编器(assembler)。
  • -Wp,<options> :将逗号分隔的 <options> 传递给预处理器(preprocessor)。
  • -Wl,<options> :将逗号分隔的 <options> 传递给链接器(linker)。
  • -Xassembler <arg> :将 <arg> 传递给汇编器(assembler)。
  • -Xpreprocessor <arg> :将 <arg> 传递给预处理器(preprocessor)。
  • -Xlinker <arg> :将 <arg> 传递给链接器(linker)。
  • -save-temps :不用删除中间文件。
  • -save-temps=<arg> :不用删除指定的中间文件。
  • -no-canonical-prefixes :在构建其他 gcc 组件的相对前缀时,不要规范化路径。
  • -pipe :使用管道而不是中间文件。
  • -time :为每个子流程的执行计时。
  • -specs=<file> :使用 <file> 的内容覆盖内置规范。
  • -std=<standard> :假设输入源为<standard>
  • --sysroot=<directory> :使用 <directory> 作为头文件和库的根目录。
  • -B <directory> :将 <directory> 添加到编译器的搜索路径。
  • -v :显示编译器调用的程序。
  • -### :与 -v 类似,但引用的选项和命令不执行。
  • -E :仅执行预处理(不要编译、汇编或链接)。
  • -S :只编译(不汇编或链接)。
  • -c :编译和汇编,但不链接。
  • -o <file> :指定输出文件。
  • -pie :创建一个动态链接、位置无关的可执行文件。
  • -I :指定头文件的包含路径。
  • -L :指定链接库的包含路径。
  • -shared :创建共享库/动态库。
  • -static :使用静态链接。
  • --help :显示帮助信息。
  • --version :显示编译器版本信息。

示例阶段编译
假设有文件 hello.c,内容如下:
#include <stdio.h> ​ int main(void) {     printf("Hello, GetIoT\n");     return 0; }
编译 hello.c,默认输出 a.out
gcc hello.c
编译 hello.c 并指定输出文件为 hello
gcc hello.c -o hello
只执行预处理,输出 hello.i 源文件
gcc -E hello.c -o hello.i
只执行预处理和编译,输出 hello.s 汇编文件
gcc -S hello.c
也可以由 hello.i 文件生成 hello.s 汇编文件
gcc -S hello.i -o hello.s
只执行预处理、编译和汇编,输出 hello.o 目标文件
gcc -c hello.c
也可以由 hello.i 或 hello.s 生成目标文件 hello.o
gcc -c hello.i -o hello.o gcc -c hello.s -o hello.o
由 hello.o 目标文件链接成可执行文件 hello
gcc hello.o -o hello

使用静态库
创建一个 foo.c 文件,内容如下:
#include <stdio.h> ​
void foo(void)
{
     printf("Here is a static library\n");
}
将 foo.c 编译成静态库 libfoo.a
gcc -c foo.c             # 生成 foo.o 目标文件
ar rcs libfoo.a foo.o    # 生成 libfoo.a 静态库
查看文件描述
$ file * foo.c:    C source, ASCII text
foo.o:    ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
libfoo.a: current ar archive

修改 hello.c 文件,调用 foo 函数
#include <stdio.h> ​
void foo(void); ​
int main(void)
{
     printf("Hello, GetIoT\n");
     foo();
     return 0;
}
编译 hello.c 并链接静态库 libfoo.a(加上 -static 选项)
gcc hello.c -static libfoo.a -o hello
也可以使用 -L 指定库的搜索路径,并使用 -l 指定库名
gcc hello.c -static -L. -lfoo -o hello
运行结果
$ ./hello
Hello, GetIoT Here is a static library
查看 hello 文件描述
$ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b72236c2211dd8f0c3003bc02ad5e70bb2354e8c, for GNU/Linux 3.2.0, not stripped

使用共享库
修改 foo.c 文件,内容如下:
#include <stdio.h> ​ void foo(void) {     printf("Here is a shared library\n"); }
将其编译为动态库/共享库(由于动态库可以被多个进程共享加载,所以需要使用 -fPIC 选项生成位置无关的代码
gcc foo.c -shared -fPIC -o libfoo.so
hello.c 代码无需修改,内容仍然如下:
#include <stdio.h> ​
void foo(void); ​
int main(void)
{
     printf("Hello, GetIoT\n");
     foo();
     return 0;
}

编译 hello.c 并链接共享库 libfoo.so
gcc hello.c libfoo.so -o hello
也可以使用 -L 和 -l 选项指定库的路径和名称
gcc hello.c -L. -lfoo -o hello
但是此时运行 hello 程序失败
$ ./hello ./hello: error while loading shared libraries:
libfoo.so: cannot open shared object file: No such file or directory
原因是找不到 libfoo.so 共享库
$ ldd hello
         linux-vdso.so.1 (0x00007fff5276d000)
         libfoo.so => not found
         libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcc90fa7000)
         /lib64/ld-linux-x86-64.so.2 (0x00007fcc911bd000)
这是因为 libfoo.so 并不在 Linux 系统的默认搜索目录中,解决办法是我们主动告诉系统,libfoo.so 共享库在哪里。

方式一:设置环境变量 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$(pwd)
将 libfoo.so 所在的当前目录添加到 LD_LIBRARY_PATH 变量,再次执行 hello
$ ./hello Hello, GetIoT Here is a shared library

方式二:使用 rpath 将共享库位置嵌入到程序
gcc hello.c -L. -lfoo -Wl,-rpath=`pwd` -o hello
rpath 即 run path,是种可以将共享库位置嵌入程序中的方法,从而不用依赖于默认位置和环境变量。这里在链接时使用 -Wl,-rpath=/path/to/yours 选项,-Wl 会发送以逗号分隔的选项到链接器,注意逗号分隔符后面没有空格哦。
这种方式要求共享库必须有一个固定的安装路径,欠缺灵活性,不过如果设置了 LD_LIBRARY_PATH,程序加载时也是会到相应路径寻找共享库的。

方式三:将 libfoo.so 共享库添加到系统路径
sudo cp libfoo.so /usr/lib/
执行程序
$ ./hello Hello, GetIoT Here is a shared library
如果 hello 程序仍然运行失败,请尝试执行 ldconfig 命令更新共享库的缓存列表。
此时,再次查看 hello 程序的共享库依赖
$ ldd hello
         linux-vdso.so.1 (0x00007ffecfbb1000)
         libfoo.so => /lib/libfoo.so (0x00007f3f3f1ad000)
         libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3f3efbb000)
         /lib64/ld-linux-x86-64.so.2 (0x00007f3f3f1d6000)

可以看到 libfoo.so 已经被发现了,其中 /lib 是 /usr/lib 目录的软链接。
示例代码可以在 GitHub 找到。


原文链接:
gcc 命令 - 人人都懂物联网​getiot.tech/linux-command/gcc.html

+10
回复

使用道具 举报

沙发
 楼主| 发表于 2023-8-12 10:06:03 | 只看该作者
-Wall

当GCC在编译过程中检查出错误的话,它就会中止编译;但检测到警告时却能继续编译生成可执行程序,因为警告只是针对程序结构的诊断信息,它不能说明程序一定有错误,而是存在风险,或者可能存在错误。虽然GCC提供了非常丰富的警告,但前提是你已经启用了它们,否则它不会报告这些检测到的警告。

在众多的警告选项之中,最常用的就是-Wall选项。该选项能发现程序中一系列的常见错误警告,该选项用法举例如下:

$ gcc -Wall test.c -o test

该选项相当于同时使用了下列所有的选项:

◆unused-function:遇到仅声明过但尚未定义的静态函数时发出警告。
◆unused-label:遇到声明过但不使用的标号的警告。
◆unused-parameter:从未用过的函数参数的警告。
◆unused-variable:在本地声明但从未用过的变量的警告。
◆unused-value:仅计算但从未用过的值得警告。
◆Format:检查对printf和scanf等函数的调用,确认各个参数类型和格式串中的一致。
◆implicit-int:警告没有规定类型的声明。
◆implicit-function-:在函数在未经声明就使用时给予警告。
◆char-subscripts:警告把char类型作为数组下标。这是常见错误,程序员经常忘记在某些机器上char有符号。


◆missing-braces:聚合初始化两边缺少大括号。
◆Parentheses:在某些情况下如果忽略了括号,编译器就发出警告。
◆return-type:如果函数定义了返回类型,而默认类型是int型,编译器就发出警告。同时警告那些不带返回值的 return语句,如果他们所属的函数并非void类型。
◆sequence-point:出现可疑的代码元素时,发出报警。
◆Switch:如果某条switch语句的参数属于枚举类型,但是没有对应的case语句使用枚举元素,编译器就发出警告(在switch语句中使用default分支能够防止这个警告)。超出枚举范围的case语句同样会导致这个警告。
◆strict-aliasing:对变量别名进行最严格的检查。
◆unknown-pragmas:使用了不允许的#pragma。
◆Uninitialized:在初始化之前就使用自动变量。

需要注意的是,各警告选项既然能使之生效,当然也能使之关闭。比如假设我们想要使用-Wall来启用个选项,同时又要关闭unused警告,利益通过下面的命令来达到目的:

$ gcc -Wall -Wno-unused test.c -o test

下面是使用-Wall选项的时候没有生效的一些警告项:

◆cast-align:一旦某个指针类型强制转换时,会导致目标所需的地址对齐边界扩展,编译器就发出警告。例如,某些机器上只能在2或4字节边界上访问整数,如果在这种机型上把char *强制转换成int *类型, 编译器就发出警告。
◆sign-compare:将有符号类型和无符号类型数据进行比较时发出警告。
◆missing-prototypes :如果没有预先声明函数原形就定义了全局函数,编译器就发出警告。即使函数定义自身提供了函数原形也会产生这个警告。这样做的目的是检查没有在头文件中声明的全局函数。

◆Packed:当结构体带有packed属性但实际并没有出现紧缩式给出警告。
◆Padded:如果结构体通过充填进行对齐则给出警告。
◆unreachable-code:如果发现从未执行的代码时给出警告。
◆Inline:如果某函数不能内嵌(inline),无论是声明为inline或者是指定了-finline-functions 选项,编译器都将发出警告。
◆disabled-optimization:当需要太长时间或过多资源而导致不能完成某项优化时给出警告。

上面是使用-Wall选项时没有生效,但又比较常用的一些警告选项。本文中要介绍的最后一个常用警告选项是-Werror。使用该选项后,GCC发现可疑之处时不会简单的发出警告就算完事,而是将警告作为一个错误而中断编译过程。该选项在希望得到高质量代码时非常有用。

+10
回复 支持 反对

使用道具 举报

板凳
发表于 2023-8-14 22:05:45 | 只看该作者
在生成动态库时,常常习惯性的加上 fPIC 选项,fPIC 有什么作用和意义,加不加有什么区别,这里做下小结。

fPIC 的全称是 Position Independent Code, 用于生成位置无关代码。什么是位置无关代码,个人理解是代码无绝对跳转,跳转都为相对跳转。

编译将报错

/usr/bin/ld: /tmp/ccCViivC.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
/tmp/ccCViivC.o: could not read symbols: Bad value

2、加 fPIC 选项
加上 fPIC 选项生成的动态库,显然是位置无关的,这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。通常的方法是获取指令指针的值,加上一个偏移得到全局变量 / 函数的地址。

加 fPIC 选项的源文件对于它引用的函数头文件编写有很宽松的尺度。比如只需要包含个声明的函数的头文件,即使没有相应的 C 文件来实现,编译成 so 库照样可以通过。

3、在内存引用上,加不加 fPIC 的异同
加了 fPIC 实现真正意义上的多个进程共享 so 文件。

多个进程引用同一个 PIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,但操作系统显然会把它们映射到同一块物理内存上。

对于不加 fPIC,则加载 so 文件时,需要对代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个 .so 文件代码段的进程在内核里都会生成这个 .so 文件代码段的 copy。每个 copy 都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置。

可见,这种方式更消耗内存。

但是不加 fPIC 编译的 so 文件的优点是加载速度比较快。
+10
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|深圳市光明谷科技有限公司|光明谷商城|Sunshine Silicon Corpporation ( 粤ICP备14060730号|Sitemap

GMT+8, 2024-4-29 16:58 , Processed in 0.095330 second(s), 46 queries .

Powered by Discuz! X3.2 Licensed

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表