动态库和静态库

1.什么是动态库和静态库

我们在编写接口或者使用第三方应用时,都会打包或者引入.so或者.a文件。这个so就是动态库,.a文件就是静态库。

具体的,动态库和静态库是在C语言中用来组织和共享代码的两种方式。

静态库(Static Library)是一组预编译的对象文件的集合,它们被组合成一个单一的文件。当使用静态库时,链接器会将库中的代码和数据复制一份到最终的可执行文件中。这意味着可执行文件会包含所有需要的函数和数据,使得程序可以独立运行。静态库的文件扩展名通常为 .a(Unix-like系统)或 .lib(Windows系统)。

动态库(Dynamic Library)是一组目标文件的集合,它们在程序运行时被加载到内存中。当程序运行时,操作系统会加载动态库中的代码和数据,多个程序可以共享已加载的库,这样可以减少内存占用并方便对库文件进行更新。动态库的文件扩展名通常为 .so(Unix-like系统)或 .dll(Windows系统)。

动态库和静态库区别在于:

  1. 静态库的代码和数据会被复制到可执行文件中,程序独立运行;而动态库的代码和数据是在程序运行时加载到内存中,多个程序可以共享已加载的库。
  2. 静态库会增加可执行文件的大小,动态库不会影响可执行文件的大小。
  3. 静态库在程序编译时就已经链接到可执行文件中,而动态库则是在程序运行时动态加载的。

应用场景:

  • 静态库常用于需要独立运行且不需要频繁更新的程序,或者在无法依赖外部动态库的环境下使用。
  • 动态库常用于需要共享代码、减少内存占用或者便于库文件更新的场景。

我们知道,在编译c语言程序时,会生成.o的中间文件和最终可执行文件,其中".o"文件是编译器生成的目标文件,它包含了源代码文件编译后的机器代码和相关的符号表信息。".elf"文件是可执行和可链接格式(Executable and Linkable Format)文件,它包含了程序的可执行代码、数据、符号表和其他信息,用于在操作系统中执行程序。通常,".o"文件是编译后的中间文件,而".elf"文件是最终的可执行文件。而本质上看,静态库和动态库都是众多.o文件的集合。

2.如何编译

编译动态库和静态库的过程略有不同。以下是具体的步骤:

静态库

假设你有一个叫做hello.c的文件,你想把它编译成一个静态库。以下是步骤:

  1. 首先,你需要使用gcc编译hello.c,但是你需要使用-c标志来生成一个对象文件而不是一个完整的程序。这将生成一个hello.o文件。
gcc -c hello.c
  1. 然后,你使用ar命令来创建静态库。此命令会生成libhello.a文件。
ar rcs libhello.a hello.o

ar命令的rcs选项执行以下操作:

  • r:替换旧的对象文件(如果存在)。
  • c:创建新的库(如果不存在)。
  • s:创建一个对象文件索引(对于库的链接很重要)。

动态库

如果你有一个源文件hello.c,你想要编译成一个动态库。以下是步骤:

  1. 首先,你需要使用gcc编译hello.c,但是你需要使用-c标志来生成一个对象文件而不是一个完整的程序。这将生成一个hello.o文件。
gcc -c -fPIC hello.c

-fPIC标志告诉gcc生成位置无关代码(PIC),这是创建共享库所必需的。

  1. 然后,你可以使用gcc命令和-shared选项来创建动态库。这将生成一个``共享库。
gcc -shared -o  hello.o

-shared选项告诉链接器创建一个共享库。

在链接时,需要使用-L-l选项指定库的路径和名称。

例如,如果你的程序名为main.c,并且你想链接之前创建的静态库或动态库,你可以这样做:

对于静态库:

gcc main.c -L. -lhello -o main

对于动态库:

gcc main.c -L. -lhello -o main

请注意,-L.告诉链接器在当前目录中查找库,-lhello告诉它链接到libhello库(无论是.a还是.so)。程序的输出在-o后指定,这里是main

