当前位置: 首页 > article >正文

gcclinux静态库动态库学习

目录

一、gcc

1.gcc编译器流程

2.gcc编译程序

3.gcc常用参数

4.多文件编译

5.gcc和g++

二、linux静态库和动态库

1.静态库

1.1 生成静态链接库

1.2静态库制作举例

1.2.1 准备测试程序

1.2.2生成静态库

1.3静态库的使用

2.动态库

2.1 生成动态链接库

2.2动态库制作

2.3动态库的使用

2.4解决动态库无法加载问题

2.4.1库的工作原理

2.4.2 动态链接器

2.4.3解决方案

2.4.4验证

3.优缺点

3.1 静态库

3.2动态库


最近系统学习了gcc,因此写下面这个博文进行总结,所参考的主要是博主:爱编程的大丙

一、gcc

1.gcc编译器流程

GCC 编译器的编译流程是:预处理、编译、汇编和链接。

预处理就是展开所有的头文件、替换程序中的宏、去掉注释行(这个阶段需要GCC调用预处理器来完成,最终得到的还是源文件,文本格式)

编译是将经过预编译处理的代码编译成汇编代码,也就是我们常说的程序编译。

汇编就是将汇编语言文件编译成二进制目标文件

链接就是将汇编出来的多个二进制目标文件链接在一起,形成最终的可执行文件,链接的时候还会涉及到静态库和动态库等问题。

2.gcc编译程序

在 Linux 下使用 GCC 编译器编译单个文件十分简单,直接使用 gcc 命令后面加上要编译的 C 语言的源文件,GCC 会自动生成文件名为 a.out 的可执行文件(也可以通过参数 -o 指定生成的文件名),也就是通过一个简单的命令上边提到的4个步骤就全部执行完毕了。

代码如下:

gcc test.c -o app  

即gcc C语言源文件 -o 指定生成的文件名

3.gcc常用参数

常用的一些gcc参数, 这些参数在 gcc命令中没有位置要求,只需要编译程序的时候将需要的参数指定出来即可。

-c 编译、汇编指定的源文件,但是不进行链接 -o [file1] [file2] / [file2] -o [file1] 将文件 file2 编译成文件 file1

//指定生成的文件名
gcc test.c -o app

-I directory (大写的i) 指定 include 包含文件的搜索目录

如果在程序中包含了一些头文件, 但是包含的一些头文件在程序预处理的时候因为找不到无法被展开,导致程序编译失败,这时候我们可以在gcc命令中添加 -I参数重新指定要引用的头文件路径, 保证编译顺利完成。

# -I, 指定头文件目录
$ tree
.
├── add.c
├── div.c
├── include
│   └── head.h
├── main.c
├── mult.c
└── sub.c
​
# 编译当前目录中的所有源文件,得到可执行程序
$ gcc *.c -o calc
main.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.

通过编译得到的错误信息可以知道, 源文件中包含的头文件无法被找到。通过提供的目录结构可以得知头文件 head.h 在 include 目录中,因此可以在编译的时候重新指定头文件位置,具体操作如下:

# 可以在编译的时候重新指定头文件位置 -I 头文件目录
$ gcc *.c -o calc -I ./include

-g 在编译的时候,生成调试信息,该程序可以被调试器调试

-D 在程序编译的时候,指定一个宏

在程序中我们可以使用宏定义一个常量, 也可以通过宏控制某段代码是否能够被执行。在下面这段程序中第8行判断是否定义了一个叫做 DEBUG的宏, 如果没有定义第9行代码就不会被执行了, 通过阅读代码能够知道这个宏是没有在程序中被定义的。

// test.c
#include <stdio.h>
#define NUMBER  3
​
int main()
{
    int a = 10;
#ifdef DEBUG
    printf("我是一个程序猿, 我不会爬树...\n");
#endif
    for(int i=0; i<NUMBER; ++i)
    {
        printf("hello, GCC!!!\n");
    }
    return 0;
}

