Linux中程序是怎样启动的
2022/9/11 5:23:23
本文主要是介绍Linux中程序是怎样启动的,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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; } } } ...; }
这篇关于Linux中程序是怎样启动的的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-18git仓库有更新,jenkins 自动触发拉代码怎么配置的?-icode9专业技术文章分享
- 2024-12-18Jenkins webhook 方式怎么配置指定的分支?-icode9专业技术文章分享
- 2024-12-13Linux C++项目实战入门教程
- 2024-12-13Linux C++编程项目实战入门教程
- 2024-12-11Linux部署Scrapy教程:新手入门指南
- 2024-12-11怎么将在本地创建的 Maven 仓库迁移到 Linux 服务器上?-icode9专业技术文章分享
- 2024-12-10Linux常用命令
- 2024-12-06谁看谁服! Linux 创始人对于进程和线程的理解是…
- 2024-12-04操作系统教程:新手入门及初级技巧详解
- 2024-12-04操作系统入门:新手必学指南