动态链接器(九):.init和.init_array

[复制链接]
发表于 2025-10-25 03:55:42 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
ELF文件中的.init和.init_array段是步伐初始化阶段的告急构成部门,用于在main函数实行前完成须要的初始化操纵。
1 .init段和.init_array 段

1.1 作用

.init段包罗编译器天生的初始化代码,通常由运行时环境(如C标准库的启动例程)直接调用。这些代码负责实行根本的初始化使命,比方设置全局非常处理处罚、初始化堆栈或准备步伐运行环境。
.init_array是一个函数指针数组,存储了全部须要在main之前实行的初始化函数。这些函数通常由用户通过__attribute__((constructor))显式界说,或由编译器隐式天生(如C++全局对象的构造函数)。它支持多个初始化函数,按优先级序次实行。GCC答应通过__attribute__((constructor(priority)))指定优先级(数值越小越早实行),链接器会按优先级归并到.init_array的子段(如.init_array.0、.init_array.1)中。
PS:在早期的ELF实现中,用户可以通过_init()函数界说初始化逻辑,但今世工具链更保举利用.init_array。
1.2 实行

对于可实行步伐,在步伐入口(如_start)调用main函数之前,.init中的代码会起首实行,在.init段代码实行后,.init_array中的函数会按序次依次实行。musl中的libc_start_init函数就是负责实行.init和.init_array中的代码的:
  1. static void libc_start_init(void)
  2. {
  3.         _init();
  4.         uintptr_t a = (uintptr_t)&__init_array_start;
  5.         for (; a<(uintptr_t)&__init_array_end; a+=sizeof(void(*)()))
  6.                 (*(void (**)(void))a)();
  7. }
复制代码
注意这个_init()函数就是1.1中所说的用户界说的_init()函数,它是一个弱符号,如果用户没有界说_init()函数,它就会利用musl中的默认实现(一个空函数,什么也不做):
  1. static void dummy(void) {}
  2. weak_alias(dummy, _init);
复制代码
固然不光可实行步伐有这两个段,动态库中也有这两个段:

对于动态库,这两个段中的函数是由动态链接器举行调用的。在动态链接器完成对动态库的加载和重定位后,就会调用这两个段中的函数。

上面都是对单一的elf文件(动态库、可实行文件)来说,但通常来说elf文件都会有本身的依赖库,此时elf文件和其依赖库的初始化(即调用.init和.init_array中的函数,下文会多次利用初始化这个词语)序次要满足拓扑排序(先调用依赖库的初始化函数,再调用自身的初始化函数),就像下面这个图所示(这是由动态链接器自主完成的,用户不须要费心):

详细细节可以参考:
https://refspecs.linuxfoundation.org/elf/elf.pdf
2 一个由Glibc的独家秘方导致的Bug

按照规范来说.init和.init_array中的函数是不须要转达参数的,但glibc做了扩展,它会向这些函数转达三个参数:argc,argv,envp。
PS:我推测它如许做的目的是为了让动态库也可以或许直接利用argc,argv,不如许做的话动态库是没法直接拿到argc和argv的,除非导出一个函数,由可实行步伐转达。(envp是可以拿到的,通过environ全局变量)。下面是glibc中实行.init和.init_array段中函数的代码,可以看到它传了这三个参数:
  1.   ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY];
  2.   if (init_array != NULL)
  3.     {
  4.       unsigned int j;
  5.       unsigned int jm;
  6.       ElfW(Addr) *addrs;
  7.       jm = l->l_info[DT_INIT_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr));
  8.       addrs = (ElfW(Addr) *) (init_array->d_un.d_ptr + l->l_addr);
  9.       for (j = 0; j < jm; ++j)
  10.         ((dl_init_t) addrs[j]) (argc, argv, env);
  11.     }
复制代码
在我实现动态链接器时,我告急参考的是musl的代码,我并不知道glibc对初始化函数做了扩展,于是Bug就产生了。在gnu linux环境下Rust std会利用到glibc的这个扩展,这导致我的动态链接器加载的动态库利用std::env::args()函数时会堕落。下面就是Rust std中利用到这个特性的地方:
  1. /// glibc passes argc, argv, and envp to functions in .init_array, as a non-standard extension.
  2. /// This allows `std::env::args` to work even in a `cdylib`, as it does on macOS and Windows.
  3. #[cfg(all(target_os = "linux", target_env = "gnu"))]
  4. #[used]
  5. #[link_section = ".init_array.00099"]
  6. static ARGV_INIT_ARRAY: extern "C" fn(
  7.         crate::os::raw::c_int,
  8.         *const *const u8,
  9.         *const *const u8,
  10. ) = {
  11.         extern "C" fn init_wrapper(
  12.                 argc: crate::os::raw::c_int,
  13.                 argv: *const *const u8,
  14.                 _envp: *const *const u8,
  15.         ) {
  16.                 unsafe {
  17.                         really_init(argc as isize, argv);
  18.                 }
  19.         }
  20.         init_wrapper
  21. };
复制代码
简单来说,Rust std会利用glibc的这个特性,在动态库初始化时设置几个全局变量,这几个全局变量中生存的就是argc,argv,envp的值。而我实现的动态链接器在调用初始化函数时不会转达这几个值,以是在动态链接器实行上面这段代码中的init_wrapper函数(它在.init_array中,是一个初始化函数)时,argc和argv传进来的都是垃圾值,而std::env::args()函数又会利用init_wrapper函数设置的全局变量,于是在被加载进来的动态库实行std::env::args()函数时,步伐就瓦解了。
Bug发现和修复的细节可以看下面这个链接:
std::env::args seems to require billions of bytes · Issue #3 · weizhiao/dlopen-rs · GitHubHi, I'm playing around with your crate and I noticed that if you create a file that looks like this: #[unsafe(no_mangle)] fn test() { let args = std::env::args(); } and a file main.rs that looks like this: use dlopen_rs::{ElfLibrary, Ope...
https://github.com/weizhiao/dlopen-rs/issues/3
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表