Shared Libraries

Table of Contents

1. Static Libraries

在介绍动态库(Shared Libraries)之前,我们先介绍一下静态库(Static Libraries)。

1.1. 制作静态库

要制作静态库,先把各个模块编译为对应的 .o 文件,再用工具 ar 打包为静态库即可。

$ cc -c mod1.c mod2.c mod3.c
$ ar r libdemo.a mod1.o mod2.o mod3.o
$ rm mod1.o mod2.o mod3.o

1.2. 链接静态库

要把程序链接到静态库,有两种方法。

方法 1:直接指定静态库的文件名。

$ cc -c prog.c
$ cc -o prog prog.o libdemo.a

方法 2:通过 -l 指定静态库的库名(库名为文件名去掉 lib 前缀和 .a 后缀)。

$ cc -c prog.c
$ cc -o prog prog.o -ldemo

如果静态库不在标准的搜索路径中,可以通过 -L 指定静态库的路径。

注: 如果 libdemo.so 和 libdemo.a 在库探索路径中同时存在,-ldemo 会优先使用共享库,通过指定 gcc 的 -static 选项可强制使用静态库。

2. Shared Libraries 基本用法

静态库有很多缺点,比如更新和维护比较麻烦,如果想使用库的最新版本,必须显示地重新链接。静态库对存储器资源造成极大地浪费,如果 100 个程序都引用某个静态库,当它们运行时都会复制库中函数代码到每个进程的 text 段中。

共享库能解决上面提到的静态库缺点。共享库以两种方法来实现“共享”:首先,在文件系统中,对于一个库只有一个 .so 文件,所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库那样被拷贝和嵌入到引用它们的可执行文件中。其次,在存储器中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。

2.1. 制作共享库

不同的编译器,生成共享库的方法不一样,下面仅介绍 gcc 的方法。

$ gcc -c -fPIC -Wall mod1.c mod2.c mod3.c
$ gcc -shared -o libfoo.so mod1.o mod2.o mod3.o

也可以用下面一个命令生成共享库 .so 文件:

$ gcc -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so

2.2. 链接共享库

要把程序链接到动态库,有两种方法。

方法 1:链接时,直接指定共享库即可。

$ gcc -Wall -o prog prog.c libfoo.so

方法 2:通过 -l 指定共享库的库名(注:库名为文件名去掉 lib 前缀和 .so 后缀)。

$ gcc -Wall -o prog prog.c -lfoo

如果共享库不在标准的搜索路径中,可以通过 -L 指定共享库的路径。如果找不到共享库,gcc 会尝试找对应的静态库 libfoo.a。

运行程序时,如果共享库不在标准路径中,需要指定环境变量 LD_LIBRARY_PATH。

$ LD_LIBRARY_PATH=. ./prog

2.2.1. LD_LIBRARY_PATH/LIBPATH/DYLD_LIBRARY_PATH

在不同的 Unix 系统的,设置非标准共享库的搜索路径的环境变量可能不同,如表 1 所示。

Table 1: 不同 Unix 系统中,设置非标准共享库的搜索路径的环境变量
Linux AIX Solaris MacOS
LD_LIBRARY_PATH LIBPATH LD_LIBRARY_PATH DYLD_LIBRARY_PATH

3. 加载共享库的两种机制(Load-time Relocation 和 PIC)

共享库是一个目标模块。在运行时,共享库可以加载到任意的存储器地址,并和一个存储器中的程序链接起来,这个过程称为“动态链接”。

有两种主要的机制来处理动态链接:

  1. Load-time Relocation(加载时重定位)
  2. Position Independent Code (PIC)

如果没有使用 -fPIC 编译共享库,则会使用前者(即加载时重定位)来处理动态链接;不过现在一般使用后者(即 PIC),后面会介绍 PIC 的优点。

3.1. Load-time Relocation