如果不想在程序中定义这个宏,但是又想它存在,通过gcc的参数-D便可以实现,编译器会认为参数后边指定的宏在程序中是存在的

gcc test.c -o app -D DEBUG

-D 参数的应用场景: 在发布程序的时候, 一般都会要求将程序中所有的log输出去掉, 如果不去掉会影响程序的执行效率,很显然删除这些打印log的源代码是一件很麻烦的事情,解决方案是这样的:

将所有的打印log的代码都写到一个宏判定中, 可以模仿上边的例子
    在编译程序的时候指定 -D 就会有log输出
    在编译程序的时候不指定 -D, log就不会输出

-l 在程序编译的时候,指定使用的库 -L 指定编译的时候,搜索的库的路径。 -fPIC/fpic 生成与位置无关的代码 -shared 生成共享目标文件。通常用在建立共享库时 -std 指定C方言,如:-std=c99,gcc默认的方言是GNU C

4.多文件编译

gcc可以自动编译连接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中

首先可以将程序编译之前的代码准备出来,此处,例如,一个项目包含3个文件,文件string.h,string.c中有一个函数用来计算字符串的长度,而在main函数中,调用这个函数将计算的结果显示出来

然后是编译运行,因为头文件是包含在源文件中的,因此在使用gcc编译程序的时候不需要指定头文件的名字(在头文件无法被找到的时候需要使用参数-I指定具体的路径而不是名字),可以通过一个gcc命令来将多个源文件编译并且生成可以执行的程序,也可以分部完成

//直接链接生成可执行程序
gcc string.c main.c -o test
//或者
gcc *.c -o test
5.gcc和g++
关于对gcc和g++很多人的理解都是比较片面的或者是对二者的理解有一些误区,下边从三个方面介绍一下二者的区别:

在代码编译阶段(第二个阶段):
    后缀为 .c 的,gcc 把它当作是C程序,而 g++ 当作是 C++ 程序
    后缀为.cpp的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
    g++会调用gcc,对于C++代码,两者是等价的, 也就是说 gcc 和 g++ 都可以编译 C/C++代码

在链接阶段(最后一个阶段):
    gcc 和 g++ 都可以自动链接到标准C库
    g++ 可以自动链接到标准C++库, gcc如果要链接到标准C++库需要加参数 -lstdc++

关于 __cplusplus宏的定义

    g++ 会自动定义__cplusplus宏,但是这个不影响它去编译C程序

    gcc 需要根据文件后缀判断是否需要定义 __cplusplus 宏 (规则参考第一条)

综上所述:

不管是 gcc 还是 g++ 都可以编译 C 程序,编译程序的规则和参数都相同
g++可以直接编译C++程序, gcc 编译 C++程序需要添加额外参数 -lstdc++
不管是 gcc 还是 g++ 都可以定义 __cplusplus宏

代码示例:

#编译C程序
gcc test.c -o test
g++ test.c -o test

#编译C++程序
g++ test.cpp -o test
gcc test.cpp -lstdc++ -o test

二、linux静态库和动态库

项目中使用库函数一般有两个目的

1.为了程序更加简洁不需要在项目中维护太多的源文件

2.另一方面是为了源代码保密,因为不是所有人都想把自己编写的代码开源出来

拿到库文件(静态库和动态库)之后,要想使用还必须有这些库中提供的api函数的声明,也就是头文件,将这些添加到项目即可

1.静态库
在Linux中静态库由程序 ar 生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:

在Linux中静态库以lib作为前缀, 以.a作为后缀, 中间是库的名字自己指定即可, 即: libxxx.a
1.1 生成静态链接库

生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。

使用ar工具创建静态库的时候需要三个参数:

参数c:创建一个库,不管库是否存在,都将创建。 参数s:创建目标文件索引,这在创建较大的库时能加快时间。 参数r:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。

