Linux共享库,静态库与相关系统调用,工具的使用总结

 
Category: Linux-Shell

写在前面

总结Unix/Linux操作系统的共享库/静态库部分, 以及一些系统调用.

参考Linux/UNIX系统编程手册41-42章.

测试程序均在Ubuntu下使用cc(gcc-9)运行成功.

  $ gcc -v
  Using built-in specs.
  COLLECT_GCC=gcc
  COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
  OFFLOAD_TARGET_NAMES=nvptx-none:hsa
  OFFLOAD_TARGET_DEFAULT=1
  Target: x86_64-linux-gnu
  Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-Av3uEd/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
  Thread model: posix
  gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

测试代码文件

这里我为了方便就用了C自带的宏命令, __FILE__用于显示当前程序的文件名, __func__用于显示当前执行的函数名.

主要结构就是:

prog调用mod*的函数, mod*中各自实现一个函数.

prog.c

#include <stdio.h>

void x1();
void x2();
void x3();

int main(void) {
    printf("%s-%s running...\n", __FILE__, __func__);
    x1();
    x2();
    x3();
    return 0;
}

mod1.c

#include <stdio.h>
void x1() { printf("%s-%s running...\n", __FILE__, __func__); }

mod2.c

#include <stdio.h>
void x2() { printf("%s-%s running...\n", __FILE__, __func__); }

mod3.c

#include <stdio.h>
void x3() { printf("%s-%s running...\n", __FILE__, __func__); }

注意: 后面会根据实际情况更改代码.

基本概念

构建程序的一种方式是简单地将每一个源文件编译成目标文件,然后将这些目标文件链接在一起组成一个可执行程序. 下面是两种常用的链接目标文件(.o)的方式, 包括静态库(归档文件)和共享库(动态链接库).

静态库

静态库也被称为归档文件(archive), 静态库能带来下列好处:

  1. 可以将一组经常被用到的目标文件组织进单个库文件,这样就可以使用它来构建多个可执行程序并且在构建各个应用程序的时候无需重新编译原来的源代码文件。
  2. 链接命令变得更加简单了。在链接命令行中只需要指定静态库的名称即可,而无需一个个地列出目标文件了。链接器知道如何搜素静态库并将可执行程序需要的对象抽取出来。

静态库实际上就是一个保存所有被添加到其中的目标文件的副本的文件.

一般来说, 静态库被命名为libname.a(Linux下). 后面会介绍创建静态库的具体方式(通过ar命令).

共享库(动态链接库)

共享库是一种将库函数打包成一个单元使之能够在运行时被多个进程共享的技术。这种技术能够节省磁盘空间和RAM.

根据惯例,共享库的前缀为lib,后缀为.so(表示shared object)

静态库实战

使用ar命令创建, 下面是几种常用参数:

  • r: (replace)替换, 用于创建静态库, 将目标文件插入到静态库并取代同名的目标文件.
  • d: (delete)删除, 删除静态库中的指定目标(模块).
  • t: (table)目录表, 输出静态库中所有的目标名称(目录表).
  • v: 与t合用, 用于输出详细信息.

示例: ar命令创建静态库

以上面的四个C源程序为例, 创建名为libstatic.a的静态库, 方法如下:

  1. 生成目标文件:
    cc -c -g mod*.c
    
  2. 创建静态库文件:
    ar r libstatic.a mod*.o
    # ar: creating libstatic.a
    
  3. (可选)删除目标文件:

    rm *.o
    
  4. 查看静态库文件:
    ar tv libstatic.a
    # rw-r--r-- 0/0   6112 Jan  1 08:00 1970 mod1.o
    # rw-r--r-- 0/0   6112 Jan  1 08:00 1970 mod2.o
    # rw-r--r-- 0/0   6112 Jan  1 08:00 1970 mod3.o
    
  5. 删除模块(对应的prog.c)也要相应操作(注释掉x3()的调用):
    ar d libstatic.a mod3.o
    