Load-time Relocation 是一种直观的方法,在加载共享库时把共享库中的符号地址地址修改为当前进程地址空间的一个地址(这就是重定位过程)。这种方法有两个缺点:

  1. 存在性能问题,当一个程序使用很多共享库时,在启动时要花费较多的时间在来对每个共享库进行重定位。
  2. 共享库的代码段没有办法在内存中共享。由于进行重定位过程时共享库的代码段被修改了,所以无法在多个进程的地址空间中共享(除非所有进程加载共享库到地址空间的相同位置,但会带来管理问题,Linux 没有这么做)。

参考: Load-time relocation of shared libraries

3.2. Position Independent Code (PIC)

The –fPIC option specifies that the compiler should generate position-independent code. It allow the code to be located at any virtual address at run time (rather than load time).

现在一般使用这种方法。它在动态链接过程中不会修改共享库中代码段。
如何访问 PIC 共享库中的全局变量符号?利用 GOT (Global Offset Table)。
如何访问 PIC 共享库中的函数符号?利用 GOT (Global Offset Table) 和 PLT(Procedure Linkage Table)。

说明:PIC 解决了 Load-time relocation 方法的两个缺点,但 PIC 代码也有性能缺陷——访问全局变量需要更多的指令(因为它通过 GOT 间接地访问全局变量)。

参考:
深入理解计算机系统(原书第 2 版)7.12 与位置无关代码
Position Independent Code (PIC) in shared libraries

3.2.1. 检测 .o 文件是否是用 -fPIC 选项编译

如何检测 .o 文件是否是用 -fPIC 选项编译?

先做个测试,不用或用 -fPIC 选项编译时,目标文件中的符号有什么不同?

$ cat mod1.c
extern int i;

void test(void)
{
    i = 1;
}
$ gcc -c mod1.c                            # 不使用 -fPIC 编译
$ nm mod1.o
                 U i
0000000000000000 T test
$ gcc -c -o mod1-PIC.o -fPIC mod1.c        # 使用 -fPIC 编译
$ nm mod1-PIC.o
                 U _GLOBAL_OFFSET_TABLE_
                 U i
0000000000000000 T test

由上面测试可以知道, 当目标文件用 -fPIC 选项编译生成时,会多个符号 _GLOBAL_OFFSET_TABLE_ 所以可以用下面任一个命令来检测 .o 文件是否是用 -fPIC 选项编译,如果有输出,则说明 .o 文件用 -fPIC 选项编译生成。

$ nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
$ readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_

Global Offset Table 参考:http://www.bottomupcs.com/global_offset_tables.html

4. 把“库搜索目录”写死在目标文件中 (-Wl,-rpath,/path/to/lib)

We have already seen two ways of informing the dynamic linker of the location of shared libraries: using the LD_LIBRARY_PATH environment variable and installing a shared library into one of the standard library directories (/lib, /usr/lib, or one of the directories listed in /etc/ld.so.conf).
There is a third way: during the static editing phase, we can insert into the executable a list of directories that should be searched at run time for shared libraries. This is useful if we have libraries that reside in fixed locations that are not among the standard locations searched by the dynamic linker. To do this, we employ the –rpath linker option when creating an executable:

$ gcc -g -Wall -Wl,-rpath,/home/mtk/pdir -o prog prog.c libdemo.so

The above command copies the string /home/mtk/pdir into the run-time library path (rpath) list of the executable prog, so, that when the program is run, the dynamic linker will also search this directory when resolving shared library references.

4.1. 编译时指定 LD_RUN_PATH

An alternative to the –rpath option is the LD_RUN_PATH environment variable. This variable can be assigned a string containing a series of colon-separated directories that are to be used as the rpath list when building the executable file. LD_RUN_PATH is employed only if the –rpath option is not specified when building the executable.

4.2. 指定动态库相对搜索路径(在 rpath 中使用$ORIGIN)

The dynamic linker interprets $ORIGIN to mean “the directory containing the application.”
This means that we can, for example, build an application with the following command:

$ gcc -Wl,-rpath,'$ORIGIN'/lib  ...

This presumes that at run time the application’s shared libraries will reside in the subdirectory lib under the directory that contains the application executable.

