Linux操作系统下编译、链接过程详解

gcc和g++的区别:

gcc和g++是GNU编译器集合中的两个不同的编译器,它们之间的主要区别在于它们所针对的编程语言以及它们的行为和功能。

1. 编译器的目标语言:gcc是用于编译C语言的编译器,而g++是用于编译C++语言的编译器。因此它们分别用于编译不同的源代码文件;

2. 语法支持:gcc和g++对于各自的目标语言有着不同的语法支持。gcc支持C语言的语法和功能,包括C11、C99等版本的标准,而g++则支持C++语言的语法和功能,包括C++11、C++14、C++17等版本的标准;

3. 标准库链接:gcc和g++默认链接的标准库不同。gcc会链接C标准库(libc),g++会链接C++标准库(libstdc++),是因为C和C++标准库在实现和功能上有所不同;

4. 标准扩展:g++提供了对C++语言的一些扩展支持,例如模板、异常处理、运算符重载等C++特有的功能;

5. 链接器行为:g++会自动链接C++运行时支持库(libstdc++),以提供C++语言所需的特定运行时支持,相比之下,使用gcc编译C程序时,默认不会自动链接C运行时的支持库(libc)。

gcc/g++的编译过程:

gcc/g++编译过程有四大步骤:预处理、编译、汇编、链接   ;以下面的C代码做示例:test.cpp

1. 预处理:

预处理是读取c源程序,对其中的伪指令(以#开头的指令,也就是宏)和特殊符号进行“替代”处理;经过此处理,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义跟没有经过预处理的源文件是相同的,仍然是C文件,只是内容有所不同。

预处理的过程主要处理包括以下过程:

  • 将所有的#define删除,并且展开所有的宏定义;
  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等;
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置;
  • 删除所有注释 “//”和”/* */”;
  • 添加行号和文件标识,以便编译时生成调试用的行号及编译错误警告行号;
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。

预处理命令生成.i文件:gcc/g++ -E test.cpp -o test.i   //-E只对其预处理,-o给文件取别名

2. 编译:

 编译所做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码(计算机所认识的01语言)。

 编译命令生成.s汇编文件:gcc/g++  -S test.i -o test.s(-S只进行编译和汇编,生成汇编代码而不进行链接

 使用不同的交叉编译器编译同一个test.i文件生成的汇编文件也不相同,这也是C语言可移植性的一种体现。

3. 汇编:

汇编的过程实际上指把汇编语言代码翻译成目标机器指令的过程,对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

目标文件由段组成,通常一个目标文件中至少有两个段:

  • 代码段(文本段):该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般不可写;
  • 数据段:主要存放程序中要用到的各种常量、全局变量、静态的数据。一般数据段都是可读,可写,可执行的;

汇编命令生成.o文件:gcc/g++ -c test.s -o test.o    //-c汇编文件,不链接

4. 链接:

 链接的主要工作就是将有关的目标文件(如:库函数、其他程序中的变量等)彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体,也就是生成可执行程序

根据指定的库函数的链接方式的不同,链接处理可分为两种:

  • 静态链接

  gcc/g++ test.o -o test-static//静态链接编译

  • 动态链接(运行时把所需要的文件进行链接,程序的可扩展性和兼容性更高)

 gcc/g++ test.o -o test//动态链接编译

静态链接:静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样导致的结果是可执行文件会比较大(即在编译时就进行链接)

动态链接:动态链接是指在链接阶段仅仅只加入一些描述信息,而程序执行时再从操作系统中把相应的动态库加载到内存中去(即在运行时进行链接)

编译参数说明:

gcc/g++支持多种参数,这些参数用于控制编译过程、指定文件和目录、选择编译模式等,下面是一些常用的编译器参数和解释:

1. 编译器控制参数:

-c:只进行编译,生成目标文件而不进行链接;

-E:只进行预处理,生成预处理后的原代码;

-S:只进行编译和汇编,生成汇编代码而不进行链接;

-o <文件>:指定生成的目标文件或可执行文件的名称。

2. 语言和标准库参数:

-std=<标准>:指定要使用的语言标准,如-std=c++11、-std=c++17等;

-nostdinc:禁止使用默认的标准库头文件;

-nostdlib:禁止使用默认的标准库;

-l<库>:链接所指定的库文件,如-lm表示链接数学库;

-L<目录>:指定要链接库文件的搜索路径;

-I:指定需要的头文件。

3. 调试和优化参数:

-g:生成调试信息,用于gdb调试;

-O<级别>:指定优化级别,如:-O0(禁止优化)或-O2(启动常用的优化);

-Wall:显示所有警告信息;

-Werror:见所有警告视为错误。

4. 预处理器、汇编器、链接器参数:

汇编器是将高级编程语言源代码转换为机器码的工具

-Wp<参数>-Wpreprocessor <参数>将参数传递给预处理器,如:

-Wp,-D<宏>:编译时定义一个宏;

-Wp,-U<宏>:编译时取消定义一个宏;

-Wp,-I<目录>:指定头文件的搜索目录;

-Wp,-include<文件>:在编译之前先包含所指定的文件;

-Wa<参数>-Wassembler <参数>将参数传递给汇编器,如:

-Wa,-ahl:生成带有行号的汇编代码(包括C/C++源代码行号);

-Wa,-gstabs:生成包含调试信息的汇编代码;

-Wa,-mfpu=vfp:指定FPU(浮点计算单元选项);

-Wa,-march=armv8-a:指定CPU架构选项、-Wa,-march=<架构>:指定其他架构选项;

-Wl<参数>-Wlinker <参数>将参数传递给链接器,如:

-Wl,--as--needed(告诉链接器仅在需要具体库时才链接该库,此时链接器就不再强制遵循命令行参数的链接顺序);

-Wl,--no-as-needed(确保特定库的始终在其他库之前链接,可使用该参数);

-Wl,--start-group和-Wl,--end-group这两个参数用于将链接的范围定义在一对花括号之间,并且可以保证范围内的链接库按照正确的顺序进行链接。

5. 其他参数:

-v显示详细的编译器输出信息;

-save-temps:保留中间文件,不删除;

-save-temps=<args>:不删除中间文件,是一个目录名;

-no-canonical-prefixes:构建相对于其他gcc组件的前缀时,不规范化路径;

-pipe:使用管道而不是中间文件;

-time:计时每个子进程的执行时间;

-pthread:表示为多线程程序需要链接线程库。

参数-fPIC的作用:

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

gcc -shared -fPIC -o 1.so 1.c

PIC使得.so文件的代码段变为真正意义上的共享,如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy。每个copy都不一样,取决于这个.so文件代码段和数据段内存映射的位置。也就是不加fPIC编译出来的so,是要再加载时根据加载到的位置再次重定位的(因为它里面的代码并不是位置无关代码)如果被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本了(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)我们总是用fPIC来生成.so,也从来不用fPIC来生成.a;fPIC与动态链接可以说基本没有关系,libc.so一样可以不用fPIC编译,只是这样的so必须要在加载到用户程序的地址空间时重定向所有表目。因此,不用fPIC编译.so并不总是不好。

Linux与链接器相关的编译选项说明:

链接组:

-Wl,--start-group -Wl,--whole-archive -Wl,--no-whole-archive -Wl,--end-group是一组常用的编译选项,用于控制链接器对库的链接顺序:

  • -Wl,--start-group:表示开始一个新的链接组,在该组中的库将被循环链接,直到遇到--end-group;

  • -Wl,--whole-archive:用于告诉编译器对接下来的所有库都执行完整链接,而不仅仅是解决未定义符号;

  • -Wl,--no-whole-archive:结束完整链接的范围,后续的库将恢复到默认的按需链接;

  • -Wl,--end-group:表示结束当前链接组。

禁止自动添加链接库的依赖项:

-Wl,--no-as-needed是一个编译选项,用于告诉编译器在链接过程中不要根据符号的依赖关系自动添加被链接库的依赖项:

默认情况下,链接器会根据目标文件和库之间的符号依赖关系来确定需要链接的库。

这意味着如果一个库中的某个符号被目标文件使用,链接器会自动将该库添加到最终生成的可执行文件或共享库中。

然而,有时候我们可能希望禁止链接器根据这种自动依赖机制添加库的依赖项。使用-Wl,--no-as-needed 选项可以实现这一点。

它告诉链接器不要自动添加被链接库的依赖项,即使目标文件使用了该库中的符号。这样可以避免由于库之间的依赖顺序导致的链接错误。

请注意,-Wl,--no-as-needed 是一个链接器选项,需在编译命令中加入该选项才能生效。

减小输出文件的大小并提高程序运行效率:

-Wl,--gc-sections -Wl,-rpath都是与链接器相关的编译选项

  • -Wl,--gc-sections:这个选项时用于告诉编译器在最终生成的可执行文件或共享库中去除未使用的代码和数据段(garbage collection of sections),通过启用该选项,可以减小输出文件的大小,去除未使用的代码和数据,提高程序的运行效率。

  • -Wl,-rpath:这个选项用于指定运行时库搜索路径(runtime library search path),当运行生成可执行文件或共享库时,系统会搜索指定的路径以及查找所需的动态链接库,可以通过使用该选项来指定自定义的运行时库搜索路径,以确保程序在运行时能够正确地找到所需的库文件。

注意:-Wl,--gc-sections 和 -Wl,-rpath 通常作为传递给编译器的参数,其中 -Wl 是将选项传递给链接器的方式。

gcc编译时开启编译选项实现代码的静态扫描

使用gcc编译程序时可以开启特定的编译选项来实现对代码的静态扫描-赋值解决编译警告,使得代码安全性更高:

  • -Wall:开启常见的警告;

  • -g:开启调试打印,支持gdb调试;

  • -Olevel:优化等级level为0~3,3为最高;

  • -Werror:编译警告变为编译错误,会导致编译不通过;

  • -w:禁止所有警告;

  • -fanalyzer:开启静态分析(gcc 10版本及以上);

  • -Wno-error=:指定某个警告不变成错误;

  • -Wfatal-errors:有一个编译error就终止;

  • -Wextra:开启除-Wall以外的多个警告;

  • -fanalyzer-verbose-edges:打印fanlyzer底层的调试信息,控制流(不常用);

  • -fanalyzer-verbose-state-changes:打印fanalyzer底层调试信息,状态变化(不常用);

  • -fanalyzer-verbosity=level:设置打印等级level为0~4,4最详细;

  • -Wshadow:同名变量在不同作用域被定义;

  • -Winline:某些函数inline失败时打印;

  • -Wduplicated-branches:if和else分支执行语句内容一样;

  • -Wduplicated-cond:if条件重复;

  • -Walloc-size-larger-than=byte-size:分配堆内存大于某个数值byte-size时;

  • -Wlarger-than=byte-size:单个栈变量内存大于某个数值byte-size时;

  • -Wframe-larger-than=byte-size:栈帧大小大于某个数值byte-size时;

  • -Wstack-usage=byte-size:函数调用栈内存大于某个数值byte-size时;

  • -Wunsafe-loop-optimizations:循环无法被优化;

  • -Wpedantic:是否符合ANSI/ISO C标准;

  • -Wunused-macros:已定义但是位使用的宏定义;

  • -Wcast-qual:可读写双指针转换成const类型;

  • -fno-inline:禁止内联展开。

静态库和动态库的优缺点:

静态库和动态库是在编程中常用的两种库文件形式。它们的区别在于链接方式和引用方式不同:

• 静态库:在编译时将库文件的代码完全拷贝到可执行程序中,因此无需依赖外部库文件。以 .a 或 .lib 作为扩展名。

• 动态库:在程序运行时动态加载库文件,程序只需要引用动态库的入口即可使用其中的函数或资源。以 .so 或 .dll 作为扩展名。

以下是静态库和动态库的优缺点:

静态库的优点:

• 使用简单,方便移植,无需关注依赖情况。

• 执行速度快,因为代码已经被完整地嵌入到可执行程序中。

• 可以避免由于库文件版本变化导致的兼容性问题,保证程序稳定性。

静态库的缺点:

• 相对占用更多的磁盘空间和内存,尤其是在多个程序共享同一个库时会重复消耗系统资源。

• 更新库或修改源代码后需要重新编译和链接,修改后的程序必须重新发布给用户。

动态库的优点:

• 共享库可以被多个程序共用,节省磁盘空间和内存空间。

• 对于多个程序使用相同的库,在系统中只需存在一份动态库。

• 更新库和修改程序时,只需要替换可执行文件或者动态库即可,不必重新编译链接。

动态库的缺点:

• 要求开发者安装正确版本的库文件,且无法控制用户环境下的库文件版本,存在兼容性风险。

• 相对于静态库而言,会存在一定的运行时开销,由于需要在每次程序运行时加载和卸载库文件。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倔强de番茄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值