生成静态链接库的具体步骤如下:

1)需要将源文件进行汇编,得到.o文件,需要使用参数-c

#执行如下操作,默认生成二进制的.o文件
#-c 参数位置没有要求
$ gcc 源文件(*.c) -c

2)将得到的.o进行打包,得到静态库

ar rcs 静态库的名字(libXX.a) 原材料(.o)

3)发布静态库

#发布静态库
		 1.提供头文件 *.h
		 2.提供制作出来的静态库libXX.a
1.2静态库制作举例
1.2.1 准备测试程序
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│   └── head.h
├── main.c
├── mult.c
└── sub.c
1.2.2生成静态库

第一步,将源文件add.c, div.c, mult.c,sub.c进行汇编,得到二进制目标文件,add.o,div.o,mult.o,sub.o;

main.c是对接口的测试程序,制作库的时候不需要将main.c算进去

#1.生成.o
$ gcc add.c div.c mult.c sub.c -c
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.
#提示头文件找不到,添加参数-I重新头文件路径即可
$ gcc add.c div.c mult.c sub.c -c -I ./include/

#查看目标文件是否已经生成
$ tree
.
├── add.c
├── add.o            # 目标文件
├── div.c
├── div.o            # 目标文件
├── include
│   └── head.h
├── main.c
├── mult.c
├── mult.o           # 目标文件
├── sub.c
└── sub.o            # 目标文件

第二步,将生成的目标文件通过ar工具打包生成静态库

#2.将生成的目标文件.o打包成静态库
$ ar rcs libcalc.a add.o div.o mult.o sub.o  #若.o文件都在同一个目录中可以写为*.o

#查看目录中的文件
$ tree

.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│   └── `head.h  ===> 和静态库一并发布
├── `libcalc.a   ===> 生成的静态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o

第三步:将生成的静态库libcalc.a和库对应的头文件head.h一并发布给使用者即可

# 3. 发布静态库
	1. head.h    => 函数声明
	2. libcalc.a => 函数定义(二进制格式)
1.3静态库的使用

我们得到一个可以用的静态库之后,需要将其放到一个目录中,然后根据得到的头文件编写出测试代码,对静态库中的函数进行调用

# 1. 首先拿到了发布的静态库
	`head.h` 和 `libcalc.a`
	
# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h          # 函数声明
├── libcalc.a       # 函数定义(二进制格式)
└── main.c          # 函数测试

编译测试程序,得到可以执行的文件

# 3. 编译测试程序 main.c
$ gcc main.c -o app
/tmp/ccR7Fk49.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status

上述错误分析:

编译的源文件中包含了头文件 head.h, 这个头文件中声明的函数对应的定义(也就是函数体实现)在静态库中,程序在编译的时候没有找到函数实现,因此提示 undefined reference to xxxx。

解决方案:在编译的时将静态库的路径和名字都指定出来

-L: 指定库所在的目录(相对或者绝对路径) -l: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字

#4.编译的时候指定库信息
		-L:指定库所在的目录(相对或者绝对路径)
		-l:指定库的名字,掐头(lib)去尾(.a) ==> calc

#-L -l,参数和参数值之间可以有空格,也可以没有  -L./ -lcalc
$ gcc main.c -o app -L ./ -l calc
#查看目录信息,发现可执行顺序已经生成
$tree
.
├── app   		# 生成的可执行程序
├── head.h
├── libcalc.a
└── main.c
2.动态库

动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态链接库也可称之为共享库

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。

关于动态库的命名规则如下:

在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so

2.1 生成动态链接库

生成动态链接库是直接使用gcc命令,并且需要添加 -fPIC(-fpic)以及-shared参数。

-fPIC(-fpic)参数的作用是 使得gcc生成的代码是与位置无效的,也就是使用相对位置

-shared参数 的作用是告诉编译器生成一个动态链接库。

具体步骤如下:

1)将源文件进行汇编操作,需要使用参数-c,还需要添加额外参数 -fpic / -fPIC

#得到若干个 .o文件
$ gcc 源文件(*.c) -c -fpic

2)将得到的.o文件打包成动态库,还是使用gcc,使用参数-shared指定生成动态库(位置无要求)

gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libXXX.so)

3)发布动态库和头文件

#发布
		1.提供头文件:xxx.h
		2.提供动态库:libXX.so
2.2动态库制作

要制作的代码目录如下:

# 举例, 示例目录如下:
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│   └── head.h
├── main.c
├── mult.c
└── sub.c

第一步: 使用gcc将源文件进行汇编(参数-c), 生成与位置无关的目标文件, 需要使用参数 -fpic或者-fPIC

# 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC
$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/

# 查看目录文件信息, 检查是否生成了目标文件
$ tree
.
├── add.c
├── add.o                # 生成的目标文件
├── div.c
├── div.o                # 生成的目标文件
├── include
│   └── head.h
├── main.c
├── mult.c
├── mult.o               # 生成的目标文件
├── sub.c
└── sub.o                # 生成的目标文件

第二步: 使用gcc将得到的目标文件打包生成动态库, 需要使用参数 -shared

# 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared
$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so  

# 检查目录中是否生成了动态库
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│   └── `head.h   ===> 和动态库一起发布
├── `libcalc.so   ===> 生成的动态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o

第三步:发布生成的动态库和相关的头文件

# 3. 发布库文件和头文件
	1. head.h
	2. libcalc.so
2.3动态库的使用

当我们得到了一个可用的动态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对动态库中的函数进行调用。