5. 运行时符号解析 (-Wl,-Bsymbolic)

Suppose that a global symbol (i.e., a function or variable) is defined in multiple locations, such as in an executable and in a shared library, or in multiple shared libraries. How is a reference to that symbol resolved?

假设有下面程序,符号 xyz 同时出现在主程序和一个动态库中。

You might expect this:

           prog                               libfoo.so
+----------------------------+      +----------------------------+
|  #include<stdio.h>         |      |  #include<stdio.h>         |
|  extern void func(void);   |      |                            |
|                            |      |  void xyz() {              |
|  void xyz() {              |      |      printf("foo-xyz\n");  |
|      printf("main-xyz\n"); |      |  }                         |
|  }                         |      |        ^                   |
|                            |      |        |                   |
|  int main() {              |      |        |                   |
|      xyz();                |      |        |                   |
|      func();        -------+----> |  void func() {             |
|      return 0;             |      |      xyz();                |
|  }                         |      |  }                         |
+----------------------------+      +----------------------------+


But what you get may be this:

           prog                               libfoo.so
+----------------------------+      +----------------------------+
|  #include<stdio.h>         |      |  #include<stdio.h>         |
|  extern void func(void);   |      |                            |
|                            |      |  void xyz() {              |
|  void xyz() {              |<-+   |      printf("foo-xyz\n");  |
|      printf("main-xyz\n"); |  |   |  }                         |
|  }                         |  |   |                            |
|                            |  +---+--------+                   |
|  int main() {              |      |        |                   |
|      xyz();                |      |        |                   |
|      func();        -------+----> |  void func() {             |
|      return 0;             |      |      xyz();                |
|  }                         |      |  }                         |
+----------------------------+      +----------------------------+

测试 1:

$ gcc -g -c -fPIC -Wall foo.c
$ gcc -g -shared -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so   # 把libfoo.so放在前面不会影响结果 gcc -g -o prog libfoo.so prog.c
$ LD_LIBRARY_PATH=. ./prog
main-xyz
main-xyz

通过上面的结果可知,func() 调用的是 prog 中的 xyz(),而没有调用动态库 libfoo.so 中的 xyz()。

The –Bsymbolic linker option specifies that references to global symbols within a shared library should be preferentially bound to definitions (if they exist) within that library. (Note that, regardless of this option, calling xyz() from the main program would always invoke the version of xyz() defined in the main program.)

测试 2(编译动态库时,增加选项 -Wl,-Bsymbolic ):

$ gcc -g -c -fPIC -Wall foo.c
$ gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o    # libfoo.so中的符号优先调用同一库中的符号
$ gcc -g -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog
main-xyz
foo-xyz

说明:上面的实验在 Linux 中测试通过,其它平台可能不一样,如默认地 Mac OS X 中库中的函数优先使用同一个库中的函数。
说明:仅就上面的问题而言,我们还有其它办法来使 func() 调用动态库 libfoo.so 中的 xyz(),如:

  1. 把 prog.c 中的函数 xyz() 声明为 static;
  2. 利用 gcc 的 __attribute__ 把 prog.o 中的符号 xyz() 隐藏起来。即:
#include<stdio.h>
extern void func(void);

__attribute__ ((visibility ("hidden")))
void xyz() {
    printf("main-xyz\n");
}

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

6. 动态加载共享库

当开始程序执行时,所有的共享库都会被加载,如果找不到会直接报错。但有时候我们只想在需要的时候进行加载(如插件)。这可以通过在程序运行过程中动态加载共享库来实现。

6.1. dlopen API

The dlopen API enables a program to open a shared library at run time, search for a function by name in that library, and then call the function. A shared library loaded at run time in this way is commonly referred to as a dynamically loaded library, and is created in the same way as any other shared library.

The core dlopen API consists of the following functions (all of which are specified in SUSv3):

  • The dlopen() function opens a shared library, returning a handle used by subsequent calls.
  • The dlsym() function searches a library for a symbol (a string containing the name of a function or variable) and returns its address.
  • The dlclose() function closes a library previously opened by dlopen().
  • The dlerror() function returns an error-message string, and is used after a failure return from one of the preceding functions.

