使用环境变量调试动态链接器

Linux 下的动态链接/装载器 (ld.so, ld-linux.so*)  支持通过环境变量来改变程序运行时的动态链接行为,灵活使用会给调试工作带来很多方便。

  • LD_BIND_NOT 标记;如果设定,则会执行延迟绑定,即决议 (resolve) 某个符号之后,不更新全局偏移表 (GOT) 和过程链接表 (PLT)
  • LD_BIND_NOW 标记;如果设定,则会指定立即绑定,即程序启动后决议所有的符号,并更新 GOT 和 PLT
  • LD_DEBUG 输出动态链接器的 debug 信息,设为 all 将会输出所有的调试信息,设为 help 将会输出帮助信息
  • LD_DEBUG_PATH 指定 LD_DEBUG 输出的文件路径;如果未指定,默认输出到标准输出
  • LD_DYNAMIC_WEAK 允许弱符号 (weak symbols) 被外部符号覆盖 (旧版 glibc 的行为)
  • LD_HWCAP_MASK 设置硬件、平台兼容性的掩码
  • LD_LIBRARY_PATH 一个用冒号分隔的列表,用来指定运行时动态库的搜索路径
  • LD_PRELOAD 一个用空格分隔的列表,用来指定程序在运行时首先被装载的共享库。可利用这个特性来覆盖其他共享库中定义的函数。对于设定了强制位 (setuid/setgid) 的可执行程序,只有在标准搜索路径里面并且也被设定强制位 (setuid) 的共享库才能被装载。
  • LD_ORIGIN_PATH 指向可执行文件的系统路径
  • LD_PROFILE 指定要分析的共享库
  • LD_PROFILE_OUTPUT 指定共享库分析结果的输出文件;如果未指定,默认输出到标准输出
  • LD_SHOW_AUXV 显示从内核传递的辅助数组
  • LD_TRACE_LOADED_OBJECTS 标记;如果设定,将会列出依赖性而不是实际运行(如 ldd)
  • LD_WARN 标记;如果设定,将会警告未定义符号
  • LD_VERBOSE 标记;如果设置,则在查询有关程序的信息时输出有关程序的符号版本控制信息

注:以上环境变量只支持 glibc 2.2 及以上版本。

通过 man 手册可以查看有关动态链接器的更详细的信息:

指定共享库搜索路径的几种方式

Linux 上有三种方式指定运行时动态链接库的搜索路径, 按照优先级顺序从高到低依次是:

  • rpath
  • LD_LIBRARY_PATH
  • runpath

假设我们有一个程序 a.out 动态链接 liba.so

然后将 liba.so 拷贝到两个独立的目录中:

目录结构如下

使用 -Wl,–enable-new-dtags 参数,通过 gcc 告诉 linker (ld, ld.gold, lld) 使用新的 dtags,即 runpath

运行时通过环境变量 LD_LIBRARY_PATH 指定共享库查找路径为目录 2;通过调试动态链接器,可以看到优先在目录 2 中寻找 liba.so,即 LD_LIBRARY_PATH 的优先级高于 runpath.

如果不指定 –enable-new-dtags 这个 linker 参数,或者使用 –disable-new-dtags,则会使用旧的 dtags,即 rpath

运行时通过环境变量 LD_LIBRARY_PATH 指定共享库查找路径为目录 2;通过调试动态链接器,可以看到优先在目录 1 中寻找 liba.so;即 rpath 的优先级高于 LD_LIBRARY_PATH.

 

C++ 多重继承的内存布局和指针偏移

在 C++ 程序里,在有多重继承的类里面。指向派生类对象的基类指针,其实是指向了派生类对象里面,该基类对象的起始位置,该位置相对于派生类对象可能有偏移。偏移的大小,等于派生类的继承顺序表里面,排在该类前面的所有的类的数据成员(含虚表指针)所占的空间大小总和。

下面以一个简单的程序为例,揭示有多重继承关系的派生类对象的内存布局:

注意该程序用 #pragma pack 指令指示数据在内存中按 4 字节来对齐。在 x64 平台上编译执行结果:

我们首先来分析这四个类的大小。

A 只有一个普通函数成员 foo,没有任何数据成员,是一个空类,其大小为 1 字节。之所以空类大小不为零,是需要标识类对象在内存中的位置,这 1 字节空间仅作占位用,不代表任何意义。

B 有一个成员变量 int b 和一个虚函数成员 func,其中 b 的大小为 4 字节。 由于存在虚函数,因此 B 类起始位置有一个虚表指针(vptr),在 64 平台上指针的大小为 8 字节。因此 B 的大小为 4 + 8 = 12 字节。

C 仅有一个成员变量 int c,因此其大小也就为 4 字节。

D 继承自A, B, C,它的大小等于 A, B, C 的所有数据成员的大小,加上其自身的数据成员和虚表指针的总的大小:4(b) + 4(c) + 4(d) + 8(vptr) = 20 字节。

注意:在 D 的继承关系链里面,只有基类 B 有虚函数,因此对于 D 对象而言,总体只有一个虚表指针,也就是(B)基类对象中的虚表指针。如果派生类的多个基类都有虚函数,则对应每个有虚函数的基类,在派生类对象里都有一个虚表指针。

因此,对于分析派生类 D 的对象,其内存布局如下:

分析结果与程序运行结果一致。