一文读懂C语言动静态库

Linux动静态库


动态库: 在Linux中,.so 后缀为动态库;在Windows中,.dll 后缀为动态库。
静态库: 在Linux 中,.a 后缀为静态库;在Windows 中,.lib 后缀为静态库。

查看可执行文件所依赖的动态库


ldd /usr/local/bin/uwsgi

查看文件详细信息


file /usr/local/bin/uwsgi

Linux链接静态库


gcc [源文件] -o [输出文件名] -static    

Linux(mini)操作系统安装系统静态库


# C静态库
sudo yum install -y glibc-static
# CPP静态库
sudo yum install -y libstdc++-static

1. 制作静态库

  • 新建cacl.h文件

#include <stdio.h>

int Add(int x, int y);
int Sub(int x, int y);
int Mul(int x, int y);
int Div(int x, int y);

  • 新建 cacl.c文件:

#include "cacl.h"

int Add(int x, int y)
{
    return x + y;
}
int Sub(int x, int y)
{
    return x - y;
}
int Mul(int x, int y)
{
    return x * y;
}
int Div(int x, int y)
{
    if (y == 0){
        printf("除0错误\n");
	return -1;
    }
    return x / y;
}

  • 将函数实现cacl.c文件编译成目标文件cacl.o

gcc -c cacl.c -o cacl.o

  • 将生成的目标文件打包成.a文件(库)

ar rc libmylib.a cacl.o

  • 将库打包成目录文件给其他人使用

mkdir -p lib/include/
mkdir -p lib/mylib/
cp cacl.h lib/include/
cp libmylib.a lib/mylib

  • 链接静态库命令及参数

gcc <源文件>  -I 头文件路径 -L 库文件路径 -l 库名1 -l 库名2 ...

-I选项:表示指定搜索头文件的路径。如果不指定默认会在路径/usr/include或者该进程路径下查找

-L选项:表示指定搜索库文件的路径。不指定默认在/lib64路径下搜索。

-l选项:由于库文件路径下可能存在多个库文件,所以要明确指定库名称。gcc/g++ 默认找的就是 stdc/stdc++库

- 库名1: 指定名称可以去掉头部lib及尾部.so即可

  • 执行命令,并运行程序查看结果

目录结构:
lib/
| -- include
    ` -- cacl.h
| -- mylib
    ` -- libmycacl.so
cacl.c

生成可执行文件执行命令:
gcc cacl.c -o myexe -I ./lib/include/ -L ./lib/mylib/ -l mycacl

执行:
./myexe

  • 安装静态库

系统编译器此时在默认路径下就可以找到,我们称这一步为安装库。注意:安装到系统需要提升用户权限。

sudo cp ./lib/include/cacl.h /usr/include/
sudo cp ./lib/mylib/libmylib.a /lib64/

  • 使用静态库

我们的库不是系统级别的库,还是一个第三方库,所以仍然需要指明链接的库的名字
gcc cacl.c -o myexe -l mylib

  • 删除库

注意: 将自己写的文件安装到系统可能会导致系统环境被污染, 用完后记得手动删除

rm -rf /usr/include/cacl.h
rm -rf /lib64/libmylib.a

2. 制作动态库

  • cacl.h文件
#include <stdio.h>

int Add(int x, int y);
int Sub(int x, int y);
int Mul(int x, int y);
int Div(int x, int y);
  • cacl.c文件:
#include "cacl.h"

int Add(int x, int y)
{
    return x + y;
}
int Sub(int x, int y)
{
    return x - y;
}
int Mul(int x, int y)
{
    return x * y;
}
int Div(int x, int y)
{
    if (y == 0){
        printf("除0错误\n");
	return -1;
    }
    return x / y;
}
  • test.c文件
#include "cacl.h"

int main()
{
    printf("1 + 1 = %d\n", Add(1, 1));
    return 0;
}
2.1. 将函数实现cacl.c文件编译成目标文件cacl.o
  • 执行命令: 此命令执行完毕后会生成cacl.o文件