注:在 Linux 上,如果程序使用了 dlopen/dlsym/dlclose/dlerror 这些 API,则编译时必须使用 -ldl 选项以链接到 libdl 库。

6.2. 让动态加载库能访问主程序中符号 (-rdynamic)

Suppose that we use dlopen() to dynamically load a shared library, use dlsym() to obtain the address of a function x() from that library, and then call x(). If x() in turn calls a function y(), then y() would normally be sought in one of the shared libraries loaded by the program.
Sometimes, it is desirable instead to have x() invoke an implementation of y() in the main program. (This is similar to a callback mechanism.)
In order to do this, we must make the (global-scope) symbols in the main program available to the dynamic linker, by linking the program using the –rdynamic linker option:

$ gcc -rdynamic main.c

gcc 的下面 4 个选项的含义一样:
-rdynamic
-export-dynamic
-Wl,--export-dynamic
-Wl,-E

7. 控制共享库的导出符号

如果我们不想公开共享库中的某个符号,可通过把它设置为 static 来实现。但有时这种方法不可行,如:

// file foo.c
void foo() {    // foo()是libfoo的主要函数,想公开
  bar();
}

// file bar.c
void bar() {     // bar()仅是内部函数,不想公开。但却不能把它设置为static,因为另一文件foo.c中调用了它。
}

可把上面的文件 foo.c 和 bar.c 分别编译,链接生成 libfoo.so。

$ gcc -fPIC -c foo.c
$ gcc -fPIC -c bar.c
$ gcc -shared -o libfoo.so foo.o bar.o
$ nm libfoo.so |egrep 'foo|bar'
0000000000001126 T bar
0000000000001115 T foo

可以看到有两个符号 foo 和 bar 在动态库 libfoo.so 中是对外可见(记号 T 表示 Global Symbol in Text Section),但 bar 并不是我们想公开的。如何隐藏符号 bar 呢?可以利用符号的“visibility 属性”,具体请看下文分解。

参考:
Binary Hacks: 黑客秘笈 100 选,Hack#29 控制对外公开库的符号
How To Write Shared Libraries, by Ulrich Drepper: http://www.akkadia.org/drepper/dsohowto.pdf
How To Write Shared Libraries, (slides) by Ulrich Drepper Slides: http://www.akkadia.org/drepper/ukuug2002slides.pdf
Shared Library Symbol Conflicts: https://holtstrom.com/michael/blog/post/437/Shared-Library-Symbol-Conflicts.html

7.1. 控制共享库导出符号——visibility 属性

可用通过定义符号的 visibility 属性来控制它的对外可见性。

具体来说,下面任一种策略都行:

  1. 把想要公开的符号声明为 __attribute__ ((visibility("default"))) ;然后编译 .o 时,默认使用选项 -fvisibility=hidden
  2. 把所有不想公开的符号声明为 __attribute__ ((visibility("hidden"))) ;然后编译 .o 时,默认使用选项 -fvisibility=default

下面是采用策略 1 的测试:

// file foo.c
__attribute__ ((visibility("default")))
void foo() {  // foo() 是 libfoo 的主要函数,想公开,设置 __attribute__ ((visibility("default")))
  bar();
}

编译时指定 -fvisibility=hidden

$ gcc -fPIC -c -fvisibility=hidden foo.c
$ gcc -fPIC -c -fvisibility=hidden bar.c
$ gcc -shared -o libfoo.so foo.o bar.o
$ nm libfoo.so |egrep 'foo|bar'
0000000000001116 t bar
0000000000001105 T foo

可以看到,符号 bar 在 libfoo.so 中已经不再对外可见了(记号 t 表示 Local Symbol in Text Section,这不是对外可见的)。

参考:
https://gcc.gnu.org/onlinedocs/gcc-5.2.0/gcc/Code-Gen-Options.html#index-fvisibility-2829

