Linux中程序是怎样启动的
前言
- 新程序的启动往往是通过libc中exe()系列函数进行的, exe系列函数最终都可以归纳为execve这个系统调用
- 系统层面
- kernel会检查这个文件的类型
- 确定是elf之后会为新进程分配页表, 文件描述符, task描述符等各种资源
- 然后解析这个elf文件, 把text data bss等段都映射到内存中
- 然后jmp到elf的入口点, 从而开始执行
ELF的入口点_start()
-
由于现代ELF使用运行时重定位机制。即:运行钱各种libc库函数的地址,编译时不知道libc会被mmap到哪
-
因此如果kernel发现需要运行时重定位, 那么就会转而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()
ENTRY (_start) /* 编译时告诉链接器, 这里才是整个函数的入口点 */
/* Clearing frame pointer is insufficient, use CFI. */
cfi_undefined (rip)
/* 初始化栈底: %ebp=0 */
xorl %ebp, %ebp
/* 设置__libc_start_main的参数
调用__libc_start_main的参数会通过如下寄存器传递, 因为linux才用cdecl函数调用约定:
main: %rdi
argc: %rsi
argv: %rdx
init: %rcx
fini: %r8
rtld_fini: %r9
stack_end: stack. */
mov %RDX_LP, %R9_LP /* 设置参数rtld_fini */
#ifdef __ILP32__
mov (%rsp), %esi /* Simulate popping 4-byte argument count. */
add $4, %esp
#else
popq %rsi /* Pop argc */
#endif
/* 设置参数argv */
mov %RSP_LP, %RDX_LP
/* rsp对齐 */
and $~15, %RSP_LP
/* Push garbage because we push 8 more bytes. */
pushq %rax
/* Provide the highest stack address to the user code (for stackswhich grow downwards). */
pushq %rsp
#ifdef SHARED
/* 设置参数init和fini */
mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP
/* 设置参数main函数地址 */
mov main@GOTPCREL(%rip), %RDI_LP
/* 调用__libc_start_main()
__libc_start_main()进行一些构造工作, 然后调用main()
main() return到__libc_start_main之后 __libc_start_main会进行析构工作 */
call __libc_start_main@PLT
#else
/* Pass address of our own entry points to .fini and .init. */
mov $__libc_csu_fini, %R8_LP
mov $__libc_csu_init, %RCX_LP
mov $main, %RDI_LP
/* Call the user's main function, and exit with its value.
But let the libc call main. */
call __libc_start_main
#endif
hlt /* Crash if somehow `exit' does return. */
END (_start)
- 由此注册析构函数的关键点就在__libc_start_main()中
__libc_start_main
这个函数定义在libc源码的libc-start.c文件中, 由于比较复杂, 因此只分析关键部分
- 首先是其参数列表也就是_start()传递的参数, 我们中重点注意下面三个
- init: ELF文件 也就是main()的构造函数
- fini: ELF文件 也就是main()的析构函数
- rtld_fini: 动态链接器的析构函数
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_start_mian()主要做了以下几件事
- 为libc保存一些关于main的参数, 比如__environ…
- 通过atexit()注册fini 与 rtld_fini 这两个参数
- 调用init为main()进行构造操作
- 然后调用main()函数
_dl_fini()
- 我们把进程空间中的一个单独文件, 称之为模块
- ld.so.2会通过dl_open()把所需文件到进程空间中, 他会把所有映射的文件都记录在结构体_rtld_global中
- 当一个进程终止, ld.so.2自然需要卸载所映射的模块, 这需要调用每一个非共享模块的fini_arrary段中的析构函数
- 一言以蔽之: _dl_fini()的功能就是调用进程空间中所有模块的析构函数
_dl_fini的任务:
- 遍历rtld_global中所有的命名空间
- 遍历命名空间中所有的模块
- 找到这个模块的fini_array段, 并调用其中的所有函数指针
- 找到这个模块的fini段, 调用
void internal_function _dl_fini(void)
{
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) //遍历_rtld_global中的所有非共享模块: _dl_ns[DL_NNS]
{
__rtld_lock_lock_recursive(GL(dl_load_lock)); //对rtld_global上锁
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* 如果这个NameSapce没加载模块, 或者不需要释放, 就不需要做任何事, 就直接调用rtld中的函数指针释放锁 */
if (nloaded == 0 || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit)
__rtld_lock_unlock_recursive(GL(dl_load_lock));
else //否则遍历模块
{
/* 把这个命名空间中的所有模块指针, 都复制到maps数组中 */
struct link_map *maps[nloaded];
unsigned int i;
struct link_map *l;
assert(nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next) //遍历链表
if (l == l->l_real) /* Do not handle ld.so in secondary namespaces. */
{
assert(i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
/* Bump l_direct_opencount of all objects so that they are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
...;
unsigned int nmaps = i; //多少个模块
/* 对maps进行排序, 确定析构顺序 */
_dl_sort_fini(maps, nmaps, NULL, ns);
//释放锁
__rtld_lock_unlock_recursive(GL(dl_load_lock));
/* 从前往后, 析构maps中的每一个模块 */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];
if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
/* 是否包含fini_array节, 或者fini节 */
if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL)
{
/* debug时打印下相关信息 */
if (__builtin_expect(GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf("/ncalling fini: %s [%lu]/n/n",DSO_FILENAME(l->l_name),ns);
/* 如果有fini_array节的话 */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
/*
l->l_addr: 模块l的加载基地址
l->l_info[DT_FINI_ARRAY]: 模块l中fini_array节的描述符
l->l_info[DT_FINI_ARRAY]->d_un.d_ptr: 模块l中fini_arrary节的偏移
array: 为模块l的fini_array节的内存地址
*/
ElfW(Addr) *array = (ElfW(Addr) *)(l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
/*
ELF中 fini_arraysz节用来记录fini_array节的大小
l->l_info[DT_FINI_ARRAYSZ]: 模块l中fini_arraysz节描述符
l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val: 就是fini_array节的大小, 以B为单位
i: fini_array节的大小/一个指针大小, 即fini_array中有多少个析构函数
*/
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof(ElfW(Addr)));
while (i-- > 0) //从后往前, 调用fini_array中的每一个析构函数
((fini_t)array[i])();
}
/* 调用fini段中的函数 */
if (l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}
...;
}
/* Correct the previous increment. */
--l->l_direct_opencount;
}
}
}
...;
}
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/288739.html