gcc -fPIC -c calc.c

- 注: -fPIC: 选项来产生与地址无关码
2.2 将生成的目标文件.o打包成动态库。使用 -shared 选项将目标文件打包成动态库。
  • 执行命令: 此命令执行完毕后会生成libmycacl.so文件
gcc -shared -o libmycacl.so cacl.o
2.3 将头文件 + 库打包给别人(具体看静态库中的制作步骤)
  • 项目目录结构:
lib/
| -- include
    ` -- cacl.h
| -- mylib
    ` -- libmycacl.so
test.c
2.4 使用动态库, 像使用静态库一样使用动态库(指定路径及库名)
  • 命令执行
gcc test.c -o myexe -I ./lib/include/ -L ./lib/mylib/ -l mycacl
  • 运行可执行文件提示信息:
./myexe: error while loading shared libraries: libmycacl.so: cannot open shared object file: No such file or directory
  • 错误解释
1. 上面提示:无法打开动态库目标文件。可是我们在编译的时候明确指定了库文件路径以及要连接的库文件名称。

2. 这是因为当前只告诉了编译器gcc动态库的位置,一旦当程序myexe形成以后,就和编译器没关系了;当你把执行./myexe,就意味着程序要加载到内存,这里出现的问题恰好是加载的时候没有找到库。因此,除了编译生成动态库之外,还需要告知系统的加载器动态库的位置。
  • 为什么动态链接的过程中,程序加载到内存还要找到动态库?

- 动态链接的过程中,程序加载到内存后还需要找到动态库,主要是因为在链接阶段,动态链接库的代码并没有被直接包含在可执行文件中,而只是在程序需要时才去动态加载。因此,程序加载到内存后需要知道动态库的位置,以便在需要时能够正确地加载并调用其中的函数。这也就是为什么动态链接可以减小可执行文件的体积,同时也使得动态链接库可以被共享!

- 而对于静态链接,在gcc链接时,内容已经被拷贝到可执行文件中,而不需要在运行时再去查找和加载外部的静态库文件(运行时不再依赖静态库)。

3. 解决程序加载到内存无法找到动态库(四种方法)

  • 方法1:

在Linux中,加载器会根据默认的搜索路径(如/lib64 or /usr/lib64等)来查找动态库,
因此可以把动态库放在/lib64或者/usr/lib64路径下。(永久有效且最常用)

sudo cp lib/mylib/libmycacl.so /lib64

再次执行: ./myexe

  • 方法2:

在系统默认的库路径/lib64 or /usr/lib64下建立软连接(创建快捷方式)
sudo ln -s /var/www/learn/c_learn/dynamic_c/lib/mylib/libmycacl.so /lib64/libmycacl.so

再次执行:
./myexe

  • 方法3:

加载器也会根据环境变量 LD_LIBRARY_PATH 来搜索库文件路径。这个路径是搜索用户自定义的库文件路径。因此我们可以添加动态库路径至 LD_LIBRARY_PATH 环境变量中。注意:这种方法一旦重启xshell,原来添加的所有内容都会消失(临时)

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/var/www/learn/c_learn/dynamic_c/lib/mylib/

再次执行:
./myexe

  • 方法4:

可以通过配置/etc/ld.so.conf.d/目录下的配置文件来指定加载器在程序加载时查找动态库的路径。(永久有效)

- 在/etc/ld.so.conf.d/目录下创建一个新的配置文件(通常以.conf结尾),然后在其中添加需要动态链接器搜索的库文件路径,每个路径占据一行。

echo /var/www/learn/c_learn/dynamic_c/lib/mylib/ > tmp.conf
sudo mv tmp.conf /etc/ld.so.conf.d/

sudo ldconfig

再次执行:
./myexe

4. 动态库是如何做到被所有进程所共享