7.1.1. visibility 属性的实现基础

The ELF application binary interface (ABI) defines the visibility of symbols. Generally, it defines four classes:

  1. STV_DEFAULT - Symbols defined with it will be exported. In other words, it declares that symbols are visible everywhere.
  2. STV_HIDDEN - Symbols defined with it will not be exported and cannot be used from other objects
  3. STV_PROTECTED - The symbol is visible outside the current executable or shared object, but it may not be overridden. In other words, if a protected symbol in a shared library is referenced by an other code in the shared library, the other code will always reference the symbol in the shared library, even if the executable defines a symbol with the same name.
  4. STV_INTERNAL - The symbol is not accessible outside the current executable or shared library.

In most cases, only STV_DEFAULT and STV_HIDDEN are more commonly used.

参考:
man 5 elf
Controlling symbol visibility for shared libraries: http://www.ibm.com/developerworks/aix/library/au-aix-symbol-visibility/

7.1.2. 检查 .o 文件中符号的可见性

如何检查 .o 文件中符号的可见性呢?这时,不能直接使用 nm 命令。

$ cat file.c
__attribute__ ((visibility("default")))
void fun1() {
}

__attribute__ ((visibility("hidden")))
void fun2() {
}
$ gcc -fPIC -c file.c
$ nm file.o
0000000000000000 T fun1
0000000000000007 T fun2

在上面例子中,我们指定了 fun1 和 fun2 分别为对外可见和对外不可见。但使用 nm 检查时,输出的都是 T。

下面介绍两种在 Linux 中检查 .o 文件中符号的可见性的方法。

方法一、使用 objdump -t file.o ,如:

$ objdump -t file.o

file.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 file.c
0000000000000000 l    d  .text	0000000000000000 .text
0000000000000000 l    d  .data	0000000000000000 .data
0000000000000000 l    d  .bss	0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack	0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame	0000000000000000 .eh_frame
0000000000000000 l    d  .comment	0000000000000000 .comment
0000000000000000 g     F .text	0000000000000007 fun1
0000000000000007 g     F .text	0000000000000007 .hidden fun2

在上面输出中 fun2 前面有个 .hidden 字样,表示是对外不可见的。

方法二,使用 readelf -s file.o ,如:

$ readelf -s file.o

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS file.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     8: 0000000000000000     7 FUNC    GLOBAL DEFAULT    1 fun1
     9: 0000000000000007     7 FUNC    GLOBAL HIDDEN     1 fun2

在命令 readelf -s file.o 输出的第 6 列(即 Vis 列)中的 DEFAULT/HIDDEN 分别表示符号对外可见/不可见。如上面例子中 fun1 是对外可见的,而 fun2 是对外不可见的。

7.2. 控制共享库导出符号——LD Version Script (--version-script)

使用 GNU 链接器的版本控制脚本可以控制对外公开的符号。

如,可以准备下面文件来实现仅导出符号 foo:

$ cat libfoo.map
{
  global:
     foo;             # only export symbol foo

  local: *;
};

使用链接选项 --version-script 来指定这个文件即可。

$ gcc -fPIC -c foo.c
$ gcc -fPIC -c bar.c
$ gcc -shared -o libfoo.so foo.o bar.o -Wl,--version-script,libfoo.map
$ nm libfoo.so | egrep 'foo|bar'
0000000000001116 t bar
0000000000001105 T foo

可以看到,符号 bar 已经对外不可见了。

参考:https://sourceware.org/binutils/docs/ld/VERSION.html

7.2.1. Version Script 的格式

LD Version Script 的格式和 Sun's linker in Solaris 2.5 相同。

参考:
Sun's Linker and Libraries Guide: http://docs.oracle.com/cd/E19253-01/817-1984/
http://docs.oracle.com/cd/E19683-01/817-3677/817-3677.pdf

7.2.2. Solaris 系统(-M<mapfile>)

Solaris 系统中 C 编译器(cc)不支持 --version-script 选项,可以使用 -M<mapfile> 选项。