示例: 使用静态库

使用静态库链接可执行程序.

  1. 编译主程序:
    cc -c -g prog.c
    
  2. 通过静态库生成可执行程序: (下面三种方法都可)
    # cc -g -o prog_static prog.o libstatic.a
    cc -g -o prog_static prog.o -lstatic
    # cc -g -o prog_static prog.o -L. -lstatic # 指定静态库的搜索路径(默认是当前路径`.`)
    
  3. 执行: (prog.c中注释了x3())
    ./prog_static
    # prog.c-main running...
    # mod2.c-x1 running...
    # mod3.c-x2 running...
    

共享库实战

以上面的四个C源程序为例, 创建名为libshared.so的共享库, 并查看相关信息. 方法如下:

示例: 使用cc创建共享库

  1. 编译生成目标文件:

    -fPIC选项指定编译器应该生成位置独立(无关)的代码(Position-independent Code),这会改变编译器生成执行特定操作的代码的方式,包括访问全局、静态和外部变量,访问字符串常量,以及获取函数的地址。这些变更使得代码可以在运行时被放置在任意一个虚拟地址处。这一点对于共享库来讲是必需的,因为在链接的时候是无法知道共享库代码位于内存的何处的。

    一个共享库在运行时所处的内存位置依赖于很多因素,如

    • 加载这个库的程序已经占用的内存数量
    • 这个程序已经加载的其他共享库
    cc -g -c -fPIC -Wall mod*.c
    
  2. 链接生成共享库:
    cc -g -shared -o libshared.so mod*.o
    
  3. 在链接阶段将共享库的名称嵌入可执行文件中:
    cc -g -Wall -o main prog.c libshared.so
    

1+2可以一步到位:

cc -g -fPIC -Wall mod*.c -shared -o libshared.so

后续步骤: 动态链接

创建完之后还要进行一个步骤, 否则执行时会出现:

./main
# 报错:
# ./main: error while loading shared libraries: libshared.so: cannot open shared object file: No such file or directory

动态链接, 其实就是运行时解析内嵌的库名. 由动态链接器完成, 路径为/lib/ld-linux.so.2

$ /lib/ld-linux.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
			object we can handle
  --inhibit-cache       Do not use /etc/ld.so.cache
  --library-path PATH   use given PATH instead of content of the environment
			variable LD_LIBRARY_PATH
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
			in LIST
  --audit LIST          use objects named in LIST as auditors
  --preload LIST        preload objects named in LIST

动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件

很多共享库位于/lib/usr/lib中。之所以出现上面的错误消息是因为程序所需的库位于当前工作目录中,而不位于动态链接器搜索的标准目录清单中。

方法1: 指定环境变量LD_LIBRARY_PATH

通知动态链接器一个共享库位于一个非标准目录中的一种方法是将该目录添加到LD_LIBRARY_PATH环境变量中以分号分隔的目录列表中.

那么要想找到路径, 就要指定当前路径为动态链接库的搜索路径:

$ LD_LIBRARY_PATH=. ./main
prog.c-main running...
mod1.c-x1 running...
mod2.c-x2 running...

方法2: (生产环境常用)共享库移动到/usr/local/lib/

$ sudo cp libshared.so /usr/local/lib/libshared.so
$ ./main
prog.c-main running...
mod1.c-x1 running...
mod2.c-x2 running...

方法3: 将共享库目录列表插入可执行程序

在静态编辑阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。这种方式对于库位于一个固定的但不属于动态链接器搜索的标准位置的位置中时是非常有用的。

需要给链接器(linker)传入参数-rpath, 使用-Wl选项:

man cc:

-Wl,option
           Pass option as an option to the linker.  If option contains commas,
           it is split into multiple options at the commas.  You can use this
           syntax to pass an argument to the option.  For example,
           -Wl,-Map,output.map passes -Map output.map to the linker.  When
           using the GNU linker, you can also get the same effect with
           -Wl,-Map=output.map.

           NOTE: In Ubuntu 8.10 and later versions, for LDFLAGS, the option
           -Wl,-z,relro is used.  To disable, use -Wl,-z,norelro.

