前言
本篇是个人学习笔记。有大量抄录,但并非转载,故仍记为原创,仅个人学习使用,侵删,有帮助到您 并非我本意。
本篇是在学习linux时写的,所以只关于linux下的gcc。
1、什么是GCC编译器
GCC是GNU公社的一个项目,它是GNU Compiler Collection的缩写,起初,GCC只是一个C语言编译器(GNU C Collection),随着开发者的加入,GCC已扩展为可以编译C、C++、Java、Objective-C等多种编程语言的编译器集合了。如今的GCC也具有了交叉编译的功能,即在一个平台上编译另一个平台的代码。
所谓编译器,可以简单地将其理解为“翻译器”。要知道,计算机只认识二进制指令(仅有 0 和 1 组成的指令),我们日常编写的 C语言代码、C++ 代码、Go 代码等,计算机根本无法识别,只有将程序中的每条语句翻译成对应的二进制指令,计算机才能执行。
C语言编程中常见的编译器包括MSVC,GCC,Clang+LLVM,MinGW,Visual C++等等
GCC的功能组成可以大致分为以下几个部分:
- 前端(Front-end):GCC的前端负责将源代码解析并生成中间表示(IR),同时进行语法分析、语义分析和类型检查等相关工作。针对不同的编程语言,GCC提供了相应的前端模块,如C前端(gcc/c)、C++前端(gcc/cc1plus)、Objective-C前端(gcc/cc1obj)等。
- 优化器(Optimizer):GCC的优化器对生成的中间表示进行优化,以提高程序的性能和执行效率。优化器可以进行诸如常量传播、死代码消除、循环优化、函数内联等一系列优化操作。
- 后端(Back-end):GCC的后端将优化后的中间表示翻译成目标机器的汇编代码。GCC的后端部分是与目标机器架构相关的,针对不同的目标机器架构,需要提供相应的后端模块,如x86后端(gcc/cc1)、ARM后端(gcc/cc1arm)等。
- 连接器(Linker):GCC的连接器负责将编译后的目标文件(或库文件)进行链接,生成可执行文件或共享库。连接器将不同的目标文件合并成一个整体,并解析符号引用和重定位等操作。
1.1、什么是GUN
GUN 是 Richard Stallman在1984年组织开发的一个完全基于自由软件的软件体系结构,英文叫做General Public License,简称GPL.Linux以及相关modules的大量软件在GPL的推动下开发和发布.Stallman一直在传播自由软件的好处,他创立的GUN梦想是:“自由的思想,而不是免费的午餐”。
自由软件意味着使用者有运行、复制、发布、研究、修改和改进该软件的自由。
更精确地说,自由软件赋予软件使用者四项基本自由:
- 不论目的为何,有运行该软件的自由(自由之零)。
- 有研究该软件如何工作以及按需改写该软件的自由(自由之一)。取得该软件源代码为达成此目的之前提。
- 有重新发布拷贝的自由,这样你可以借此来敦亲睦邻(自由之二)。
- 有向公众发布改进版软件的自由(自由之三),这样整个社群都可因此受惠。取得该软件源码为达成此目的之前提。
GNU 操作系统做了名为 Thr Hurd 的系统内核,但由于其性能比不上同时期诞生的 Linux 内核,最终 GNU 计划放弃 The Hurd 而选用 Linux 作为 GNU 操作系统的内核。在 Linux 内核的基础上,GNU 计划开发了很多系统部件,GCC 就是其中之一(除此之外,还有 Emacs 等非常实用的软件)。
dev c++就是集成了 GCC 编译器的开发软件
2、第一次编译
2.1、安装GCC编译器
- 打开你的 Linux 系统的终端。
- 输入
sudo apt-get update
并按回车键,以确保你的软件包索引是最新的。这样当你安装、升级或搜索软件包时,就能确保你获取的是最新的版本信息。 - 输入
sudo apt-get install gcc
并按回车键,开始安装 GCC 编译器。 - 系统会提示你输入密码,输入你的用户密码以获取管理员权限。
- 系统会显示安装过程中的信息,包括下载和安装的进度。
- 安装完成后,你可以通过输入
gcc --version
来验证 GCC 是否已正确安装。
执行 sudo apt-get install gcc
命令时,除了安装 GCC 编译器本身,还会安装一些与之相关的依赖包和文件。这些文件和包可能包括:
- GCC 编译器:这是主要的编译器程序,用于编译 C 和 C++ 程序。
- 标准库:GCC 需要一些标准库文件来编译程序,例如 glibc。
- 头文件:这些是 C 和 C++ 语言的头文件,它们定义了语言的标准接口。
- 静态库和动态库:这些是编译过程中可能需要的库文件。
- 调试工具:如
gdb
,用于调试编译后的程序。 - 其他工具:例如
make
,用于自动化编译过程;g++
,用于编译 C++ 程序;gcc-ar
,gcc-nm
,gcc-ranlib
等,这些都是与编译过程相关的辅助工具。 - 文档:安装过程中可能会安装一些手册页和文档。
- 配置文件:用于配置编译器和编译过程的文件。
- 示例代码:有时,安装 GCC 也会附带一些示例代码或测试程序。
2.2、第一个程序
首先你要有一个熟悉的编辑器,比如vim,Emacs等
在编辑器内写入一段c语言程序,比如 hello.c。
在终端页面,执行gcc ./hello.c -o hello
执行./hello
则会执行hello.c的代码内容。
2.3、使用方法
GCC最基本的用法是∶gcc [options] [filenames]
options就是参数,filename就是相关文件名称
以下是一些常用的 GCC 参数,根据个人情况查看:
-o <file>:指定输出文件的名称,如果不给出输出名称,可能生成a.out
-c:只编译和汇编,但不链接成可执行文件,此时编译器,只根据.c等源文件,生成.o后缀的目标文件,通常用于编译子程序文件。
-S:只编译,不汇编,此时文件后缀为.s
-E:只进行预处理,此时文件后缀为.i
-g:生成调试信息,供GDB使用
-O<level>:优化代码。<level> 可以是 0、1、2、3,级别越高,优化程度越高。
-I<dir>:添加头文件搜索路径。
-L<dir>:添加库文件搜索路径。
-l<library>:链接时搜索并使用指定的库。
-Wall:打开大多数警告信息。
-Werror:将所有警告当错误处理。
-pedantic:要求 GCC 严格按照标准来编译代码。
-std=<standard>:指定使用的编程语言标准,例如 -std=c99 用于 C99 标准。
-static:静态链接所有库。
-dynamic:动态链接所有库。
-fPIC:生成位置无关代码,用于生成共享库。
-fpic:生成位置无关代码,但比 -fPIC 产生的代码体积稍大。
-shared:生成共享库。
-nostdinc:不使用标准头文件。
-nostdlib:不使用标准库。
-nostartfiles:不使用启动文件。
-nodefaultlibs:不使用默认库。
-D<macro>:定义宏。
-U<macro>:取消宏定义。
-Wl,<option>:传递链接器选项。
-Wa,<option>:传递汇编器选项。
-Wp,<option>:传递预处理器选项。
-v:显示编译过程中的信息。
--help:显示帮助信息。
--version:显示版本信息。
3、GCC编译的基本流程
一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇 编(assembly)和链接(linking)等4步才能变成可执行文件。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
3.1、预处理
我们写的c代码,首先是预处理源代码,如果需要在预处理之后停止需要使用选项-E
:
gcc –E hello.c –o hello.i
简单来说,预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个 “.i” 文件中等待进一步处理。所用 到的工具为cc1(它的名字就是cc1,x86有自己的cc1命令,ARM板也有自己的 cc1 命令)。
预编译过程主要处理那些源代码文件中以 “#“开始的预编译指令。比如”#include”、"#define"等,主要处理规则如下:
- 将所有的 “#define” 删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如"#if"、“#ifdef”、“#elif”、“#else”、“#endif”
- 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
- 删除所有的注释"//“和”/* */"
- 添加行号和文件名标识,比如 #2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
- 保留所有的 #pragma 编译器指令,因为编译器需要使用它们
经过预编译后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确的时候,可以查看预编译后的文件来确定问题。
预处理指令
指示字 | 描述 |
---|---|
#define | 定义宏名字,预处理程序会把这个宏扩展到使用该名字的位置 |
#elif | 由#if 指示字提供一个用于计算的可选表达式 |
#else | 如果#if、#ifdef 或#ifndef 为假,提供一个用于编译的可选代码集合 |
#error | 产生出错消息,挂起预处理程序 |
#if | 如果计算算术表达式的结果为非零值,就编译指示字和它匹配的#endif 之间的代码 |
#ifdef | 如果已经定义了指定的宏,就编译指示字和它匹配的#endif 之间的代码 |
#ifndef | 如果没有定义指定的宏,就编译指示字和它匹配的#endif 之间的代码 |
#include | 查找指示字列表,直到找到指定的文件,然后将文件内容插入,就好像在文本编辑器中插入一样 |
#include_next | 和#include 一样,但该指示字从找到当前文件的目录之后的目录开始查找 |
#line | 指出行号以及可能的文件名,报告给编译程序,用于创建目标文件中的调试信息 |
#pragma | 提供额外信息的标准方法,可用来指出一个编译程序或一个平台 |
#undef | 删除前面用#define 指示字创建的定义 |
#warning | 由预处理程序创建一个警告消息 |
## | 连接操作符,可用于宏内将两个字符串连接成一个 |
头文件的处理
头文件一般分为系统头文件和用户头文件。
-
系统头文件通常是用来调用系统库,在#include后面要用尖括号。
-
用户头文件中通常是函数、全局变量的外部声明, 宏定义,结构体定义,类型定义等。用户头文件起到了一个接口的作用,将不同的独立C文件通过头文件联系起来。用户头文件在#include后面要用引号。
对于头文件的处理实际上就是复制粘贴一份到你的代码里面,但是怎么查找到这些头文件的呢。
#include <> : 直接到系统指定的某些目录中去找某些头文件。
#include “ ” : 先到源文件所在文件夹去找,然后再到系统指定的某些目录中去找某些头文件。
gcc寻找头文件的路径(按照1->2->3的顺序)
-
先搜索当前目录;(用户头文件)
-
在gcc编译源文件的时候,通过参数-I指定头文件的搜索路径,如果指定路径有多个路径时,则按照指定路径的顺序搜索头文件。这里源文件的路径可以是绝对路径,也可以是相对路径
“gcc -I /path/where/theheadfile/in sourcefile.c“
设当前路径为/root/test,include_test.c如果要包含头文件“include/include_test.h“,有两种方法:
-
include_test.c中#include “include/include_test.h”或者#include “/root/test/include/include_test.h”,然后gcc include_test.c即可
-
include_test.c中#include <include_test.h>或者#include <include_test.h>,然后gcc –I include include_test.c也可
-
-
通过查找gcc的环境变量C_INCLUDE_PATH/CPLUS_INCLUDE_PATH/OBJC_INCLUDE_PATH来搜索头文件位置。(系统头文件)
-
再找内定目录搜索,分别是
/usr/include
/usr/local/include
/usr/lib/gcc-lib/i386-linux/2.95.2/include
最后一行是gcc程序的库文件地址,各个用户的系统上可能不一样。
gcc还有一个参数:使用
-nostdinc
选项时,编译器不会自动包含这些标准头文件,而只搜索-I选项指定的路径和当前路径。这意味着你只能使用你自己提供的头文件。这在某些情况下是有用的,比如当你想确保代码不依赖于任何特定的库或者当你想完全控制编译器的预处理阶段时。(系统头文件)
-
当#include使用相对路径的时候,gcc最终会根据上面这些路径,来最终构建出头文件的位置。如#include <sys/types.h>就是包含文件/usr/include/sys/types.h(系统头文件)
修改搜索路径
可以去/etc/profile修改全局环境变量(C_INCLUDE_PATH / CPLUS_INCLUDE_PATH)去添加自定义的头文件路径
export C_INCLUDE_PATH=/home/xxxxxxx:$C_INCLUDE_PATH
export CPLUS_INCLUDE_PATH=/home/xxxxxxx:$CPLUS_INCLUDE_PATH
修改后的文件不会立即生效,可以通过如下命令使修改生效:
source ~/.bashrc
小结
预处理之后,代码还是代码,是文本文件,打开你的代码,你还是能看的懂.
3.2、编译
编译阶段将预处理后的文件转换成汇编语言。GCC会检查代码的语法错误,并生成汇编语言文件,通常以*.s*为后缀。也是文本文件。编译成汇编文件大小已经非常小了,没有像预处理的时候文件大小这么臃肿。所用 到的工具为cc1(它的名字就是cc1,x86有自己的cc1命令,ARM板也有自己的 cc1 命令)。
编译的命令为:
gcc -S test.i -o test.s
不同优化级别:
- 使用
-O0
,-O1
,-O2
,-O3
等选项来指定不同的优化级别。 - 比较不同优化级别生成的汇编代码,观察代码结构和效率上的差异。
比较不同优化级别的汇编代码
不同的优化级别会影响编译器的优化决策,从而影响生成的汇编代码:
- 无优化(-O0):生成的汇编代码通常较长,保持了源代码的结构和顺序。便于调试,但性能不是最优的。
- 一些优化(-O1):执行基本优化,提高执行效率,但仍然保持一定的可读性。
- 更多优化(-O2):进行更多优化,包括代码重排、循环优化等。通常是生产环境中的首选优化级别。
- 高级优化(-O3):最高级别的优化,可能包括内联函数、向量化等。生成的汇编代码可能与原始C++代码差异较大。
什么是汇编语言
汇编语言(Assembly Language)是任何一种用于电子 计算机 、 微处理器 、 微控制器 或其他可编程器件的低级语言,亦称为 符号语言。 在汇编语言中,用 助记符 代替 机器指令 的 操作码,用地址符号或 标号 代替指令或 操作数 的地址
简单的例子
Hello World 是一个简单的程序,打印 Hello,World!在屏幕上。该程序通常用于向初学者介绍新的编程语言。
让我们看看如何在 汇编语言 中打印出 “Hello,World!”:
section .text
global _start ;必须为链接器(ld)声明
_start: ;告诉链接器入口点
mov edx,len ;消息长度
mov ecx,msg ;写消息
mov ebx,1 ;文件描述符 (stdout)
mov eax,4 ;系统调用号 (sys_write)
int 0x80 ;调用内核
mov eax,1 ;系统调用号 (sys_exit)
int 0x80 ;调用内核
section .data
msg db 'Hello, world!', 0xa ;要打印的字符串
len equ $ - msg ;字符串的长度
编译器如何将C++代码转换为汇编
编译器的主要任务是将预处理后的代码转换为汇编语言。这个过程包括几个关键步骤:
- 词法分析:将源代码分解为标记(tokens),例如关键字、标识符、运算符。
- 语法分析:根据C++的语法规则,将标记组织成语法结构(例如表达式、语句)。
- 语义分析:检查代码的语义正确性,如类型检查、变量声明。
- 中间代码生成:生成一种中间表示(IR)的代码,用于优化和进一步处理。
- 优化:在IR上执行各种优化,以提高代码效率。
- 目标代码生成:将IR转换为特定架构的汇编代码。
这个过程举例:
假设我们有以下简单的C++程序:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3);
std::cout << "The result is: " << result << std::endl;
return 0;
}
- 词法分析
过程:编译器首先进行词法分析,将代码分解为一系列标记(tokens)。
示例:例如,int, add, (, int, a, ), {, return, a, +, b, ;, } 等。
- 语法分析
过程:这些标记被组织成语法结构,形成一个语法树。
示例:编译器识别 int add(int a, int b) { return a + b; } 为一个函数定义,int result = add(5, 3); 为一个变量声明和函数调用。
- 语义分析
过程:编译器检查语义正确性,如类型匹配、变量是否声明等。
示例:确认函数 add 接受两个整数参数,返回值也是整数,以及 result 变量的类型。
4. 中间代码生成
过程:编译器生成中间代码,通常是一种平台独立的低级代码。
示例:生成可以表示程序逻辑的中间代码,如三地址代码(Three-Address Code)。
- 优化
过程:在中间代码上执行各种优化,提高代码效率。
示例:优化可能包括消除不必要的计算和操作,优化循环等。
6. 目标代码生成
过程:将中间代码转换为特定架构的机器代码或汇编代码。
示例:生成x86或ARM等架构的汇编代码,如将 add 函数和 main 函数转换为汇编指令。
汇编代码示例(假设)
假设目标是x86架构,编译后的汇编代码可能类似于:
add:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
pop ebp
ret
main:
push ebp
mov ebp, esp
sub esp, 4
push 3
push 5
call add
add esp, 8
mov [ebp-4], eax
... ; 代码以打印结果和退出程序继续
具体汇编语言的学习,移步别的大佬
cc1
具体来说,cc1 程序的主要作用包括以下几个方面:
- 对源代码进行预处理:cc1 程序会对源代码文件进行预处理,包括宏展开、条件编译、头文件包含等操作。
- 语法分析和语义分析:cc1 程序会对预处理后的代码进行语法分析和语义分析,以确定代码的结构、变量类型、函数定义等信息。
- 中间代码生成:在进行语法和语义分析后,cc1 程序会生成中间代码,这是一种与机器无关的代码表示形式,它可以被进一步优化和转换为目标代码。
- 代码优化:cc1 程序会对生成的中间代码进行优化,以提高程序的执行效率和性能。
- 汇编代码生成:在中间代码优化后,cc1 程序将生成汇编代码,并进行一系列的指令选择和寄存器分配等操作,以生成最终的目标代码。
在Linux命令行中逐步运行c语言程序,包括cpp、cc1、as、gcc
cpp test.c > test.i //使用预处理器对源代码预处理
cc1 test.i -o test.s //使用编译器cc1对预处理的代码进行编译
as test.s -o test.o //使用汇编器as将汇编代码转化为目标文件
gcc test.o -o test //使用链接器ld将目标文件与所需要的库链接成最终的可自行文件
具体内部实现后续再写。
3.3、汇编
汇编阶段将汇编语言文件转换成机器代码,生成的文件通常以*.o为后缀。这些.o*文件被称为目标文件
gcc –c hello.s –o hello.o
gcc/g++的汇编过程通过 as 工具完成,所以我们可以通过g++ -c
或as
命令完成汇编。
- as的内部预处理主要包括三个方面的工作
- 调整和去除额外的间隔符,保留每行的关键字前的一个空格或者TAB,其他任意的间隔符都转换为一个空格。
- 去除所有注释,代之以一个空格,或者新行的合适的数字。
- 把字符常量转换成相应的数字值。
- 它不能做宏处理和文件包含处理,如果需要用,可以交给 C 预处理器来处理
语法:as(选项)(参数)
常用选项如下:
选项 | 描述 |
---|---|
-ac | 忽略失败条件; |
-ad | 忽略调试指令; |
-ah | 包括高级源; |
-al | 包括装配; |
-am | 包括宏扩展; |
-an | 忽略形式处理; |
-as | 包括符号; |
=file | 设置列出文件的名字; |
–alternate | 以交互宏模式开始; |
-f | 跳过空白和注释预处理; |
-g | 产生调试信息; |
-J | 对于有符号溢出不显示警告信息; |
-L | 在符号表中保留本地符号; |
-o | 指定要生成的目标文件; |
–statistics | 打印汇编所用的最大空间和总时间。 |
例子:as -o main.o main.s –32,编译32位的汇编代码
3.4、链接
链接阶段是将所有的目标文件和库文件汇集成一个可执行的二进制代码文件,在成功编译之后,就进入了链接阶段。在这个阶段,编译器会解决程序中的符号引用和地址计算,使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
在这里涉及到一个重要的概念:库文件。
库文件
所谓库文件,读者可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件)。
库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。例如,C
语言库文件提供有大量的函数(如 scanf()
、printf()
、strlen()
等),C++
库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string
字符串类)。库文件的调用方法也很简单,以 C
语言中的 printf()
输出函数为例,程序中只需引入 <stdio.h>
头文件,即可调用 printf()
函数。
调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:
- 头文件只存储变量、函数或者类等这些功能模块的声明部分;
- 库文件才负责存储各模块具体的实现部分;
所有的库文件都提供有相应的头文件作为调用它的接口。也就是说,库文件是无法直接使用的,只能通过头文件间接调用。
头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。
库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。要知道,虽然库文件用于程序的链接阶段,但编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中
采用静态链接方式实现链接操作的库文件,称为静态链接库;
采用动态链接方式实现链接操作的库文件,称为动态链接库;
我们还可以根据实际需要,手动创建静态链接库或者动态链接库。
静态链接库
静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
使用静态库文件实现程序的链接操作,既有优势也有劣势:
-
优势是,生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强);
-
劣势是,如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。
和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。
在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示;
在 Windows 系统中,静态链接库文件的后缀名为 .lib;
创建静态链接库
静态链接库其实就相当于压缩包,其内部可以包含多个源文件。但需要注意的是,并非任何一个源文件都可以被加工成静态链接库,其至少需要满足以下 2 个条件:
- 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含
main
主函数; - 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件;
将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:
将所有指定的源文件,都编译成相应的目标文件
g++ -c greeting.cpp name.cpp
ls
function.h greeting.cpp greeting.o main.cpp name.cpp name.o
然后使用 ar 压缩指令,将生成的目标文件打包成静态链接库,其基本格式如下:
ar rcs 静态链接库名称 目标文件1 目标文件2 ...
有关 ar 打包压缩指令,以及 rcs 各选项的含义和功能,请参考 Linux ar命令
制作静态库时所使用的指令$ ar rcs libcalc.a add.o sub.o mult.o div.o
共有三个参数:
- -c:创建一个库,不管库是否存在,都将创建。这个很好理解,就不做过多的解释了。
- -r:在库中插入(替换)模块 。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
- -s:创建目标文件索引,这在创建较大的库时能加快时间。
- 在获取一个静态库的时候,我们可以通过
$ nm -s libcalc.a
来显示库文件中的索引表:
重点说明的是,静态链接库的不能随意起名,需遵循如下的命名规则:
libxxx.a
Linux 系统下,静态链接库的后缀名为 .a;
Windows 系统下,静态链接库的后缀名为 .lib;
其中,xxx 代指我们为该库起的名字,比如 Linux 系统自带的一些静态链接库名称为 libc.a、libgcc.a、libm.a,它们的名称分别为 c、gcc 和 m。
下面,将 greeting.o、name.o 打包到一个静态链接库中:
ar rcs libmyfunction.a name.o greeting.o
ls
function.h greeting.cpp greeting.o libmyfunction.a main.cpp name.cpp name.o
其中,libmyfunction.a 就是 name.o、greeting.o 一起打包生成的静态链接库,myfunction 是我们自定义的库名。
使用静态链接库
静态链接库的使用很简单,就是在程序的链接阶段,将静态链接库和其他目标文件一起执行链接操作,从而生成可执行文件。
g++ -static main.o libmyfunction.a
-static
选项强制 GCC
编译器使用静态链接库
注意,如果 GCC 编译器提示无法找到 libmyfunction.a,还可以使用如下方式完成链接操作:
g++ -static main.o -L /home/wohu/cpp/src -l myfunction
ls
a.out function.h greeting.cpp greeting.o libmyfunction.a main.cpp main.o name.cpp name.o
其中,
- -L(大写的 L)选项用于向 GCC 编译器指明静态链接库的存储位置(可以借助 pwd 指令查看具体的存储位置);
- -l(小写的 L)选项用于指明所需静态链接库的名称,注意这里的名称指的是 xxx 部分,且建议将 -l 和 xxx 直接连用(即 -lxxx),中间不需有空格。
由此,就生成了 a.out 可执行文件
库函数和你的代码之间,依靠头文件联系
动态链接库
在 Linux 中动态库以 lib 作为前缀、以 .so 作为后缀,形如 libxxx.so(其中的 xxx 是库的名字,自己指定即可)。相比于静态库,使用动态库的程序,在程序编译时并不会链接到目标代码中,而是在运行时才被载入。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。
生成动态库是直接使用 gcc 命令,并且需要添加 -fpic 以及 -shared 参数:
- -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。
- -shared 参数的作用是告诉编译器生成一个动态链接库。
gcc -shared add.o sub.o mult.o -o libcalc.so
- 提供头文件 head.h
- 提供动态库 libcalc.so
和静态库的链接方式一样,都是通过指令$ gcc main.c -o main -L ./ -l calc
来进行链接库操作。
gcc 通过指定的动态库信息生成了可执行程序 main,但是可执行程序运行却提示无法加载到动态库:
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
这是怎么回事呢?
解决动态库加载失败的问题
首先来看一下不同库的工作原理:
- 静态库如何被加载:
- 在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。
- 当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
- 动态库如何被加载:
- 在程序编译的最后一个阶段也就是链接阶段,在 gcc 命令中虽然指定了库路径,但是这个路径并没有被记录到可执行程序中,只是检查了这个路径下的库文件是否存在。同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。
- 当可执行程序被执行起来之后:
- 程序会先检测所需的动态库是否可以被加载,加载不到就会提示上边的错误信息。
- 当动态库中的函数在程序中被调用了,这个时候动态库才加载到内存,如果不被调用就不加载。
动态库的检测和内存加载操作都是由动态链接器来完成的
动态链接器是一个独立于应用程序的进程,属于操作系统。当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L 指定的路径。
那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
- 可执行文件内部的 DT_RPATH 段。
- 系统的环境变量 LD_LIBRARY_PATH。
- 系统动态库的缓存文件 /etc/ld.so.cache。
- 存储「静态库 / 动态库」的系统目录 /lib、/usr/lib 等。
按照以上四个顺序,依次搜索,找到之后结束遍历。若检索到最终还是没找到,那么动态连接器就会提示动态库找不到的错误信息。一般情况下,我们都是通过修改系统的环境变量的方式设置动态库的地址。
将动态库路径追加到环境变量 LD_LIBRARY_PATH 中:$ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:动态库的绝对路径
比如,我所需要的动态库的绝对路径为 /mnt/hgfs/SharedFolders/DynamicLibrary,那么:
$ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/mnt/hgfs/SharedFolders/DynamicLibrary
这样的话,我在运行 main,就不会报错了。
但是通过这种方式设置的环境变量尽在当前的终端中有效,那么怎样才能让这个设置永久生效呢?
通过指令$ vim ~/.bashrc
打开并修改该文件:
修改后,使用$ source ~/.bashrc
使修改立即生效。
链接
懂了库之后,链接几乎也就懂了
链接就是将汇编生成的.o文件、系统库的.o文件、库文件链接起来,最终生成可以在特定平台运行的可执行程序。
编译的时候:
gcc会先搜索-L指定的目录;
再搜索gcc的环境变量LIBRARY_PATH;
再搜索系统目录:/lib和/lib64、/usr/lib 和/usr/lib64、/usr/local/lib和/usr/local/lib64,这是当初compile gcc时写在程序内的。
运行时动态库的搜索路径
动态库的搜索路径搜索的先后顺序是:
编译目标代码时指定的动态库搜索路径;
环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
配置文件/etc/ld.so.conf中指定的动态库搜索路径;
默认的动态库搜索路径/lib;
默认的动态库搜索路径/usr/lib。
hello程序调用了printf函数。 printf函数存在于一个名为printf.o的单独的预编译目标文件中。 链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。
这个设置永久生效呢?
通过指令$ vim ~/.bashrc
打开并修改该文件:
[外链图片转存中…(img-GfL6WPZf-1725801378872)]
修改后,使用$ source ~/.bashrc
使修改立即生效。
链接
懂了库之后,链接几乎也就懂了
链接就是将汇编生成的.o文件、系统库的.o文件、库文件链接起来,最终生成可以在特定平台运行的可执行程序。
编译的时候:
gcc会先搜索-L指定的目录;
再搜索gcc的环境变量LIBRARY_PATH;
再搜索系统目录:/lib和/lib64、/usr/lib 和/usr/lib64、/usr/local/lib和/usr/local/lib64,这是当初compile gcc时写在程序内的。
运行时动态库的搜索路径
动态库的搜索路径搜索的先后顺序是:
编译目标代码时指定的动态库搜索路径;
环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
配置文件/etc/ld.so.conf中指定的动态库搜索路径;
默认的动态库搜索路径/lib;
默认的动态库搜索路径/usr/lib。
hello程序调用了printf函数。 printf函数存在于一个名为printf.o的单独的预编译目标文件中。 链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。