$ cc -flags|grep -w M
-M<file>                      Pass <file> mapfile to linker

测试实例如下(源文件及 map 文件和前面实例相同):

$ cc -fPIC -c foo.c
$ cc -fPIC -c bar.c
$ cc -shared -o libfoo.so foo.o bar.o
$ nm libfoo.so |grep GLOB
[49]    |     66288|       0|OBJT |GLOB |0    |11     |_DYNAMIC
[50]    |     66220|       0|OBJT |GLOB |0    |9      |_GLOBAL_OFFSET_TABLE_
[40]    |     66224|       0|OBJT |GLOB |0    |10     |_PROCEDURE_LINKAGE_TABLE_
[48]    |     66440|       0|OBJT |GLOB |0    |13     |_edata
[44]    |     66440|       0|OBJT |GLOB |0    |14     |_end
[42]    |       684|       0|OBJT |GLOB |0    |8      |_etext
[41]    |       668|      12|FUNC |GLOB |0    |7      |_fini
[43]    |       656|      12|FUNC |GLOB |0    |6      |_init
[47]    |       680|       4|OBJT |GLOB |0    |8      |_lib_version
[45]    |       632|      20|FUNC |GLOB |0    |5      |bar
[46]    |       584|      28|FUNC |GLOB |0    |5      |foo

$ cc -shared -o libfoo.so foo.o bar.o -M libfoo.map
$ nm libfoo.so |grep GLOB
[45]    |     66136|       0|OBJT |GLOB |0    |11     |_DYNAMIC
[48]    |     66132|       0|OBJT |GLOB |0    |10     |_GLOBAL_OFFSET_TABLE_
[47]    |         0|       0|OBJT |GLOB |0    |ABS    |_PROCEDURE_LINKAGE_TABLE_
[49]    |     66248|       0|OBJT |GLOB |0    |13     |_edata
[44]    |     66248|       0|OBJT |GLOB |0    |14     |_end
[46]    |       596|       0|OBJT |GLOB |0    |9      |_etext
[50]    |       496|      28|FUNC |GLOB |0    |6      |foo

从上面的测试结果可以看到使用 -M libfoo.map 后,符号 bar 被隐藏了。

7.2.3. AIX 系统(-bE:exportlist)

AIX 系统中用来控制导出符号的选项是 -bE:exportlist

但是对应的“导出符号配置文件”的格式和 Linux/Solaris 不同。AIX 使用的格式更简单,把需要导出的符号一行行地列到文件中即可。如:

$ cat libfoo.exp
foo

测试实例如下(源文件和前面实例相同):

$ xlc -c foo.c
$ xlc -c bar.c
$ xlc -G -o libfoo.so foo.o bar.o
$ nm -g  libfoo.so
.bar                 T   268435840
.bar                 T   268435912
.foo                 T   268435776
.foo                 T   268435872
bar                  D   536871420          12
foo                  D   536871408          12

$ xlc -G -o libfoo.so foo.o bar.o -bE:libfoo.exp
$ nm -g  libfoo.so
.bar                 T   268435840
.foo                 T   268435776
.foo                 T   268435872
foo                  D   536871368          12

从上面的测试结果可以看到使用 -bE:libfoo.exp 后,符号 bar 被隐藏了。

参考:
Compiling a shared library: http://www.ibm.com/support/knowledgecenter/SSGH2K_13.1.2/com.ibm.xlc131.aix.doc/proguide/compiling_shared_aix.html?lang=zh

7.3. 隐藏共享库内部符号的好处