那么, 具体的方法就是: (注意, 是绝对路径)

$ cc -g -Wall -Wl,-rpath=/home/zorch/code/c_cpp_code/library-test -o main prog.c liba.so

为了测试, 删除之前放入/usr/local/lib/的共享库libshared.so.

  $ sudo rm /usr/local/lib/libshared.so

测试一下:

$ ./main
prog.c-main running...
mod1.c-x1 running...
mod2.c-x2 running...

共享库版本与别名(soname)

引子:

一般来讲,一个共享库相互连续的两个版本是相互兼容的,这意味着每个模块中的函数对外呈现出来的调用接口是一致的,并且函数的语义是等价的(即它们能取得同样的结果)。这种版本号不同但相互兼容的版本被称为共享库的次要版本。但有时候需要创建创建一个库的新主版本——即与上一个版本不兼容的版本.

版本号规则

  • 主要版本: 主要版本标识符由一个数字构成,这个数字随着库的每个不兼容版本的发布而顺序递增

  • 次要版本: 次要版本标识符可以是任意字符串。但根据惯例,它要么是一个数字,要么是两个由点分隔的数字,其中第一个数字标识出了次要版本,第二个数字表示该次要版本中的补丁号或修订号.

命名规则

需要理解下面的三个名称:

  1. 真实名称: 格式为libname.so.major-id.minor-id;

  2. 别名: soname, 格式为libname.so.major-id;

  3. 链接器名称: 就是链接时制定的名称, 不包含版本号, 只有lib*.so. 将可执行文件与共享库链接起来时会用到这个名称

    有了链接器名称之后就可以构建能够自动使用共享库的正确版本(即最新版本)的独立于版本的链接命令了

三者关系为: (以一个共享库libdevmapper.so.1.02.1为例) \(\underbrace{\underbrace{\overbrace{\text{libdevmapper.so}}^{\large 链接器名称}.1}_{\text{\Large soname}}.02.1}_{\large 真实名称}\)

示例: 查看共享库信息

readelf/objdump/nm/ldconfig/ldd/pldd/ld

ld

主要功能是链接器, 但是可以查看共享库的搜索路径:

$ ld --verbose | grep SEARCH
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");

直观一点:

$ ld --verbose | grep SEARCH | awk -v FS="=" -v RS="\");" '{print $2}' | sort

/lib
/lib64
/lib/x86_64-linux-gnu
/usr/lib
/usr/lib64
/usr/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu64
/usr/local/lib
/usr/local/lib64
/usr/local/lib/x86_64-linux-gnu
/usr/x86_64-linux-gnu/lib
/usr/x86_64-linux-gnu/lib64

ldconfig

  1. -p查看系统的共享库信息:

    事实上显示的是/etc/ld.so.cache文件内容(格式化输出的内容) -p, –print-cache Print the lists of directories and candidate libraries stored in the current cache.

    $ ldconfig -p | grep liba.so
    	liba.so (libc6,x86-64) => /lib/liba.so
    

ldd/pldd

ldd查看指定程序运行所需的共享库:

$ ldd main
	linux-vdso.so.1 (0x00007ffee634e000)
	liba.so => /lib/liba.so (0x00007ff8cd512000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff8cd320000)
	/lib64/ld-linux-x86-64.so.2 (0x00007ff8cd534000)

pldd查看指定进程运行所需的共享库:

$ sleep 10&
[1] 402337

$ sudo pldd 402337
402337:	/usr/bin/sleep
linux-vdso.so.1
/lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2

objdump/readelf

反汇编工具, 这两个命令都有等价的选项和参数, 需要根据实际情况来选择.

例如, 查看某一模块是否使用了-fPIC选项(使用位置无关代码).