1. 在程序执行过程中,当需要调用动态库中的函数或者访问其中的数据时,操作系统会在磁盘上找到所需要的动态库文件(文件系统),然后加载器会负责将动态库文件直接加载到物理内存,并通过页表映射到进程地址空间的共享区中(专门给动态库用的区域)。

3. 当程序在执行过程中需要调用动态库中的函数时,程序会从自身的代码区跳转到共享区中动态库的代码位置执行相应的函数。一旦函数执行完毕,程序会再次跳转回自身的代码区,继续执行程序的其他部分。所以这样的话,从此我们执行的任何代码,都是在我们的进程地址空间中进行执行的!

4. 除此之外,一个程序可能会链接多个动态库,那么操作系统必定要对这些动态库进行管理,所以要请出管理六字真言:先描述,后组织。所以,操作系统非常清楚所有动态库的加载情况。因此,当未来第二个进程还需要用到这个同样的共享库的时候,就不会再去磁盘重新将动态库文件加载到内存,而是直接将已加载的动态库通过页表映射到新进程的地址空间中,避免重复加载同一份动态库文件到内存中,提高了系统的效率和资源利用率,这就是为什么动态库在内存中只需要一份就够了!

4. 注意:如果这个动态库里面有一个全局变量,那么当一个进程中对这个变量进行了修改以后,根据往期学习进程相关知识,为了确保进程的独立性,必然会发生写时拷贝!


5. -fPIC与地址无关码


在Linux中,你可以使用objdump命令来查看可执行文件中的程序信息

objdump -d <可执行文件>   # 查看可执行文件的机器指令
objdump -s <可执行文件>   # 查看可执行文件的数据
objdump -t <可执行文件>   # 查看可执行文件的符号表

1. 一个源程序被编译成可执行文件后,这个可执行文件通常包含了程序的机器指令、数据、符号表等信息。

2. 注:可执行文件中的指令和数据是按照程序加载进程地址空间的偏移量编码的,这个偏移量描述了程序加载到进程地址空间起始位置的偏移量。

3. 而当可执行文件运行起来时,操作系统会为该程序创建一个进程,并为其分配进程地址空间,这个进程地址空间包含了程序的机器指令、数据等其他相关信息;

4. 为了让程序能够正确地在实际的硬件上执行,操作系统还要通过页表(页表是操作系统用来管理虚拟地址到物理地址映射的数据结构)将进程地址空间中的虚拟地址映射到物理内存。

5. 接着CPU就开始读取可执行文件上的指令,它实际上是在读取虚拟地址。而在涉及函数调用时,机器指令中通常会包含一个跳转指令(例如 call 指令),它告诉CPU要跳转到哪个地址执行代码。这个地址通常是一个相对地址(相对于当前指令的地址)

- 5.1 相对地址跳转:如果跳转地址是相对地址,CPU 会将当前指令的位置与跳转地址相加,得到实际的目标地址。然后,CPU 将程序计数器PC(存储下一个目标指令的地址)设置为目标地址,从而执行跳转。

- 5.2 绝对地址跳转:如果跳转地址是绝对地址,CPU 会直接将程序计数器(PC)设置为跳转地址,从而执行跳转。

6. 制作动态库时需要加上-fPIC,即需要设置与位置无关码


1. 一个源程序被编译成可执行文件后,这个可执行文件通常包含了程序的机器指令等,当这个指令时调用动态库的一个函数,随之动态库也就需要加载到物理内存,然后通过页表映射到进程地址空间的共享区。

2. 因为调用动态库函数的机器指令的地址(虚拟地址)是在编译时确定的,但动态库通过页表可能会被映射的位置是不确定的(动态)。那么这样加载到不同位置的话,进程就无法正确调用其中的函数或访问其中的变量,因为它们的地址已经发生了偏移。

3. 因此,我们需要保证动态库能够在虚拟内存中的任意一个位置加载。因此,程序在链接动态库函数时,是通过 动态库起始地址 + 所链接函数偏移量 的方式进行链接访问的,而这个偏移量就是 fPIC 与位置无关码。