It is useful because

  • It prevents abuse of undocumented APIs of your library. Symbols that are not exported from the library cannot be used. This eliminates the problem that when the maintainer of the library changes internals of the library, maintainers of other projects cry "breakage". Instead, these maintainers are forced to negotiate the desired API from the maintainer of the library.
  • It reduces the risk of symbol collision between your library and other libraries. For example, the symbol 'readline' is defined in several libraries, most of which don’t have the same semantics and the same calling convention as the GNU readline library.
  • It reduces the startup time of programs linked to the library. This is because the dynamic loader has less symbols to process.
  • It allows the compiler to generate better code. Within a shared library, a call to a function that is a global symbol costs a "call" instruction to a code location in the so-called PLT (procedure linkage table) which contains a "jump" instruction to the actual function’s code. (This is needed so that the function can be overridden, for example by a function with the same name in the executable or in a shared library interposed with LD_PRELOAD.) Whereas a call to a function for which the compiler can assume that it is in the same shared library is just a direct "call" instructions. Similarly for variables: A reference to a global variable fetches a pointer in the so-called GOT (global offset table); this is a pointer to the variable’s memory. So the code to access it is two memory load instructions. Whereas for a variable which is known to reside in the same shared library, it is just a direct memory access: one memory load instruction.

参考:https://www.gnu.org/software/gnulib/manual/html_node/Exported-Symbols-of-Shared-Libraries.html

7.4. 查看共享库的导出符号 (nm)

在 Linux 中,可以用 nm -D /path/to/libfoo.so 或者 readelf -W --dyn-syms /path/to/libfoo.so 查看共享库的导出符号。

下面是在 Linux 中使用 nm 查看前面生成的 libfoo.so 的导出符号的例子:

$ nm -D libfoo.so
                 w _Jv_RegisterClasses
0000000000200848 A __bss_start
                 w __cxa_finalize
                 w __gmon_start__
0000000000200848 A _edata
0000000000200858 A _end
00000000000005c8 T _fini
0000000000000458 T _init
000000000000057c T bar
000000000000056c T foo

下面是在 MacOS 中使用 nm 查看共享库的导出符号的例子:

$ nm libfoo.dylib       # MacOS 下不要使用 -D 选项,否则提示 File format has no dynamic symbol table
0000000000000fb0 T _bar
0000000000000f90 T _foo
                 U dyld_stub_binder

下面是在 Linux 中使用 readelf 查看共享库的导出符号的例子:

$ readelf -s -W libfoo.so

Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000458     0 SECTION LOCAL  DEFAULT    9
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     4: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2           .5 (2)
     5: 000000000000057c     6 FUNC    GLOBAL DEFAULT   11 bar
     6: 000000000000056c    16 FUNC    GLOBAL DEFAULT   11 foo
     7: 0000000000200858     0 NOTYPE  GLOBAL DEFAULT  ABS _end
     8: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     9: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    10: 0000000000000458     0 FUNC    GLOBAL DEFAULT    9 _init
    11: 00000000000005c8     0 FUNC    GLOBAL DEFAULT   12 _fini