$ cc -g --no-pic -c mod1.c
$ readelf -s mod1.o | grep OFF
    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_

或者通过查看共享库来判断-fPIC的使用:

$ objdump --all-headers libx1.so | grep TEXTREL
$ readelf -d libx1.so | grep TEXTREL

若产生输出就说明至少有一模块未使用-fPIC编译.

新版gcc好像默认不加-fPIC也是会使用-fPIC的, 如果用了--no-pic编译, 之后就无法链接共享库了.

  $ cc -g --no-pic -c mod1.c
  $ cc -g -Wall -shared -o libx1_wiout_PIC.so mod1.o
  /usr/bin/ld: mod1.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
  collect2: error: ld returned 1 exit status

nm

查看目标库或可执行程序中定义的一组符号.

$ nm -A /usr/lib/lib*.so | grep x
/usr/lib/libshared.so:                 w __cxa_finalize@@GLIBC_2.2.5
/usr/lib/libshared.so:00000000000010d0 t __do_global_dtors_aux
/usr/lib/libshared.so:0000000000003e18 d __do_global_dtors_aux_fini_array_entry
/usr/lib/libshared.so:0000000000001119 T x1
/usr/lib/libshared.so:0000000000001143 T x2
/usr/lib/libshared.so:000000000000116d T x3

上面的-A选项指定了在显示符号的每一行的开头处应该列出库的名称。

这样做是有必要的,因为在默认情况下,nm只列出库名一次,然后在后面会列出库中包含的所有符号,这对于像上面那样进行某种过滤的例子来讲是没有用处的

如果不加的话, 就是下面这样:

$ nm /usr/lib/lib*.so | grep x
                 w __cxa_finalize@@GLIBC_2.2.5
00000000000010d0 t __do_global_dtors_aux
0000000000003e18 d __do_global_dtors_aux_fini_array_entry
0000000000001119 T x1
0000000000001143 T x2
000000000000116d T x3

或者用来查看编译器是否使用-fPIC选项构建目标文件.

$ nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
   U _GLOBAL_OFFSET_TABLE_

示例: 共享库的链式调用

这个名称是我起的, 就是说在一个共享库中还要调用另外的一些共享库才能完成共享库的构建工作.

需要通过-rpath来指定搜索路径.

为了完成这个例子, 这里我们把mod1.c改一下, 让x1()实现为调用x2(), 如下:

// mod1.c (修改)
#include <stdio.h>
void x2();

void x1() {
    printf("%s-%s running...\n", __FILE__, __func__);
    x2();
}

然后: mkdir dir1 && mv mod1.c dir1, mkdir dir2 && mv mod2.c dir2.

现在测试文件为:

.
├── dir1
│   └── mod1.c
├── dir2
│   └── mod2.c
├── mod3.c // 未用到, 在prog.c中已注释掉
└── prog.c

这时候再生成共享库, 步骤如下:

  1. dir2中:
     cc -g -c -fPIC -Wall mod2.c
     cc -g -shared -o libx2.so mod2.o
    
  2. dir1中:

    -L指定了库的链接时位置.

     cc -g -c -Wall -fPIC mod1.c
     cc -g -shared -o libx1.so mod1.o -Wl,-rpath,/home/zorch/code/c_cpp_code/library-test/dir2 -L/home/zorch/code/c_cpp_code/library-test/dir2 -lx2
    
  3. 在主目录中:
     cc -g -Wall -o main prog.c -Wl,-rpath,/home/zorch/code/c_cpp_code/library-test/dir1,-rpath,/home/zorch/code/c_cpp_code/library-test/dir2 -L/home/zorch/code/c_cpp_code/library-test/dir1 -L/home/zorch/code/c_cpp_code/library-test/dir2 -lx1 -lx2
    
  4. 执行:
     $ ./main
     prog.c-main running...
     mod1.c-x1 running...
     mod2.c-x2 running...
     mod2.c-x2 running...
    

