CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,我会很高兴~
exit
重要成员
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
首先要关注这个handlers的第二个参数,这里留下一个疑问:为什么这个函数带一个handler
struct exit_function_list *__exit_funcs = &initial;
__exit_funcs被定义为一个指向 exit_function_list 结构体的指针,并将其初始化为 initial 的地址,然后看看struct exit_function_list
struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};
然后就是这个exit_function
类型的结构体数组(数组中每一个元素都是结构体成员,不是结构体指针奥)
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
glibc-2.31/sysdeps/unix/sysv/linux/_exit.c,调用INLINE_SYSCALL就直接退出了
run_exit_handlers
run_exit_handlers()的主要工作就是调用exit_funcs中保存的各种函数指针
void attribute_hidden __run_exit_handlers(int status, struct exit_function_list **listp, bool run_list_atexit)
{
//首先释放线程局部储存, 即TLS
__call_tls_dtors();
//遍历exit_fundtion_list链表,链表种每个节点里又有一个函数指针数组,根据里面的函数类型进行调用
while (*listp != NULL)
{
struct exit_function_list *cur = *listp; //cur指向当前exit_function_list节点
//cur->idx表示cur->fns中有多少个函数,从后往前遍历
while (cur->idx > 0) //遍历exit_function_list节点中 析构函数数组fns[32]中的函数指针
{
const struct exit_function *const f = &cur->fns[--cur->idx]; //f指向对应析构函数的描述符
switch (f->flavor) //选择析构函数类型
{
void (*atfct)(void);
void (*onfct)(int status, void *arg);
void (*cxafct)(void *arg, int status); //先设置三种回调函数,再根据注册情况选择调用
case ef_free:
case ef_us:
break; //这两种类型不调用
//------------------at没有参数
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(atfct);
#endif
atfct();
break;
//--------------------直接调用,没有参数
//---------------------on类型的参数为注册时设定的参数
case ef_on:
onfct = f->func.on.fn; //设置函数指针
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(onfct);
#endif
onfct(status, f->func.on.arg);
break;
//---------------------调用这个函数指针
//----------------------cxa类型则先为设定时的参数,再为状态码
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE(cxafct);
#endif
cxafct(f->func.cxa.arg, status);
break;
}
}
*listp = cur->next; //listp指向下一个exit_function_list节点
//最后一个链表节点为libc .data段中的initial,不需要释放
//除此以外的节点都是malloc申请得到的, 所以需要释放
if (*listp != NULL)
free(cur);
}
if (run_list_atexit) //调用_atexit
RUN_HOOK(__libc_atexit, ());
_exit(status); //真正的exit系统调用,用户层面的析构完后就属于内存层面的析构了(也就是exit系统调用)
}
这里直接是调用注册好的函数,还得再看看怎么注册函数的
atexit
libc提供了一个接口: atexit()用来注册exit()调用的析构函数
/* DSO由GCC定义,用来识别模块的*/
extern void *__dso_handle __attribute__((__weak__));
/* 注册一个exit时调用的析构函数*/
int atexit(void (*func)(void))
{
return __cxa_atexit((void (*)(void *))func, NULL, &__dso_handle == NULL ? NULL : __dso_handle);
}
然后cxa_atexit()是对internal_atexit()的封装
//注册一个exit/共享库被卸载时调用的函数,只会被C++编译器生产的代码调用,C会通过atexit调用(环境细节,与主干知识关系不大)
int __cxa_atexit(void (*func)(void *), void *arg, void *d)
{
return __internal_atexit(func, arg, d, &__exit_funcs);
}
libc_hidden_def(__cxa_atexit) //隐藏内部函数
然后就是重头戏,internel_atexit()通过new_exitfn()找到一个在__exit_funcs链表上注册析构函数的位置, 然后进行写入
int attribute_hidden __internal_atexit(void (*func)(void *), void *arg, void *d, struct exit_function_list **listp)
{
struct exit_function *new = __new_exitfn(listp); //先在__exit_funcs链表上添加一个描述析构函数的结构体
if (new == NULL)
return -1;
#ifdef PTR_MANGLE
PTR_MANGLE(func);
#endif
//然后注册函数相关信息
new->func.cxa.fn = (void (*)(void *, int))func; //函数指针
new->func.cxa.arg = arg; //参数
new->func.cxa.dso_handle = d;
atomic_write_barrier();
new->flavor = ef_cxa; //类型
return 0;
}
重点关注下func 析构函数指针
和arg 参数指针
,把传进来的函数和参数写到__exit_funcs上就算注册完毕了
然后就是__new_exitfn函数,主要就是分配空间的,类似分配器,__internal_atexit就类似构造器
struct exit_function *__new_exitfn(struct exit_function_list **listp)
{
struct exit_function_list *p = NULL;
struct exit_function_list *l;
struct exit_function *r = NULL; //三个临时变量
size_t i = 0;
__libc_lock_lock(lock); //上锁
//-----------------------------首先寻找一个析构函数类型为ef_free的位置----------------------------------
for (l = *listp; l != NULL; p = l, l = l->next) //遍历链表,l指向当前节点, p指向l的前一个节点
{
for (i = l->idx; i > 0; --i) //搜索l中的函数指针数组fns[32]
if (l->fns[i - 1].flavor != ef_free) //有一个不是ef_free的就停止
break;
if (i > 0) //在l中找到了空闲的exit_function位置, 停止链表遍历
break;
l->idx = 0; //初始化析构函数数量
}
if (l == NULL || i == sizeof(l->fns) / sizeof(l->fns[0])) //没有找到空闲位置,一般来说i就等于数组的长度
{
/*
l==null 说明整个__exit_funcs中都没有ef_free
i == sizeof(l->fns) / sizeof(l->fns[0]) 说明对于l节点, fns已经全部遍历了, 都没找到ef_free
此时就需要插入一个新的exit_function_list节点
*/
if (p == NULL)
{
assert(l != NULL);
p = (struct exit_function_list *)calloc(1, sizeof(struct exit_function_list)); //申请一个结构体, p指向新节点
if (p != NULL)
{
p->next = *listp; //头插法, 再__exit_funcs中插入一个节点
*listp = p;
}
}
if (p != NULL) //分配成功
{
r = &p->fns[0]; //r指向新节点的第一个析构函数描述结构体
p->idx = 1;
}
}
else //找到空闲位置了, l节点中第i个为ef_free
{
r = &l->fns[i];
l->idx = i + 1;
}
/* 此时这个函数位置的类型从空闲(ef_free)变为使用中(ef_us), 等待写入函数指针 */
if (r != NULL)
{
r->flavor = ef_us;
++__new_exitfn_called;
}
__libc_lock_unlock(lock);
return r;
}
大概流程就是:尝试在__exit_funcs中找到一个exit_function类型的ef_free的位置, ef_free代表着此位置空闲 如果没找到, 就新建一个exit_function节点, 使用头插法插入__exit_funcs链表, 使用新节点的第一个位置作为分配到的exit_function结构体 设置找到的exit_function的类型为ef_us, 表示正在使用中, 并返回
程序启动流程
然后再看run_exit_handlers的流程,是在调用析构函数之后才注册新的exit_function,所以一开始调用的函数实在之前就注册了的,那就要从程序的启动开始看看
新程序的启动往往是通过libc中exe()系列函数进行的, exe系列函数最终都可以归纳为execve这个系统调用 在系统层面,kernel会检查这个文件的类型,确定是elf之后会为新进程分配页表, 文件描述符, task描述符等各种资源,然后解析这个elf文件, 把text data bss等段都映射到内存中,然后jmp到elf的入口点, 从而开始执行。
一般情况下是jmp到text段的首地址开始执行,但是现代ELF一般都使用了运行时重定位机制,text段还有些地址位置还没有确定(比如各种libc库函数的地址,编译时并不知道libc会被mmap到哪里),此时内核会转而jmp到这个elf指定的动态链接器(也就是常用的ld.so.2),由ld去重定位elf中相关地址后再jmp到elf的入口点。ld并不是直接执行main()函数, 因为有析构函数就必定有构造函数, 在进入main之前还需要进行程序构造操作
比如参数设置, 申请流缓冲区等操作,实际上ld会跳转到elf中的_start标号处, 这才是elf中第一个被执行的指令地址
_start标号处的程序由汇编编写, 对应libc中start.S文件, _start做的工作很少, 只会为__libc_start_main()设置好参数, 然后调用它,_start()会在编译的时候被链接入ELF文件中,而libc_start_main()定义在libc中, _start()通过PLT+GOT调用到libc_start_main(),这个才是程序真正的构造函数
.text:0000000000001360 ; void __fastcall __noreturn start(__int64, __int64, void (*)(void))
.text:0000000000001360 public start
.text:0000000000001360 start proc near ; DATA XREF: LOAD:0000000000000018↑o
.text:0000000000001360 ; __unwind {
.text:0000000000001360 F3 0F 1E FA endbr64
.text:0000000000001364 31 ED xor ebp, ebp
.text:0000000000001366 49 89 D1 mov r9, rdx ; rtld_fini
.text:0000000000001369 5E pop rsi ; argc
.text:000000000000136A 48 89 E2 mov rdx, rsp ; ubp_av
.text:000000000000136D 48 83 E4 F0 and rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000001371 50 push rax
.text:0000000000001372 54 push rsp ; stack_end
.text:0000000000001373 45 31 C0 xor r8d, r8d ; fini
.text:0000000000001376 31 C9 xor ecx, ecx ; init
.text:0000000000001378 48 8D 3D CA 00 00 00 lea rdi, main ; main
.text:000000000000137F FF 15 53 3C 00 00 call cs:__libc_start_main_ptr
ida比源码好看(),需要的参数都被符号标出来了。所以注册析构函数的关键点就在__libc_start_main()中,这个函数很复杂(所以才放到libc),我们只看关键部分
static int __libc_start_main(
int (*main)(int, char **, char **MAIN_AUXVEC_DECL), //参数: main函数指针
int argc, char **argv, //参数: argc argv
ElfW(auxv_t) * auxvec,
__typeof(main) init, //参数: init ELF的构造函数
void (*fini)(void), //参数: fini ELF的析构函数
void (*rtld_fini)(void), //参数: rtld_fini ld的析构函数
void *stack_end //参数: 栈顶
)
{
//...函数体;
}
大概流程:为libc保存一些关于main的参数,比如__environ…,通过atexit()注册fini 与 rtld_fini 这两个参数,调用init为main()进行构造操作,然后调用main()函数 我们主要关注这些
static int __libc_start_main(...)
{
/* 注册动态链接器(ld.so.2)的析构函数(重要) */
if (__glibc_likely(rtld_fini != NULL))
__cxa_atexit((void (*)(void *))rtld_fini, NULL, NULL);
/* 进行一些简单的libc初始化工作: 在libc中保存argc argv env三个参数 */
__libc_init_first(argc, argv, __environ);
/* 注册ELF的fini函数(不重要) */
if (fini)
__cxa_atexit((void (*)(void *))fini, NULL, NULL);
/* 如果ELF有构造函数的话, 那么先调用init() */
if (init)
(*init)(argc, argv, __environ MAIN_AUXVEC_PARAM);
/* 调用main() */
result = main(argc, argv, __environ MAIN_AUXVEC_PARAM);
/* 如果main()返回后, __libc_start_main()回帮他调用exit()函数 */
exit(result);
}
所以libc_start_mian()会在exit_funcs中放入下面两个函数:ELF的fini函数,ld的rtld_fini函数 然后会调用一个构造函数:init(),后面就是详细分析这几个要素
注册的参数
ELF把所有的构造函数的指针放在一个段: .init_array
中, 所有的析构函数的指针放在一个段 .fini_array
中(只有fini与init的话, ELF只能有一个构造/ 析构函数,那么ELF应对的场景就很单一)
所以这里的init就负责遍历.init_array, 并调用其中的构造函数, 从而完成多个构造函数的调用
void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3)
{
signed __int64 v4; // rbp
__int64 i; // rbx
init_proc();
v4 = &_do_global_dtors_aux_fini_array_entry - &_frame_dummy_init_array_entry;
if ( v4 )
{
for ( i = 0LL; i != v4; ++i )
((void (__fastcall *)(_QWORD, __int64, __int64))*(&_frame_dummy_init_array_entry + i))(a1, a2, a3); //调用构造函数
}
}
也就是常用的ret2csu的这里。ELF不负责fini(), 在ELF里它就是一个空函数, 那么析构函数只能由rtdl_fini来负责。rtdl_fini实际指向_dl_fini()函数, 源码在dl-fini.c文件中, 会被编译到ld.so.2中
这又是一个很复杂的函数,为了不在源码中迷失, 首先要知道_dl_fini()的功能就是调用进程空间中
所有模块
的析构函数。进程空间中的一个单独文件称之为模块(标记,这里不是很懂)
重点就是这个所谓的模块的结构了,它保存在rtld_global
这个结构体中,这同样是一个非常复杂的结构体,我们只看与模块相关的部分
struct rtld_global
{
#define DL_NNS 16
struct link_namespaces //命名空间,也就是所谓的模块
{
//每个模块用_ns_loaded描述, 这个命名空间中所映射的模块组成一个双向链表, _ns_loaded就是这个链表的指针
struct link_map *_ns_loaded;
/* _ns_loaded中有多少模块 */
unsigned int _ns_nloaded;
/* 映射模块的搜索表 */
struct r_scope_elem *_ns_main_searchlist;
size_t _ns_global_scope_alloc;
/* 这个命名空间中的符号表, 单个命名空间中的符号不允许重复 */
struct unique_sym_table
{
__rtld_lock_define_recursive(, lock) struct unique_sym
{
uint32_t hashval; //符号hash值
const char *name; //名称
const ElfW(Sym) * sym; //符号
const struct link_map *map; /* 所属模块 */
} * entries; //entries可以理解为struct unique_sym数组的指针, 通过entries[idx]就可找到第idx个符号
size_t size; //有多少个元素
size_t n_elements;
void (*free)(void *); /* 析构函数 */
} _ns_unique_sym_table;
struct r_debug _ns_debug; //调试信息
} _dl_ns[DL_NNS]; //一个数组的元素代表一个命名空间,代表一个link_namespace结构体
/* _dl_nns表示使用了多少个命名空间: Dynamic Link Num of NameSpace */
size_t _dl_nns;
//...;
}
一些缩写的含义:ns代表着NameSpace,nns代表着Num of NameSpace,rtld_global先以命名空间为单位建立了一个数组 _dl_ns[DL_NNS],在每个命名空间内部加载的模块以双向链表组织, 通过_ns_loaded索引 同时每个命名空间内部又有一个符号表_ns_unique_sym_table, 记录着所有模块导出的符号集合。namespace就是一个进程的集合, 这个进程集合中可以看到相同的全局资源, 并与其他命名空间独立
_dl_ns里重要的就是_ns_loaded也就是link_map指针。ELF文件都是通过节的组织的, ld自然也延续了这样的思路
struct link_map
{
ElfW(Addr) l_addr; /* 模块在内存中的的基地址 */
char *l_name; /* 模块的文件名 */
ElfW(Dyn) * l_ld; /* 指向ELF中的Dynamic节 */
struct link_map *l_next, *l_prev; /* 双向链表指针 */
struct link_map *l_real;
/* 这个模块所属NameSapce的idx */
Lmid_t l_ns;
struct libname_list *l_libname;
/*
l_info是ELF节描述符组成的的数组
ELF中一个节, 使用一个ElfW(Dyn)描述
各个类型的节在l_info中的下标固定, 因此可以通过下标来区分节的类型
*/
ElfW(Dyn) * l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
const ElfW(Phdr) * l_phdr; /* ELF的头表 */
ElfW(Addr) l_entry; /* ELF入口点 */
ElfW(Half) l_phnum; /* 头表中有多少节 */
ElfW(Half) l_ldnum; /* dynamic节中有多少描述符 */
//...;
}
l_info中的指针都指向ELF中Dyn节中的描述符, Dyn中节描述符类型是ElfW(Dyn)
typedef struct
{
Elf64_Sxword d_tag; /* 便签, 用于标注描述符类型 */
union
{
Elf64_Xword d_val; /* 内容可以是一个值 */
Elf64_Addr d_ptr; /* 也可以是一个指针 */
} d_un;
} Elf64_Dyn;
至此rtld_global的结构就清楚了, 他自顶向下按照: 命名空间->模块->节 的形式描述所有的模块, 通过_ns_unique_sym_table描述命名空间中所有的可见符号
理解了模块是如何组织的之后, _dl_fini的任务就显而易见了:1 遍历rtld_global中所有的命名空间, 2 遍历每个命名空间中的所有模块, 3 找到每个模块的fini_array段, 并调用其中的所有函数指针, 4 找到这个模块的函数指针然后调用,源码在dl-fini.c文件中