Symbol table '.symtab' contains 54 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000190     0 SECTION LOCAL  DEFAULT    1
     2: 00000000000001b8     0 SECTION LOCAL  DEFAULT    2
     3: 00000000000001f8     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000318     0 SECTION LOCAL  DEFAULT    4
     5: 000000000000038e     0 SECTION LOCAL  DEFAULT    5
     6: 00000000000003a8     0 SECTION LOCAL  DEFAULT    6
     7: 00000000000003c8     0 SECTION LOCAL  DEFAULT    7
     8: 0000000000000428     0 SECTION LOCAL  DEFAULT    8
     9: 0000000000000458     0 SECTION LOCAL  DEFAULT    9
    10: 0000000000000470     0 SECTION LOCAL  DEFAULT   10
    11: 00000000000004a0     0 SECTION LOCAL  DEFAULT   11
    12: 00000000000005c8     0 SECTION LOCAL  DEFAULT   12
    13: 00000000000005d8     0 SECTION LOCAL  DEFAULT   13
    14: 00000000000005f8     0 SECTION LOCAL  DEFAULT   14
    15: 0000000000200658     0 SECTION LOCAL  DEFAULT   15
    16: 0000000000200668     0 SECTION LOCAL  DEFAULT   16
    17: 0000000000200678     0 SECTION LOCAL  DEFAULT   17
    18: 0000000000200680     0 SECTION LOCAL  DEFAULT   18
    19: 0000000000200688     0 SECTION LOCAL  DEFAULT   19
    20: 0000000000200808     0 SECTION LOCAL  DEFAULT   20
    21: 0000000000200820     0 SECTION LOCAL  DEFAULT   21
    22: 0000000000200848     0 SECTION LOCAL  DEFAULT   22
    23: 0000000000000000     0 SECTION LOCAL  DEFAULT   23
    24: 00000000000004a0     0 FUNC    LOCAL  DEFAULT   11 call_gmon_start
    25: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    26: 0000000000200658     0 OBJECT  LOCAL  DEFAULT   15 __CTOR_LIST__
    27: 0000000000200668     0 OBJECT  LOCAL  DEFAULT   16 __DTOR_LIST__
    28: 0000000000200678     0 OBJECT  LOCAL  DEFAULT   17 __JCR_LIST__
    29: 00000000000004c0     0 FUNC    LOCAL  DEFAULT   11 __do_global_dtors_aux
    30: 0000000000200848     1 OBJECT  LOCAL  DEFAULT   22 completed.6338
    31: 0000000000200850     8 OBJECT  LOCAL  DEFAULT   22 dtor_idx.6340
    32: 0000000000000540     0 FUNC    LOCAL  DEFAULT   11 frame_dummy
    33: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    34: 0000000000200660     0 OBJECT  LOCAL  DEFAULT   15 __CTOR_END__
    35: 0000000000000650     0 OBJECT  LOCAL  DEFAULT   14 __FRAME_END__
    36: 0000000000200678     0 OBJECT  LOCAL  DEFAULT   17 __JCR_END__
    37: 0000000000000590     0 FUNC    LOCAL  DEFAULT   11 __do_global_ctors_aux
    38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.c
    39: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bar.c
    40: 0000000000200820     0 OBJECT  LOCAL  DEFAULT  ABS _GLOBAL_OFFSET_TABLE_
    41: 0000000000200680     0 OBJECT  LOCAL  DEFAULT   18 __dso_handle
    42: 0000000000200670     0 OBJECT  LOCAL  DEFAULT   16 __DTOR_END__
    43: 0000000000200688     0 OBJECT  LOCAL  DEFAULT  ABS _DYNAMIC
    44: 000000000000057c     6 FUNC    GLOBAL DEFAULT   11 bar
    45: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    46: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    47: 00000000000005c8     0 FUNC    GLOBAL DEFAULT   12 _fini
    48: 000000000000056c    16 FUNC    GLOBAL DEFAULT   11 foo
    49: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.           2.5
    50: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    51: 0000000000200858     0 NOTYPE  GLOBAL DEFAULT  ABS _end
    52: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
    53: 0000000000000458     0 FUNC    GLOBAL DEFAULT    9 _init

比较新的 readelf 支持 --dyn-syms 选项来仅输出 .dynsym 中的符号,如:

$ readelf --dyn-syms -W libfoo.so

Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000458     0 SECTION LOCAL  DEFAULT    9
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     4: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2           .5 (2)
     5: 000000000000057c     6 FUNC    GLOBAL DEFAULT   11 bar
     6: 000000000000056c    16 FUNC    GLOBAL DEFAULT   11 foo
     7: 0000000000200858     0 NOTYPE  GLOBAL DEFAULT  ABS _end
     8: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     9: 0000000000200848     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    10: 0000000000000458     0 FUNC    GLOBAL DEFAULT    9 _init
    11: 00000000000005c8     0 FUNC    GLOBAL DEFAULT   12 _fini

参考:
http://stackoverflow.com/questions/1237575/how-do-i-find-out-what-all-symbols-are-exported-from-a-shared-object

8. 参考

本文主要参考:
"The Linux Programming Interface" 41 Fundamentals of Shared Libraries
"The Linux Programming Interface" 42 Advanced Features of Shared Libraries

Author: cig01

Created: <2015-03-15 Sun>

Last updated: <2017-12-13 Wed>

Creator: Emacs 27.1 (Org mode 9.4)