注意事项

  1. 按照书中的方法, 在主目录中执行:
    $ cc -g -Wall -o main prog.c -Wl,-rpath,/home/zorch/code/c_cpp_code/library-test/dir1 -L/home/zorch/code/c_cpp_code/library-test/dir1 -lx1
    /usr/bin/ld: /tmp/cc34q2m6.o: undefined reference to symbol 'x2'
    /usr/bin/ld: /home/zorch/code/c_cpp_code/library-test/dir2/libx2.so: error adding symbols: DSO missing from command line
    collect2: error: ld returned 1 exit status
    

    会报错, 所以要加上-lx2, 以及搜索路径:

    $ cc -g -Wall -o main prog.c -Wl,-rpath,/home/zorch/code/c_cpp_code/library-test/dir1 -L/home/zorch/code/c_cpp_code/library-test/dir1 -L/home/zorch/code/c_cpp_code/library-test/dir2 -lx1 -lx2
    

    但是这样执行的话会出现:

    $ ./main
    ./main: error while loading shared libraries: libx2.so: cannot open shared object file: No such file or directory
    

    所以还要通过-Wl加上关于libx2.so-rpath:

    $ cc -g -Wall -o main prog.c -Wl,-rpath,/home/zorch/code/c_cpp_code/library-test/dir1,-rpath,/home/zorch/code/c_cpp_code/library-test/dir2 -L/home/zorch/code/c_cpp_code/library-test/dir1 -L/home/zorch/code/c_cpp_code/library-test/dir2 -lx1 -lx2
    

    猜测可能是gcc版本过高的问题.

  2. 可以通过objdump查看写入的路径:
    $ objdump -p main | grep PATH
      RUNPATH              /home/zorch/code/c_cpp_code/library-test/dir1:/home/zorch/code/c_cpp_code/library-test/dir2
         
    $ objdump -p dir1/libx1.so | grep PATH
      RUNPATH              /home/zorch/code/c_cpp_code/library-test/dir2
    

    或者用readelf查看:

    $ readelf -d main | grep PATH
     0x000000000000001d (RUNPATH)            Library runpath: [/home/zorch/code/c_cpp_code/library-test/dir1:/home/zorch/code/c_cpp_code/library-test/dir2]
         
    $ readelf -d dir1/libx1.so | grep PATH
     0x000000000000001d (RUNPATH)            Library runpath: [/home/zorch/code/c_cpp_code/library-test/dir2]
    

示例: 运行时符号解析

观察下面这种情况:

创建一个新的程序, main.c, 内容如下:

#include <stdio.h>

void x1() { printf("main-x1\n"); }

void func();

int main(void) {
    func();
    return 0;
}

然后创建mod1.c:

#include <stdio.h>

void x1() { printf("mod1-x1\n"); }

void func() { x1(); }

现在开始为mod1创建共享库, 然后链接入main, 观察运行情况.

# 编译生成目标文件
$ cc -g -c -fPIC -Wall mod1.c
# 创建共享库
$ cc -g -shared -o libmod1.so mod1.o
# 链接, 加入搜索路径
$ cc -g -o main main.c libmod1.so -Wl,-rpath=/home/zorch/code/c_cpp_code/library-test/global-symbol
# 执行
$ ./main
main-x1

说明程序定义的全局符号会覆盖共享库中的符号, 那么如何指定使用共享库中实现的函数呢?

需要修改一下生成共享库时候的参数.

# 编译生成目标文件
$ cc -g -c -fPIC -Wall mod1.c
# 创建共享库, 增加了`-Bsymbolic`选项
$ cc -g -shared -o libmod1.so mod1.o -Wl,-Bsymbolic
# 链接, 加入搜索路径
$ cc -g -o main main.c libmod1.so -Wl,-rpath=/home/zorch/code/c_cpp_code/library-test/global-symbol
# 执行
$ ./main
mod1-x1

-Bsymbolic选项指定了共享库中对全局符号的引用应该优先被绑定到库中的相应定义上.

