你可以在某次特定执行中临时替换一个不同的库。在 Linux 中,环境变量 LD_LIBRARY_PATH 会被第一个搜索,在标准目录集之前。在调试新库或者是为了特定目的而使用非标准库时特别有用。环境变量 LD_PRELOAD 的作用跟 /etc/ld.so.preload 一样。注意 LD_LIBRARY_PATH 可以在大多数类 Unix 系统上运行,但是并不是所有的都可以。例如它在 HP-UX 上叫 SHLIB_PATH ,在 AIX 上叫 LIBPATH 。
LD_LIBRARY_PATH 对于开发和调试都非常有用,但是不应该为了正常用户的正常使用而被安装进程修改。如果你不想修改 LD_LIBRARY_PATH 的值,在 Linux 下你可以直接调用 ld-linux.so ,如下我们使用 PATH 而不是 LD_LIBRARY_PATH 的值来运行可执行文件:
/lib/ld-linux.so.2 –library-path PATH EXECUTABLE
在 gnu C 加载器中还有个很有用的环境变脸 LD_DEBUG 。它能让 dl* 这类函数给出它们在做什么的详细信息。例如
export LD_DEBUG=files
command_to_run
显示加载动态链接库时处理的文件和其他依赖的库,例如检测的到依赖,库加载顺序等。设置 LD_DEBUG 等于 binding 会显示符号绑定的信息,设置为 libs 会显示库搜索路径,设置为 versions 会显示版本依赖信息。
还有一些其他的环境变量能控制加载的过程,它们的名字一般以 LD_ 或者 RTLD_ 打头。另外还有些是为了提供对加载器的底层调试用,或者是为了实现特殊的需求。关于这些没有正式的文档描述,所以如果你需要了解它们,最好的办法就是读加载器的源代码。
对于 setuid/setgid 的应用程序而言,在不采取特殊措施的前提下,允许用户控制动态链接库会有灾难性后果。所以在 GNU 加载器中,如果应用程序是 setuid 或 setgid ,这些变量会被忽略或者使用受到极大限制。加载器通过检测程序的授权来决定程序是否是 setuid 或 setgid 。如果 uid 和 euid 不同,或者是 gid 和 egid 不同,加载器会假设程序是 setuid 或 setgid ,并限制其控制链接的能力。如果你阅读 GNU glibc 的源代码,你会发现在文件 elf/rtlf.c 和 sysdeps/generic/dl-sysdep.c 中,如果 uid 和 gid 与 euid 和 egid 相等,然后调用程序,这些变量会完全起作用。其他类 Unix 系统会以不同的方式处理这种情况,但是为了相同的原因:一个 setuid/setgid 应用程序不应该被环境变量集过度影响。
创建共享链接库很简单,首先,用编译选项 -fPIC 或 -fpic 生成目标文件,这两个开关将会生成” position independent code“ ,这是共享链接库所必须的。然后还要传递通过 -Wl gcc 选项来传递 soname 到链接器中。注意用逗号分隔,中间不能有空格。如下所示:
gcc -shared -Wl,-soname,your_soname /
-library_name file_list_library_list
下面这段例子先创建两个目标文件 a.o 和 b.o ,然后创建共享链接库来包含它们。这条编译命令还包含调试信息 (-g) ,生成编译警告信息 (-Wall) ,这些编译参数对于共享链接库不是必需的,但是推荐使用。
Gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c
gcc -shared -Wl,-soname,libmystuff.so.1 /
-o libmystuff.so.1.0.1 a.o b.o -lc
下面有几点需要注意的:
-
不要精简生成的类库文件,也不要使用编译选项 -fomit-frame-pointer 除非万不得已。导致类库能够运行,但是调试器会基本失效;
-
使用 -fPIC 或 -fpic 生成代码。如何选择二者之一要依编译目标而定。 -fPIC 总是能凑效,但是产生的代码较 -fpic 要多。 -fpic 产生的代码又小又快,但是受平台依赖的限制。例如全局可见符号的数量或者代码文件的大小。
-
有些情况下,我们需要使用 gcc 的编译参数” -Wl,-export-dynamic” 。正常情况下, dynamic symbol table 只是包含那些被动态对象使用的符号。这个编译选项把所有的符号都添加到 dynamic symbol table 中。当存在反向依赖时你需要使用它,例如, DL 库有未解决的符号,而这些符号定义在加载库的程序中。为了让” reverse dependencies” 能起作用,主程序必须让它的符号动态可得。注意有时候你会看到我们在 Linux 中用 -rdynamic 代替 -Wl,export-dynamic 。但是据 ELF 文档描述,这种做法在非 Linux 的系统中对 gcc 并不总是起作用。
在开发阶段,试图修改被很多其他程序使用的库是有潜在问题的。你当然不希望其他应用程序使用仍处于开发阶段的库,往往你会使用特定的应用程序来测试它。我们可以使用 ld 的链接选项 -rpath 。这个选项会标示该特定应用程序编译时的动态库搜索路径。对于 gcc ,我们可以用如下的方式引用 -rpath 参数:
-wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)
创建完共享链接库,你肯定想要安装它。一个简单的办法就是直接把库文件拷贝到标准目录 ( 例如 /usr/lib) 下,然后运行 ldconfig 。
第一步,你需要在某个地方创建共享链接库;接着,需要建立必要的从 soname 到 real name 的符号链接。最简单的办法就是运行如下命令:
ldconfig -n directory_with_shared_libraries
最后,你需要在编译程序时使用选项 -l 和 -L 告诉链接器你正在使用的静态或者动态链接库。
如果你不能或者不想把库安装在标准目录下,那么你需要换一种方法。一种方法是你可以简单地使用 gcc 的选项 -L 。另外你还可以使用 rpath ,如果只有特定的应用程序会使用这些位于非标准目录下的库。你也可以使用环境变量, LD_LIBRARY_PATH 。在 bash 下,你可以这样调用 my_program :
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program
你可以使用 ldd 命令来查看一个应用程序所使用的共享链接库。例如,如果你想看 ls 使用的共享链接库,可以这样:
ldd /bin/ls
一般你会看到一系列所依赖的 soname 和它们所指向的目录。实际中,所有的应用程序都会至少有两个依赖:
-
/lib/ld-linux.so.N(N 大于零,通常至少是 2) 。它就是程序加载器。
-
Libc.so.N(N 大于 5) 。这是 C 库。甚至其他语言也会使用 C 库。
特别提醒,不要使用 ldd 来查看不受信任的应用程序。在 ldd 的用户手册中讲, ldd 在某些场合运行时会设定一些特殊环境变量的值 ( 例如对于 ELF 对象, LD_TRACE_LOADED_OBJECTS) ,然后执行该应用程序。一个不受信任的应用程序可能会让 ldd 用户运行任意代码而不仅仅是简单地显示 ldd 信息。所以,为了安全起劲,不要使用 ldd 查看不受信任的应用程序。
对 c 而言,有四种基本原因会导致库不是二进制兼容的:
-
程序的行为改变并不符合原始规范;
-
导出数据项改变 ( 例外:在结构体末尾添加可选字段是可以的,只要这些结构体只是在库内被分配。 )
-
导出函数被移除;
-
导出函数的接口被改变。
如果能够避免这些因素,那么你就能保证库是二进制兼容的。
对 C++ 而言,情况更诡异。以上的问题仍然存在,另外还有更多的问题。原因是一些信息的实现被隐藏在编译后的代码中,导致一些不是很明显的依赖,特别是当你不知道 C++ 是怎样实现的时候。下面是一些为了保持二进制兼容性而不能在 C++ 中做的事:
-
添加虚函数覆盖,因为编译器会在编译期而不是链接期计算 SuperClass::virtualFunction() ;
-
添加或删除虚成员函数,这将会改变所有子类的 vtbl 的大小和布局;
-
改变任何成员变量的类型或移动任何能被内联成员函数访问的成员变量;
-
改变类继承结构,除了添加新的叶子类;
-
添加或删除私有成员变量,因为这会改变子类的大小和布局;
-
删除公共或保护成员函数,除非它们是内联的;
-
让公共或保护成员函数变成内联的;
-
改变内联函数的实现代码,除非老版本的应用程序仍然能继续运行;
-
在可移植代码中改变成员函数的访问属性,因为一些编译器会把访问属性 mangle 到生成的函数名中。