4. 因此,通过使用位置无关码选项,可以确保动态库在被加载到任何地址时都能正确执行。注意:操作系统会管理加载的动态库,因此动态库的起始地址操作系统是知道的!

7. 动静态库对比


1. 动态库在程序运行时会被动态加载到内存中,只有一份,并且可以被多个进程共享,这样可以大大减少内存占用,特别是当多个进程同时使用相同的动态库时。

2. 与此不同的是,静态库在编译链接时会被整体地拷贝到可执行文件中,导致内存占用增加。

8. GCC编译常用选项

8.1 编译选项

1) -O选项

-O0: 无优化。编译器不会进行任何优化,生成的代码与源代码几乎完全相同。这个级别用于调试和开发阶段,方便进行代码调试和分析。
-O1: 基本优化。编译器进行一些基本的优化,例如去除一些无效的代码、缓存优化等。生成的代码比无优化模式稍微高效,但编译时间较短。
-O2: 中等优化。编译器进行更多的优化,例如函数内联、循环展开、变量替换等。生成的代码比基本优化模式更高效,但编译时间可能更长一些。
-O3: 最高优化。编译器进行较为激进的优化,例如高级函数内联、循环展开、更复杂的指令调度等。生成的代码非常高效,但编译时间可能会更长。

2)-Wall:开启所有警告信息。它会显示一系列潜在的代码问题,如未使用的变量、未定义的函数、隐式函数声明等。

3)-Werror:将所有警告视为错误,编译时任何警告将导致编译失败。这有助于确保代码的严格规范性和质量。

4) -g:生成调试信息,使得调试器可以定位到源代码的具体位置。调试信息包括变量名、函数名和行号等。


-g: 默认级别的调试信息。生成的可执行文件中会包含基本的调试信息,例如变量名、源文件名和行号等。这是平时开发和调试时常用的级别。
-g1 或 -ggdb1: 较低级别的调试信息。与默认级别相比,它会额外包含一些调试信息,例如宏定义、内联函数等。适用于需要更详细调试信息的场景。
-g2 或 -ggdb2: 中等级别的调试信息。在 -g1 级别的基础上,进一步增加了一些调试信息,例如每个源代码的调试信息、全局变量等。适用于更复杂项目的调试需求。
-g3 或 -ggdb3: 最高级别的调试信息。包含了最详细的调试信息,适用于对可执行文件进行深度调试和分析。

5) -I :指定头文件的搜索路径, 是头文件所在的目录路径。例如,-I/usr/include 指定了系统头文件所在的目录。

6)–verbose:–verbose 是一个通用选项,可以用于多种编译器和工具。在编译过程中,使用–verbose可以显示编译器的详细输出,包括正在编译的文件、所使用的编译选项、预处理器的定义等。这有助于了解编译过程中的各个环节和相关信息。

7)-std= 是 GCC 编译器的选项之一,用于指定要遵循的 C 或 C++ 标准。

以下是一些常见的 -std=<standard> 参数值:
-std=c89 或 -ansi:使用 C89(也称为 ANSI C)标准。
-std=c99:使用 C99 标准。
-std=c11:使用 C11 标准。
-std=gnu89:使用 GNU C89 扩展,允许使用一些非标准的特性。
-std=gnu99:使用 GNU C99 扩展。
-std=gnu11:使用 GNU C11 扩展。
对于 C++ 代码,可以使用以下参数值:
-std=c++98:使用 C++98 标准。
-std=c++03:使用 C++03 标准。
-std=c++11:使用 C++11 标准。
-std=c++14:使用 C++14 标准。
-std=c++17:使用 C++17 标准。
-std=c++20:使用 C++20 标准。

请根据您的代码和目标平台的要求选择适当的标准。请注意,不同的标准可能支持不同的语言特性和功能,因此选择适当的标准能够确保代码在不同编译环境中的兼容性。