示例: 同名库的调用顺序

在链接器可以选择名称一样的静态库和共享库时, 即libxxx.a, libxxx.so, 此时会优先使用共享库, 测试如下:

在上面示例程序的基础上, 再创建一个静态库,

$ ar -r libmod1.a mod1.o
ar: creating libmod1.a

注意, 这里需要注释掉main.c中第三行的x1()函数的定义, 否则链接时候会报错:

  /usr/bin/ld: ./libmod1.a(mod1.o): in function `x1':
  /home/zorch/code/c_cpp_code/library-test/global-symbol/mod1.c:3: multiple definition of `x1'; main.o:/home/zorch/code/c_cpp_code/library-test/global-symbol/main.c:3: first defined here
  collect2: error: ld returned 1 exit status

然后使用下面的命令进行链接:

$ cc -g -o main main.c -L. -lmod1
$ ./main
mod1-x1

此时可以重命名一下libmod1.so, 例如mv libmod1.so aa.so(稍后改回来), 然后重新进行链接, 查看程序大小:

$ cc -g -o main_static main.c -L. -lmod1
$ ./main_static
mod1-x1
$ ls -lh
-rwxrwxr-x 1 zorch zorch  19K Feb 16 11:03 libmod1.so*
-rwxrwxr-x 1 zorch zorch  19K Feb 16 11:19 main*
-rwxrwxr-x 1 zorch zorch  21K Feb 16 11:18 main_static*

可以发现静态库占用比较大, 所以路径存在同名库的话, 默认是用共享库的.

那么如何在链接期指定静态库呢?

  1. 使用链接器名, 即libxx.a:
    $ cc -g -o main main.c libmod1.a
    
  2. 使用-static 指定.

    $ cc -g -o main main.c -static -L. -lmod1
    

    这种方法是全部使用静态库, 程序比较大.

    -rwxrwxr-x 1 zorch zorch 856K Feb 16 11:39 main*
    
  3. 使用-Wl,-Bstatic指定静态库:
    $ cc -g -o main main.c -Wl,-Bstatic -L. -lmod1
    /usr/bin/ld: cannot find -lgcc_s
    /usr/bin/ld: cannot find -lgcc_s
    collect2: error: ld returned 1 exit status
    

    出现这个问题主要是libgcc_s这个库只有共享库版本, 而没有静态库版, 所以会找不到.

共享库进阶

动态加载库(dlopen系统调用集)

使用dlopen系列系统调用, 可以延迟加载共享库, 即程序运行时才打开共享库, 根据名字在共享库中搜索待调用的函数, 然后进行调用. 这样的共享库称为动态加载库.

核心API如下:

  • dlopen(): 打开共享库, 返回句柄(指针).
  • dlsym(): 搜索共享库中的符号(包含函数或变量的字符串), 返回地址(函数指针).
  • dlclose(): 关闭由dlopen()打开的共享库.
  • dladdr(): 获取与加载的符号相关的信息.
  • dlerror(): 返回错误消息字符串. (用于获取执行时出现的错误消息)

使用上述API, 需要指定链接库-ldl, 链接libdl库.

示例: 使用共享库系统调用读取共享库信息/执行模块函数

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

int main(int argc, char *argv[]) {
    void *libHandle;
    void (*funcp)(void);
    const char *err;
    libHandle = dlopen(argv[1], RTLD_LAZY);
    if (!libHandle) fprintf(stderr, "dlopen: %s\n", dlerror());

    // clear dlerror
    (void)dlerror();
    // 下面三种调用等价
    /* *(void **)(&funcp) = dlsym(libHandle, argv[2]); */
    /* funcp = dlsym(libHandle, argv[2]); */
    funcp = (void (*)(void))dlsym(libHandle, argv[2]);
    err = dlerror();
    if (err != NULL) fprintf(stderr, "dlsym: %s\n", err);

    if (!funcp)
        printf("%s is NULL\n", argv[2]);
    else
        (*funcp)(); // exec function in module
    dlclose(libHandle);
    exit(EXIT_SUCCESS);

    return 0;
}

