一、动态链接工具
ldd和ldconfig是动态链接的两个重要辅助工具,所谓“辅助”,是相对于真正的主角动态链接器ld.so,说它是工具,是只它相对于配置文件/etc/ld.so.conf文件。ldd不直接参与链接过程,它依赖于ld.so,但是ld.so不依赖于这个工具,事实上,ldd只是一个脚本,它在调用ld.so的时候传递了一些约定好的环境变量,从而使ld.so执行并打印一些内容出来。而ldconfig则间接的供ld.so使用,它构造的ld.so.cache文件中包含了系统一些特定文件夹中的所有的so文件,它还会根据so文件内部DT_SONAME动态标签来建立一些符号链接,并且生成ld.so.cache文件,在动态链接器查找so文件的时候,动态链接器在某些情况下(按照一定的顺序)会读取并解析这个文件的内容,从而得到一些so文件的绝对位置,毕竟在很多时候,可执行文件的DT_NEEDED中指明的so只有文件名而没有绝对路径。
二、ldd文件
1、实现方法
这个工具并不是一个可执行文件,而是一个非常简单的脚本。在我的
[root@Harry Desktop]# uname -a
Linux Harry 2.6.31.5-127.fc12.i686.PAE #1 SMP Sat Nov 7 21:25:57 EST 2009 i686 athlon i386 GNU/Linux
系统中,输出主要相关内容为
[root@Harry Desktop]# cat `which ldd`
#! /bin/bash
……
add_env="LD_TRACE_LOADED_OBJECTS=1 LD_WARN=$warn LD_BIND_NOW=$bind_now"
add_env="$add_env LD_LIBRARY_VERSION=\$verify_out"
add_env="$add_env LD_VERBOSE=$verbose"
if test "$unused" = yes; then
add_env="$add_env LD_DEBUG=\"$LD_DEBUG${LD_DEBUG:+,}unused\""
fi
# The following use of cat is needed to make ldd work in SELinux
# environments where the executed program might not have permissions
# to write to the console/tty. But only bash 3.x supports the pipefail
# option, and we don't bother to handle the case for older bash versions.
if set -o pipefail 2> /dev/null; then
try_trace() {
eval $add_env '"$@"' | cat
}
else
try_trace() {
eval $add_env '"$@"' 这里的$@展开为/lib/ld-linux.so.2 argument
}
fi
……
对于我们常见的ldd filename命令来说,它无条件添加的LD_TRACE_LOADED_OBJECTS=1 是最为关键的,也是整个ldd中无条件添加的一个导出环境变量,其它的都是可选变量,所以知道了这一点,我们就可以构造自己的ldd工具,例如:
[root@Harry Desktop]# LD_TRACE_LOADED_OBJECTS=1 /lib/ld-linux.so.2 /bin/ls
linux-gate.so.1 => (0x00110000)
librt.so.1 => /lib/librt.so.1 (0x003d4000)
libselinux.so.1 => /lib/libselinux.so.1 (0x003f4000)
libcap.so.2 => /lib/libcap.so.2 (0x00c62000)
libacl.so.1 => /lib/libacl.so.1 (0x00dd5000)
libc.so.6 => /lib/libc.so.6 (0x0020a000)
libpthread.so.0 => /lib/libpthread.so.0 (0x0038c000)
/lib/ld-linux.so.2 (0x001e8000)
libdl.so.2 => /lib/libdl.so.2 (0x00385000)
libattr.so.1 => /lib/libattr.so.1 (0x001cd000)
这里也就可以理解为什么在交叉开发环境中没有交叉的ldd工具的原因,因为ldd需要可执行程序真正的在环境上加载运行之后才能确定各种地址,而这个条件在交叉环境上无法满足。
2、glibc支持代码
glibc中对该功能的解析集中在glibc-2.7\elf\rtld.c文件中
①、环境变量解析
该文件中process_envvars (enum mode *modep)函数负责对环境变量的解析
static void
process_envvars (enum mode *modep)
{
char **runp = _environ;
char *envline;
enum mode mode = normal;
char *debug_output = NULL;
/* This is the default place for profiling data file. */
GLRO(dl_profile_output)
= &"/var/tmp\0/var/profile"[INTUSE(__libc_enable_secure) ? 9 : 0];
while ((envline = _dl_next_ld_env_entry (&runp)) != NULL)
{
size_t len = 0;
while (envline[len] != '\0' && envline[len] != '=')
++len;
if (envline[len] != '=')
/* This is a "LD_" variable at the end of the string without
a '=' character. Ignore it since otherwise we will access
invalid memory below. */
continue;
switch (len)
{
……
case 5:
/* Debugging of the dynamic linker? */
if (memcmp (envline, "DEBUG", 5) == 0)
{
process_dl_debug (&envline[6]);
break;
}
if (memcmp (envline, "AUDIT", 5) == 0)
process_dl_audit (&envline[6]);
break;
……
case 9:
/* Test whether we want to see the content of the auxiliary
array passed up from the kernel. */
if (!INTUSE(__libc_enable_secure)
&& memcmp (envline, "SHOW_AUXV", 9) == 0)
_dl_show_auxv ();
break;
……
case 12:
/* The library search path. */
if (memcmp (envline, "LIBRARY_PATH", 12) == 0)
{
library_path = &envline[13];
break;
}
……
case 20:
/* The mode of the dynamic linker can be set. */
if (memcmp (envline, "TRACE_LOADED_OBJECTS", 20) == 0)
mode = trace;
break;
}
这里高亮了一些比较有意思的行为,大家可以自己尝试一下,其中LD_DEBUG选项对于动态库的调试尤为有用。
②、模式的使用
if (__builtin_expect (mode, normal) == verify)
{
const char *objname;
const char *err_str = NULL;
struct map_args args;
bool malloced;
args.str = rtld_progname;
args.loader = NULL;
args.is_preloaded = 0;
args.mode = __RTLD_OPENEXEC;
(void) _dl_catch_error (&objname, &err_str, &malloced, map_doit,
&args);
if (__builtin_expect (err_str != NULL, 0))
/* We don't free the returned string, the programs stops
anyway. */
_exit (EXIT_FAILURE);
}
……
if (__builtin_expect (mode, normal) != normal)
{
/* We were run just to list the shared libraries. It is
important that we do this before real relocation, because the
functions we call below for output may no longer work properly
after relocation. */
……
for (l = main_map->l_next; l; l = l->l_next)
if (l->l_faked)
/* The library was not found. */
_dl_printf ("\t%s => not found\n", l->l_libname->name);
else if (strcmp (l->l_libname->name, l->l_name) == 0)
_dl_printf ("\t%s (0x%0*Zx)\n", l->l_libname->name,
(int) sizeof l->l_map_start * 2,
(size_t) l->l_map_start);
else
_dl_printf ("\t%s => %s (0x%0*Zx)\n", l->l_libname->name,
l->l_name, (int) sizeof l->l_map_start * 2,
(size_t) l->l_map_start);
_exit (0);
}
这里对于mode的解析和处理也比较直观,比较值得注意的是只要mode不是normal执行了操作之后都会执行_exit退出可执行程序,这意味着如果导出了某个使能trace模式(例如LD_TRACE_PRELINKING、LD_TRACE_LOADED_OBJECTS),那么所有的子进程都无法完成真正的功能,而是在打印完trace模式之后直接退出。
三、ldconfig
该文件是一个货真价实的可执行程序,源代码位于glibc-2.7\elf\ldconfig.c,它定义了cache文件的名称和格式,默认使用
#ifndef LD_SO_CACHE
# define LD_SO_CACHE SYSCONFDIR "/ld.so.cache"
#endif
配置文件。
1、文件格式
#define CACHEMAGIC "ld.so-1.7.0"
/* libc5 and glibc 2.0/2.1 use the same format. For glibc 2.2 another
format has been added in a compatible way:
The beginning of the string table is used for the new table:
old_magic
nlibs
libs[0]
...
libs[nlibs-1]
pad, new magic needs to be aligned
- this is string[0] for the old format
new magic - this is string[0] for the new format
newnlibs
...
newlibs[0]
...
newlibs[newnlibs-1]
string 1
string 2
...
*/
struct file_entry
{
int flags; /* This is 1 for an ELF library. */
unsigned int key, value; /* String table indices. */
};
struct cache_file
{
char magic[sizeof CACHEMAGIC - 1];
unsigned int nlibs;
struct file_entry libs[0];
};
这个格式大致来说还是比较简单的,只是我们现在看到的一般使用的是glibc-2.11.2\elf\cache.c:print_cache (const char *cache_name)中的
else if (format == 1)
{
printf (_("%d libs found in cache `%s'\n"),
cache_new->nlibs, cache_name);
/* Print everything. */
for (unsigned int i = 0; i < cache_new->nlibs; i++)
print_entry (cache_data + cache_new->libs[i].key,
cache_new->libs[i].flags,
cache_new->libs[i].osversion,
cache_new->libs[i].hwcap,
cache_data + cache_new->libs[i].value);
}
这一分支。我们看一下系统中打印以及计算方法:
[root@Harry Desktop]# ldconfig -p | more
1074 libs found in cache `/etc/ld.so.cache'
libz.so.1 (libc6) => /lib/libz.so.1
libz.so (libc6) => /usr/lib/libz.so
libx86.so.1 (libc6) => /usr/lib/libx86.so.1
libxul.so (libc6) => /usr/lib/xulrunner-1.9.1/libxul.so
[root@Harry Desktop]# hexdump -C /etc/ld.so.cache | more
00000000 6c 64 2e 73 6f 2d 31 2e 37 2e 30 00 2a 04 00 00 |ld.so-1.7.0.*...|
00000010 03 00 00 00 e0 64 00 00 ea 64 00 00 03 00 00 00 |.....d...d......|
根据代码计算方法,此时真正偏移为
0x00000010 + 0x42a* sizeof (struct file_entry)=0x00000010 + 0x42a*0xC=0x3208
[root@Harry Desktop]# hexdump -Cs 0x3208 /etc/ld.so.cache | more
00003208 67 6c 69 62 63 2d 6c 64 2e 73 6f 2e 63 61 63 68 |glibc-ld.so.cach|
00003218 65 31 2e 31 32 04 00 00 51 b7 00 00 00 00 00 00 |e1.12...Q.......|
00003228 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
其中0x432==1074,也就是ldconfig -p显示的cache项个数目。
2、ldconfig简单功能
它会找到so文件中的DT_SONAME节,然后建立一些符号链接,从而可以完成快速搜索和引用。它同样会对自己收集的so文件进行排序,但是排序的标准并不是完全的字符比较,同样还要参考其他的信息,我看了一下我的系统的ldconfig -p 输出,是按照逆序排列的,其真正使用的比较函数为glibc-2.7\elf\cache.c。
3、cache文件何时使用
在加载一个共享库时,其搜索顺序为:
_dl_map_object (struct link_map *loader, const char *name, int preloaded,
int type, int trace_mode, int mode, Lmid_t nsid)
……
/* When the object has the RUNPATH information we don't use any
RPATHs. */
if (loader == NULL || loader->l_info[DT_RUNPATH] == NULL)如果定义了DT_RUNPATH,忽略DT_RPTAH,其中DT_RUNPATH好像是比较新引入的概念,关于这两点,之后可以再讨论,默认使用-rpath添加的路径都放入DT_RPATH中。
{
for (l = loader; l; l = l->l_loader)
if (cache_rpath (l, &l->l_rpath_dirs, DT_RPATH, "RPATH"))从DT_RPATH指明文件夹下搜索。
{
fd = open_path (name, namelen, preloaded, &l->l_rpath_dirs,
&realname, &fb, loader, LA_SER_RUNPATH,
&found_other_class);
if (fd != -1)
break;
did_main_map |= l == main_map;
}
}
/* Try the LD_LIBRARY_PATH environment variable. */
if (fd == -1 && env_path_list.dirs != (void *) -1)
fd = open_path (name, namelen, preloaded, &env_path_list,从LD_LIBRARY_PATH中指明的环境变量中查找。
&realname, &fb,
loader ?: GL(dl_ns)[LM_ID_BASE]._ns_loaded,
LA_SER_LIBPATH, &found_other_class);
……
if (fd == -1 && loader != NULL
&& cache_rpath (loader, &loader->l_runpath_dirs,从DT_RUNPATH中搜索。
DT_RUNPATH, "RUNPATH"))
fd = open_path (name, namelen, preloaded,
&loader->l_runpath_dirs, &realname, &fb, loader,
LA_SER_RUNPATH, &found_other_class);
……
/* Check the list of libraries in the file /etc/ld.so.cache,
for compatibility with Linux's ldconfig program. */
const char *cached = _dl_load_cache_lookup (name);从cache文件中搜索。
……
/* Finally, try the default path. */搜索系统默认文件夹下so文件,这个文件夹一般为/lib和/usr/lib两个系统文件夹。这意味着放入系统默认文件夹/usr和/usr/lib下的so文件不需要重新执行ldconfig就可以被动态链接器找到。
if (fd == -1
&& ((l = loader ?: GL(dl_ns)[nsid]._ns_loaded) == NULL
|| __builtin_expect (!(l->l_flags_1 & DF_1_NODEFLIB), 1))
&& rtld_search_dirs.dirs != (void *) -1)
fd = open_path (name, namelen, preloaded, &rtld_search_dirs,
&realname, &fb, l, LA_SER_DEFAULT, &found_other_class);
4、ldconfig何时执行
在系统启动的时候会执行一次,之后如果在系统文件夹中添加新的文件,需要手动执行ldconfig。我在我使用的fedora core发行版本中没有找到在哪个脚本中执行了这个脚本,只是在etc文件夹搜索到一个ldconfig字符串。
[root@Harry Desktop]# grep -r ldconfig /etc/selinux/targeted/
/etc/selinux/targeted/modules/active/file_contexts:/var/cache/ldconfig(/.*)?system_u:object_r:ldconfig_cache_t:s0
/etc/selinux/targeted/modules/active/file_contexts:/sbin/ldconfig -- system_u:object_r:ldconfig_exec_t:s0
……
删掉该文件的这一行,重启系统,放在标准/usr/lib下的so文件不会被添加到cache文件中,所以猜测自动加载可能和这一项有关。
5、DT_RUNPATH和DT_RPATH区别
简单看了一下ld的代码(链接器有些代码是动态生成的,所以有些比较晦涩)。看到通常-rpath只放入DT_RPATH标签中,这也是默认行为,但是如果在命令行上通过enable-new-tags选项,那么这个目录在放入DT_RPATH之后,还会在DT_RUNPATH中放入一份,并且两者完全相同。下面是一个测试例子:
[tsecer@Harry RUNPATHAndRPath]$ ls
foo.c Makefile
[tsecer@Harry RUNPATHAndRPath]$ cat Makefile
test:normal newtag
readelf -d normal.so
readelf -d newtag.so
normal:foo.o
ld -shared -o normal.so foo.o -rpath $(shell pwd)
newtag:foo.o
ld -shared -o newtag.so foo.o -rpath $(shell pwd) --enable-new-dtags
foo.o:foo.c
gcc -fPIC -c $< -o $@
clean:
rm -f *.o *.so
[tsecer@Harry RUNPATHAndRPath]$ make
gcc -fPIC -c foo.c -o foo.o
ld -shared -o normal.so foo.o -rpath /home/tsecer/CodeTest/RUNPATHAndRPath
ld -shared -o newtag.so foo.o -rpath /home/tsecer/CodeTest/RUNPATHAndRPath --enable-new-dtags
readelf -d normal.so
Dynamic section at offset 0x17c contains 7 entries:
Tag Type Name/Value
0x0000000f (RPATH) Library rpath: [/home/tsecer/CodeTest/RUNPATHAndRPath]
0x00000004 (HASH) 0xb4
0x00000005 (STRTAB) 0x12c
0x00000006 (SYMTAB) 0xdc
0x0000000a (STRSZ) 67 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000000 (NULL) 0x0
readelf -d newtag.so
Dynamic section at offset 0x17c contains 8 entries:
Tag Type Name/Value
0x0000000f (RPATH) Library rpath: [/home/tsecer/CodeTest/RUNPATHAndRPath]
0x0000001d (RUNPATH) Library runpath: [/home/tsecer/CodeTest/RUNPATHAndRPath]
0x00000004 (HASH) 0xb4
0x00000005 (STRTAB) 0x12c
0x00000006 (SYMTAB) 0xdc
0x0000000a (STRSZ) 67 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000000 (NULL) 0x0
[tsecer@Harry RUNPATHAndRPath]$
从输出可以看到,当使能了--enable-new-dtags选项之后,最终的文件中除了DT_RPATH之外,还有DT_RUNPATh,两者相同。但是即使这样,它将会影响这个-rpath路径在动态链接器中对LD_LIBRARY_PATH的前后搜索顺序,因为如果定义了DT_RUNPATH,那么LD_LIBRARY_PATH在最开始,否则rpath在最开始。链接器中相关处理代码 BinUtils\binutils-2.21.1\bfd\elflink.c:bfd_elf_size_dynamic_sections
if (rpath != NULL)
{
bfd_size_type indx;
indx = _bfd_elf_strtab_add (elf_hash_table (info)->dynstr, rpath,
TRUE);
if (indx == (bfd_size_type) -1
|| !_bfd_elf_add_dynamic_entry (info, DT_RPATH, indx))
return FALSE;
if (info->new_dtags)
{
_bfd_elf_strtab_addref (elf_hash_table (info)->dynstr, indx);
if (!_bfd_elf_add_dynamic_entry (info, DT_RUNPATH, indx))
return FALSE;
}
}
6、ld.so.cache文件格式
无论是
struct file_entry
{
int flags; /* This is 1 for an ELF library. */
unsigned int key, value; /* String table indices. */
};
还是
struct file_entry_new
{
int32_t flags; /* This is 1 for an ELF library. */
uint32_t key, value; /* String table indices. */
uint32_t osversion; /* Required OS version. */
uint64_t hwcap; /* Hwcap entry. */
};
它们最为核心的就是 key和value,其中key是不带路径的so文件,而value则是so文件的绝对路径。由于搜索文件夹并不唯一,所以同一名称的so可能在多个文件夹中存在,所以这里的搜索代码有对这种情况的处理。
glibc-2.7\elf\dl-cache.c文件中
#define SEARCH_CACHE(cache) \
宏在二分查找的时候内部循环即是为了这种情况处理。