8)-D:定义预处理器宏。在 CFLAGS 中使用时,可以通过 -D 选项定义指定的宏。在 LDFLAGS 中使用时,它没有直接的作用。

9)-shared 和 -fPIC 是 GCC 编译器在编译共享库时常用的选项。

请注意,-shared 和 -fPIC 选项通常配合使用,以生成可供动态链接的共享库。

-shared 选项用于告诉编译器生成一个共享库(动态链接库)而不是可执行文件。
-fPIC 选项(Position Independent Code)用于生成位置无关代码,这种代码可以在内存中的任意位置执行,而不受限于特定的内存布局。这在共享库中特别重要,因为共享库需要在不同的进程空间中加载和执行。

在编译共享库时,通常的编译命令可能如下所示:
-gcc -shared -fPIC -o mylib.so mylib.c
-shared:生成一个共享库。
-fPIC:编译时生成位置无关代码。
-o mylib.so:指定生成的共享库文件名为 mylib.so。

mylib.c:源代码文件名,这里假设需要编译成共享库的源代码文件是 mylib.c。

8.2 常用的链接选项

1)-L :指定库文件的搜索路径, 是库文件所在的目录路径。例如,-L/usr/lib 指定了系统库文件所在的目录。

2)-l:指定要链接的库文件。 通常是库文件名(不包括前缀 lib 和后缀名),例如 -lmath 表示链接数学库。

3)-static:进行静态链接,将所需的库文件嵌入到可执行文件中,使得可执行文件独立于系统环境。

4)-shared:进行动态链接,生成共享库(动态链接库),这些库可以在运行时被多个可执行文件共享使用,减少重复代码的占用空间。

5)-Wl,:将 选项传递给链接器。例如,-Wl,-rpath,/usr/local/lib 指定运行时库搜索路径。

6)-Wl,–verbose:-Wl,–verbose 是用于GCC (GNU Compiler Collection)的链接器(ld)的选项。它会将 --verbose 选项传递给链接器,以显示链接过程的详细信息,包括库的搜索路径、所链接的库文件、链接的顺序等。

7)-export-dynamic:将所有符号导出,使得它们可被加载和链接到动态库中。

8)-Wl,-Bsymbolic:生成符号的绑定版本,以避免与其他库中的同名符号冲突。

9)-Wl,-rpath=:设置运行时库搜索路径。

10)-Wl,-E:将链接器参数 -E 传递给链接器。在链接过程中,参数 -E 的作用是保留所有未定义的符号,即使这些符号在最终的可执行文件中没有被使用。

11)-soname=,它用来指定共享库的 soname(Shared Object Name)。Soname 是共享库的版本标识符,用于在运行时动态链接器加载共享库时进行符号解析和版本匹配。

8.3 编译+链接 实例
  • 例1:
gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so <source_files>

解释:
- 上面命令使用 GCC 编译器和链接器来生成一个共享库文件,并指定其 soname 为 libmylib.so.1。
- 当一个可执行文件或其他共享库依赖于一个共享库时,它将使用共享库的 soname 来确定需要加载的库的版本。通过使用soname,可以实现在不修改可执行文件的情况下,更新共享库并保持向后兼容。
- 要使用 -soname=<name> 选项,可以将其包含在链接器命令中,指定共享库的名称作为 <name>
  • 例2:
CFLAGS="-I/root/openssl3.0.14/openssl-3.0.14/include" LDFLAGS="-L/mnt/lib -Wl,-rpath,/mnt/lib" gcc -o output input.c

解释:
- CFLAGS 是用于设置 C/C++ 编译器选项的环境变量。它可以用来指定编译过程中的各种选项,如优化级别、警告级别、头文件包含路径等。
- LDFLAGS 是用于设置链接器选项的环境变量。它可以用来指定链接过程中的各种选项,如库路径、库文件等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值