特别注意的是,动态库需要加上-fPIC, 在cmake里动态库关键字为share,静态库为static

另外在链接so库的时候,编译器是如何通过-l后面的名字寻找相对应的so库呢,动态链接库的有一定的寻找顺序:

  • rpath 指定的目录;
  • 环境变量 LD_LIBRARY_PATH 指定的目录;
  • runpath 指定的目录;
  • /etc/ld.so.cache 缓存文件,通常包含 /etc/ld.so.conf 文件编译出的二进制俩别哦(比如 CentOS 上,该文件会使用 include 从而使用 ld.so.conf.d 目录下面所有的 *.conf 文件,这些都会缓存在 ld.so.cache 中)
  • 系统默认路径,比如 /lib,/usr/lib。

在编译时若使用 -z nodefaultlib 选项编译,则会跳过 4 和 5。至于 runpath,和 rpath 类似,都是二进制(ELF)文件的动态 section 属性(分别为 DT_RUNPATH 和 DT_RPATH),唯一区别就是是否优先于 LD_LIBRARY_PATH 来查找

3.如何使用

动态库和静态库使用分为隐式使用和显示打开,其中隐式使用就是通过-l直接链接到主程序中,而显示使用时通过dlopen的方式在程序使用的时候打开so文件并获取通过dlsym相应的函数接口。

这里详细介绍下dlopen的方式。

#include <stdio.h>
#include <dlfcn.h>
#include <iostream>

// g++ dlopen_test.cpp -o test -ldl
int main(int argc, char* argv[]){
    void *handle;
    int (*hello)();

    // 打开动态库
    handle = dlopen("./", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    // 获取动态库中的函数指针
    *(void **)(&hello) = dlsym(handle, "hello_world_so");
    if (!hello) {
        fprintf(stderr, "%s\n", dlerror());
        dlclose(handle);
        return 1;
    }

    // 调用动态库中的函数
    (*hello)();


    // 关闭动态库
    dlclose(handle);

通过dlopen的方式,编译并不会检查so的正确性,只会在运行时加载so到内存中,并获取相应的函数。

4.查看动态库和静态库信息

通常我们需要查看打包的动态库和静态库是否符合要求,链接是否正常以及是否包含相应的函数符号等,有一些命令可以帮助我们更可了解它们。

  1. patchelf:该命令可以用来修改ELF可执行文件的动态链接器(即解释器),RPATH,或者动态符号表。主要用于处理库路径问题,有时候运行某个程序时,系统找不到其依赖的库,可以通过这个命令来修改。

    使用示例:patchelf --set-rpath /path/to/lib your_program

    这个命令将程序二进制文件中的rpath设置为/path/to/lib

  2. nm:这个命令可以显示从目标文件的符号表中列出符号。默认情况下,nm会按照字母顺序列出符号。

    使用示例:nm /path/to/

    这个命令将列出动态库文件中的所有符号。

  3. ldd:这个命令可以用来查看可执行文件或者动态库所依赖的其他动态库文件。它会列出所有的依赖库以及这些库的位置。

    使用示例:ldd /path/to/your_program

    这个命令将列出可执行文件your_program依赖的所有动态库的位置。

  4. objdump:这个命令可以显示一个或多个目标文件的信息。它可以显示二进制文件的详细信息,包括文件头,节信息,符号表等。

    使用示例:objdump -x /path/to/your_program

    这个命令将显示可执行文件your_program的详细信息。

  5. readelf:这是一个用于显示ELF格式文件信息的程序。它可以显示ELF文件的各种信息,包括文件头信息,节信息,段信息,符号表等。

    使用示例:readelf -a /path/to/your_program

    这个命令将显示可执行文件your_program的所有ELF信息。

请记住,每一个命令都有很多选项可以使用,这里只是提供了一些基本示例。一定要查阅对应命令的man手册以获取更详细的信息。