# 1. 拿到发布的动态库
	`head.h   libcalc.so
# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用
	`main.c`
# 示例目录:
.
├── head.h          ==> 函数声明
├── libcalc.so      ==> 函数定义
└── main.c          ==> 函数测试

编译测试程序

# 3. 编译测试程序
$ gcc main.c -o app
/tmp/ccwlUpVy.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status

错误原因:

和使用静态库一样, 在编译的时候需要指定库相关的信息: 库的路径 -L和 库的名字 -l

添加库信息相关参数,重新编译测试代码:

gcc main.c -o app -L./ -lcalc

# 查看是否生成了可执行程序
$ tree
.
├── app 			# 生成的可执行程序
├── head.h
├── libcalc.so
└── main.c

# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
$ ./app 
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

关于整个操作过程的报告:

gcc通过指定的动态库信息生成了可执行程序, 但是可执行程序运行却提示无法加载到动态库。

2.4解决动态库无法加载问题
2.4.1库的工作原理

1)静态库如何被加载

在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。

2)动态库如何被加载

在程序编译的最后一个阶段也就是链接阶段: 在gcc命令中虽然指定了库路径(使用参数 -L ), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在。 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字

可执行程序被执行起来之后: 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息 当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载 动态库的检测和内存加载操作都是由动态连接器来完成的

2.4.2 动态链接器

动态链接器是一个独立于应用程序的进程, 属于操作系统, 当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L指定的路径。

那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:

可执行文件内部的 DT_RPATH 段

系统的环境变量 LD_LIBRARY_PATH

系统动态库的缓存文件 /etc/ld.so.cache

存储动态库/静态库的系统目录 /lib/, /usr/lib等

按照以上四个顺序, 依次搜索, 找到之后结束遍历, 最终还是没找到, 动态连接器就会提示动态库找不到的错误信息。

2.4.3解决方案

可执行程序生成之后, 根据动态链接器的搜索路径, 我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。

方案一:将库路径添加到环境变量LD_LIBRARY_PATH中

1)找到相关的配置文件

用户级别:~/.bashrc ------->设置对当前用户有效

系统级别:/etc/profile ------>设置对所有用户有效

2)使用vim打开配置文件,在文件最后添加一句话

#自己写入路径
export LD_LIBRARY_PATH = $LD_LIBRARY_PATH : 动态库的绝对路径
#例子:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xxl/temp/calc/test

3)让修改的配置文件生效

修改了用户级别的配置文件, 关闭当前终端, 打开一个新的终端配置就生效了

修改了系统级别的配置文件, 注销或关闭系统, 再开机配置就生效了

不想执行上边的操作, 可以执行一个命令让配置重新被加载

# 修改的是哪一个就执行对应的那个命令
# source 可以简写为一个 . , 作用是让文件内容被重新加载
$ source ~/.bashrc          (. ~/.bashrc)
$ source /etc/profile       (. /etc/profile)

方案二:更新 /etc/ld.so.cache文件

1)找到动态库所在的绝对路径(不包括库的名字)比如:/home/xxl/temp/calc/test/

2)使用vim修改 /etc/ld.so.conf 这个文件,将上边的路径添加到文件中(独自占一行)

# 1. 打开文件
$ sudo vim /etc/ld.so.conf

# 2. 添加动态库路径, 并保存退出

3)更新 /etc/ld.so.conf中的数据到 /etc/ld.so.cache 中

#必须使用管理员权限执行执行这个命令
$ sudo ldconfig
2.4.4验证

在启动可执行程序之前, 或者在设置了动态库路径之后, 我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库, 这个命令叫做 ldd

#语法:
$ ldd 可执行程序名

#举例:
$ ldd app 
	linux-vdso.so.1 =>  (0x00007ffe8fbd6000)
    libcalc.so => /home/robin/Linux/3Day/calc/test/libcalc.so (0x00007f5d85dd4000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d85a0a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5d85fd6000)  ==> 动态链接器, 操作系统提供
3.优缺点
3.1 静态库

优点:

静态库被打包到应用程序中加载速度快 发布程序无需提供静态库,移植方便

缺点:

相同的库文件数据可能在内存中被加载多份, 消耗系统资源,浪费内存 库文件更新需要重新编译项目文件, 生成新的可执行程序, 浪费时间。

3.2动态库

优点:

可实现不同进程间的资源共享 动态库升级简单, 只需要替换库文件, 无需重新编译应用程序 程序猿可以控制何时加载动态库, 不调用库函数动态库不会被加载

缺点:

加载速度比静态库慢, 以现在计算机的性能可以忽略 发布程序需要提供依赖的动态库

 


http://www.kler.cn/a/441122.html

相关文章:

  • 探索AI(chatgpt、文心一言、kimi等)提示词的奥秘
  • 跨境数据传输问题常见解决方式
  • 实战技巧:如何快速提高网站的收录比例?
  • QPS 值是怎样进行计算和应用的
  • 供应链系统设计-供应链中台系统设计(十)- 清结算中心概念片篇
  • MyBatis 框架:简化 Java 数据持久化的利器
  • 网络数据包分析
  • uniapp小程序的锚点定位(将页面滚动到目标位置)
  • linux下操作es及kibana的操作记录
  • OpenCV的简单练习
  • 性能评估工具之lmbench
  • cuda附加到python进程(vscode)
  • 记录linux websocket握手时间过长问题
  • 基于python绘制数据表(上)
  • Spark优化----Spark 数据倾斜
  • Android Room 数据库使用详解
  • 【使用PyQt5和YOLOv11开发电脑屏幕区域的实时分类GUI】——选择检测区域
  • CTFHUB靶场关于SSRF保姆级攻略
  • 大模型呼出机器人有哪些功能特点?
  • 多维高斯分布
  • docker安装Redis、docker使用Redis、docker离线安装redis、Redis离线安装
  • 代码随想录算法训练营第三十五天|01背包理论基础|卡码网46.携带研究材料|LC416.分割等和子集
  • 深入理解STL list erase
  • ThreadLocal数据结构、内存泄漏分析
  • Maven 打包(system jar 和微服务父子项目)
  • ios系统冷知识