执行:

$ ./dlopen_test.out dir1/libx1.so x1
mod1.c-x1 running...
mod2.c-x2 running...

示例: 使用dladdr获取与加载符号相关的信息

稍微修改一下上面的程序即可:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    void* libHandle;
    void (*funcp)(void);
    const char* err;
    libHandle = dlopen(argv[1], RTLD_LAZY);
    if (!libHandle) fprintf(stderr, "dlopen: %s\n", dlerror());

    // clear dlerror
    (void)dlerror();
    /* *(void **)(&funcp) = dlsym(libHandle, argv[2]); */
    /* funcp = dlsym(libHandle, argv[2]); */
    funcp = (void (*)(void))dlsym(libHandle, argv[2]);
    err = dlerror();
    if (err != NULL) fprintf(stderr, "dlsym: %s\n", err);

    /* typedef struct { */
    /*     const char* dli_fname; */
    /*     void* dli_fbase; */
    /*     const char* dli_sname; */
    /*     void* dli_saddr; */
    /* } Dl_info; */

    Dl_info info;
    if (!funcp)
        printf("%s is NULL\n", argv[2]);
    else {
        // exec
        (*funcp)();
        if (dladdr(funcp, &info)) {
            printf("dli_fname: %s\n", info.dli_fname);
            printf("dli_fbase: %p\n", info.dli_fbase);
            printf("dli_sname: %s\n", info.dli_sname);
            printf("dli_saddr: %p\n", info.dli_saddr);
            printf("call func by dli_saddr: \n");
            ((void (*)(void))(info.dli_saddr))(); // func ptr cast
        }
    }
    dlclose(libHandle);
    exit(EXIT_SUCCESS);

    return 0;
}

运行:

$ ./dladdr_test.out dir1/libx1.so x1
mod1.c-x1 running...
mod2.c-x2 running...
dli_fname: dir1/libx1.so
dli_fbase: 0x7fef52f36000 # 加载共享库的基地址
dli_sname: x1
dli_saddr: 0x7fef52f37139
call func by dli_saddr:
mod1.c-x1 running...
mod2.c-x2 running...

控制符号可见性

需要控制API调用的可见性, 避免使用者程序出现不兼容问题, 通过gcc提供的特有声明来完成, 如下:

gcc特性声明

void
__attribute__((visibility("hidden")))
func(void) { 
    /* code */
}

通过下面这种方法就可以使符号不可见: (区别于static关键字)

static将一个符号的可见性限制在单个源码文件中

hidden属性(特性)将一个符号对构成共享库的所有源码文件都可见, 但是对库之外的文件不可见.

下面是一个测试:

// test_hidden.c
void t1(void);

int main(int argc, char *argv[]) {
    t1();
    return 0;
}
// hidden.c
#include <stdio.h>

void
// __attribute__((visibility("hidden")))
t1(void) { printf("unreachable\n"); }

首先是不加特性的版本:

$ cc -g -shared -o libhidden.so special_attr_hidden.c
$ cc -g -Wall -Wl,-rpath=/home/zorch/code/c_cpp_code/library-test -o main test_hidden.c libhidden.so
$ ./main
unreachable

解注释, 使特性生效:

$ cc -g -shared -o libhidden.so special_attr_hidden.c
$ cc -g -Wall -Wl,-rpath=/home/zorch/code/c_cpp_code/library-test -o main test_hidden.c libhidden.so
/usr/bin/ld: /tmp/cc7EpQgY.o: in function `main':
/home/zorch/code/c_cpp_code/library-test/test_hidden.c:4: undefined reference to `t1'
collect2: error: ld returned 1 exit status

可见hidden特性生效了, 使得共享库内API函数的外部调用失败.

后记

书中还有版本脚本等的内容, 这里不详细列出了.