by falcon [email protected] of TinyLab.org
2008-2-26
最初发表:泰晓科技 – 聚焦嵌入式 Linux,追本溯源,见微知著!
原文链接:动态符号链接的细节
评论说明:为更好地聚合大家的讨论,请到上面原文的评论区回复。
【注】这是开源书籍《C语言编程透视》第四章,如果您喜欢该书,请关注我们的新浪微博@泰晓科技
动态符号链接的细节
前言
Linux支持动态链接库,不仅节省了磁盘、内存空间,而且可以提高程序运行效率。不过引入动态链接库也可能会带来很多问题,例如动态链接库的调试、升级更新和潜在的安全威胁1, 2。这里主要讨论符号的动态链接过程,即程序在执行过程中,对其中包含的一些未确定地址的符号进行重定位的过程1, 2。
本篇主要参考资料3和8,前者侧重实践,后者侧重原理,把两者结合起来就方便理解程序的动态链接过程了。另外,动态链接库的创建、使用以及调用动态链接库的部分参考了资料1, 2。
下面先来看看几个基本概念,接着就介绍动态链接库的创建、隐式和显示调用,最后介绍符号的动态链接细节。
基本概念
ELF
ELF是Linux支持的一种程序文件格式,本身包含重定位、执行、共享(动态链接库)三种类型(man elf
)。
代码:
/* test.c */
#include <stdio.h>
int global = 0;
int main()
{
char local = 'A';
printf("local = %c, global = %dn", local, global);
return 0;
}
演示:
通过-c生成可重定位文件test.o,这里不会进行链接:
$ gcc -c test.c
$ file test.o
test.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
链接后才可以执行:
$ gcc -o test test.o
$ file test
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
也可链接成动态链接库,不过一般不会把main函数链接成动态链接库,后面再介绍:
$ gcc -fpic -shared -W1,-soname,libtest.so.0 -o libtest.so.0.0 test.o
$ file libtest.so.0.0
libtest.so.0.0: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped
虽然ELF文件本身就支持三种不同的类型,不过它有一个统一的结构。这个结构是:
文件头部(ELF Header)
程序头部表(Program Header Table)
节区1(Section1)
节区2(Section2)
节区3(Section3)
...
节区头部表(Section Header Table)
无论是文件头部、程序头部表、节区头部表,还是节区,它们都对应着C语言里头的一些结构体(elf.h中定义)。文件头部主要描述ELF文件的类型,大小,运行平台,以及和程序头部表和节区头部表相关的信息。节区头部表则用于可重定位文件,以便描述各个节区的信息,这些信息包括节区的名字、类型、大小等。程序头部表则用于描述可执行文件或者动态链接库,以便系统加载和执行它们。而节区主要存放各种特定类型的信息,比如程序的正文区(代码)、数据区(初始化和未初始化的数据)、调试信息、以及用于动态链接的一些节区,比如解释器(.interp
)节区将指定程序动态装载/链接器ld-linux.so
的位置,而过程链接表(plt)、全局偏移表(got)、重定位表则用于辅助动态链接过程。
符号
对于可执行文件除了编译器引入的一些符号外,主要就是用户自定义的全局变量,函数等,而对于可重定位文件仅仅包含用户自定义的一些符号。
-
生成可重定位文件
$ gcc -c test.c
$ nm test.o
00000000 B global
00000000 T main
U printf
上面包含全局变量、自定义函数以及动态链接库中的函数,但不包含局部变量,而且发现这三个符号的地址都没有确定。
注:nm命令可用来查看ELF文件的符号表信息。
-
生成可执行文件
$ gcc -o test test.o
$ nm test | egrep “main ∣ p r i n t f ∣ g l o b a l | printf|global ∣printf∣global”
080495a0 B global
08048354 T main
U printf@@GLIBC_2.0
经链接,global和main的地址都已经确定了,但是printf却还没,因为它是动态链接库glibc中定义函数,需要动态链接,而不是这里的“静态”链接。
重定位:是将符号引用与符号定义进行链接的过程
从上面的演示可以看出,重定位文件test.o
中的符号地址都是没有确定的,而经过“静态"链接(gcc
默认调用ld
进行链接)以后有两个符号地址已经确定了,这样一个确定符号地址的过程实际上就是链接的实质。链接过后,对符号的引用变成了对地址(定义符号时确定该地址)的引用,这样程序运行时就可通过访问内存地址而访问特定的数据。
我们也注意到符号printf在可重定位文件和可执行文件中的地址都没有确定,这意味着该符号是一个外部符号,可能定义在动态链接库中,在程序运行时需要通过动态链接器(ld-linux.so
)进行重定位,即动态链接。
通过这个演示可以看出printf确实在glibc中有定义。
$ nm -D /lib/`uname -m`-linux-gnu/libc.so.6 | grep " printf$"
0000000000053840 T printf
除了nm
以外,还可以用readelf -s
查看.dynsym
表或者用objdump -tT
查看。
需要提到的是,用nm
命令不带-D
参数的话,在较新的系统上已经没有办法查看libc.so的符号表了,因为nm默认打印常规符号表(在.symtab
和.strtab
节区中),但是,在打包时为了减少系统大小,这些符号已经被strip掉了,只保留了动态符号(在.dynsym
和.dynstr
中)以便动态链接器在执行程序时寻址这些外部用到的符号。而常规符号除了动态符号以外,还包含有一些静态符号,比如说本地函数,这个信息主要是调试器会用,对于正常部署的系统,一般会用strip工具删除掉。
关于nm
与readelf -s
的详细比较,可参考