One*_*Day 8 c++ assembly gdb libc objdump
这是一个简单的程序:
void __attribute__ ((constructor)) dumb_constructor(){}
void __attribute__ ((destructor)) dumb_destructor(){}
int main() {}
Run Code Online (Sandbox Code Playgroud)
我用以下标志编译它:
g++ -O0 -fverbose-asm -no-pie -g -o main main.cpp
Run Code Online (Sandbox Code Playgroud)
我检查与gdb那个__libc_csu_init呼唤我标记瓦特/构造函数:
Breakpoint 1, dumb_constructor () at main.cpp:1
1 void __attribute__ ((constructor)) dumb_constructor(){}
(gdb) bt
#0 dumb_constructor () at main.cpp:1
#1 0x000000000040116d in __libc_csu_init ()
#2 0x00007ffff7abcfb0 in __libc_start_main () from /usr/lib/libc.so.6
#3 0x000000000040104e in _start ()
Run Code Online (Sandbox Code Playgroud)
我认为该destructor属性意味着dumb_destructor()将在 期间调用__libc_csu_fini,但这并没有发生:
Breakpoint 1, dumb_destructor () at main.cpp:3
3 void __attribute__ ((destructor)) dumb_destructor(){}
(gdb) bt
#0 dumb_destructor () at main.cpp:3
#1 0x00007ffff7fe242b in _dl_fini () from /lib64/ld-linux-x86-64.so.2
#2 0x00007ffff7ad4537 in __run_exit_handlers () from /usr/lib/libc.so.6
#3 0x00007ffff7ad46ee in exit () from /usr/lib/libc.so.6
#4 0x00007ffff7abd02a in __libc_start_main () from /usr/lib/libc.so.6
#5 0x000000000040104e in _start ()
Run Code Online (Sandbox Code Playgroud)
我检查__libc_csu_fini了 objdump 真的没有做任何事情,它确实是一个存根:
0000000000401190 <__libc_csu_fini>:
401190: f3 0f 1e fa endbr64
401194: c3 ret
Run Code Online (Sandbox Code Playgroud)
为什么叫这个_dl_fini?什么是_dl_fini?为什么不一致而不是调用__libc_csu_fini?
Jan*_*ann 13
我指的是撰写本文时最新的 glibc 版本标签,即 glibc 2.34(于 2021 年 8 月发布),它对启动过程进行了相当多的更改(我强调了主要差异)。大多数发现也应该适用于其他版本和架构。此答案中的 ELF 转储来自 x86-64 系统。
在研究析构函数之前,我们必须了解启动时发生了什么。
为了简洁起见,我在这里跳过了一些内核模式部分。我们从程序的 ELF 文件已经根据其段(“程序头”)表映射到内存的点开始:
$ readelf -l a.out
Elf file type is DYN (Shared object file)
Entry point 0x10a0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000215 0x0000000000000215 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000001a0 0x00000000000001a0 R 0x1000
LOAD 0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
0x0000000000000268 0x0000000000000270 RW 0x1000
...(and a few more)
Run Code Online (Sandbox Code Playgroud)
我们的应用程序是动态链接的(即,ELF 文件不包含它调用的所有函数),因此我们还必须将所有依赖项加载到进程的虚拟地址空间中。然而,内核本身对ELF格式的理解有限,无论如何也不应该对用户空间环境做出太多假设。因此,ELF指定了一个特殊的解释程序,该程序的路径可以在段中找到INTERP。
在 Linux 上,这通常恰好是动态链接器 lib64/ld-linux-x86-64.so.2。随后,内核将该动态链接器 ELF 加载到与我们的应用程序相同的虚拟地址空间中,然后调用动态链接器的入口点(不是我们应用程序的入口点)。
动态链接器现在读取我们程序的DYNAMIC段(动态表),其中包含有关所需依赖项、符号表、重定位等的信息:
$ readelf -d a.out
Dynamic section at offset 0x2dc8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1208
0x0000000000000019 (INIT_ARRAY) 0x3da8
0x000000000000001b (INIT_ARRAYSZ) 16 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3db8
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3a0
0x0000000000000005 (STRTAB) 0x470
0x0000000000000006 (SYMTAB) 0x3c8
0x000000000000000a (STRSZ) 130 (bytes)
...(and a few more)
Run Code Online (Sandbox Code Playgroud)
有了这些信息,它就开始NEEDED递归地访问我们程序的所有依赖项。对于每个依赖项,执行以下步骤:
dl_init,它调用INIT/INIT_ARRAY动态表条目中的所有函数(即库的构造函数)。一旦动态链接器完成并且所有依赖项都已加载并初始化,它将控制权移交给我们应用程序的入口点 ( _start)。
_start获取一些参数,最显着的是指向_dl_finiin 的函数指针rdx。_start然后准备堆栈,将一些参数放入寄存器中,最后调用__libc_start_main.
__libc_start_main接收以下参数:
main(这是main我们编写的方法)argc,argvinit(指向__libc_csu_initglibc 2.34 之前的版本)fini(指向__libc_csu_finiglibc 2.34 之前的版本)rtld_fini(等于rdx的参数_start,因此指向_dl_fini)该函数对 libc 进行一些初始化,设置线程本地存储和堆栈金丝雀,等等。这里我们只关心两个调用:
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);,它注册_dl_fini为析构函数以在程序退出后运行init( __libc_csu_init< glibc 2.34)或( > =call_init glibc 2.34)__libc_csu_init和两者call_init基本上做相同的事情:它们运行在动态表条目INIT和中注册的所有构造函数INIT_ARRAY。然而, while__libc_csu_init被静态编译到我们的程序中,call_init位于 libc 中,因此位于不同的内存区域中。在安全研究人员在的汇编代码中发现 ROP 小工具后,这一情况发生了变化。__libc_csu_init
因此,我们观察到每个构造函数的以下回溯:
my_constructor()__libc_csu_init()(< glibc 2.34)或 call_init()(>= glibc 2.34)__libc_start_main()_start()__libc_start_main完成后,它将控制权转移给我们的main方法:
_Noreturn static __always_inline void
__libc_start_call_main (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv MAIN_AUXVEC_DECL)
{
exit (main (argc, argv, __environ MAIN_AUXVEC_PARAM));
}
Run Code Online (Sandbox Code Playgroud)
我们现在已经看到了初始化可执行文件时会发生什么。但结局又如何呢?
正如我们在上面的代码片段中看到的,exit一旦main返回就运行。那么它有什么exit作用呢?
事实证明,它只将控制权转移到__run_exit_handlers:
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
Run Code Online (Sandbox Code Playgroud)
__run_exit_handlers__exit_funcs然后通过诸如 之类的调用调用已在列表中注册的各种函数__cxa_atexit。如果我们现在回顾启动过程,我们会发现该列表还应该包含我们的_dl_fini函数,因为它作为rtld_fini参数传递给_start/ __libc_start_main!
_dl_fini是动态链接器的终结器,它迭代所有依赖项和FINI我们的可执行文件,并从FINI_ARRAY它们中运行析构函数。
因此,我们得到每个析构函数的以下回溯:
my_destructor()_dl_fini()__run_exit_handlers()exit()__libc_start_main()_start()这回答了“什么”,但没有回答“为什么”。
__libc_csu_fini?(请对以下内容持保留态度 - 我找不到原始推理的来源,但从源代码、提交消息和一些评论中推断出这一点)
我相信实际上相反的目的是:更加一致。动态链接器负责运行所有依赖项的构造函数,因此它还应该运行它们的析构函数。由于我们的程序与这些依赖项没有太大区别,为什么不运行它的析构函数呢?大概这就是17年前残疾__libc_csu_fini的原因。我不确定为什么它没有完全删除 - 可能是为了保持与现有编译器的兼容性。
在最近发布的 glibc 2.34 中,__libc_csu_init和__libc_csu_fini函数都被完全删除,因为它们的任务现在由运行时的其他部分完成。
dl_init?好吧,dl_init在我们应用程序的入口点之前运行_start- 运行时的几个重要部分尚不可用(初始化在 中完成__libc_start_main)。因此,我们的构造函数需要是独立的,并避免调用外部函数。由于这会给可靠性和安全性带来相当大的风险,因此构造函数会在所有其他初始化完成后执行。
实际上,支持由以下命令执行的初始化函数- 这些函数可以通过和动态表条目dl_init指定,并在我们的函数之前运行。然而,似乎没有一种直接的方法来向编译器注册它们,而且出于上述原因,无论如何也不建议这样做。PREINITPREINIT_ARRAY_start
注意:回答这个问题需要深入研究 glibc 的内部工作原理,结果比我最初预期的还要复杂。为了使这个答案变得连贯,我必须简化一些事情并跳过其他事情。如果您发现任何不准确的内容,请随时编辑或在评论中提出。