C/C++编译技术——加载程序执行阶段
-
shell
工作过程:当用户输入命令名并按回车键时,shell首先将输入的命令名与其自身内置的命令进行比对。如果程序名不是shell支持的命令,那么shell就会尝试定位文件名与命令字符串完全匹配的二进制文件。如果用户只输入一个程序名(不是完整路径),shell会尝试定位由PATH环境变量所指定的目录中的可执行文件。当得到可执行二级制文件的路径后,shell就会启动加载和执行二进制文件的过程。
shell通过复制自身的进程内存映射来创建新进程的内存映射,在这个过程中将自身的环境变量传递给新进程。
-
内核
shell一旦委托了运行程序的任务,内核将通过调用exec函数族中的函数来做出响应。无论调用那种类型的exec函数,他们最终都会调用真正执行程序的sys_execve函数。
下一步操作是识别可执行文件格式,在
search_binary_handler
(位于文件fs/exec.c中)函数中执行。Linux除了支持最新的ELF二进制可执行格式外,还提供向后兼容性。如果识别出了ELF格式,下一步调用load_elf_binary
函数(位于文件fs/binfmt.c中)。当识别出可执行格式是系统所支持的格式后,则会为程序启动准备进程内存映射。
-
装载器
装载器的功能就是讲链接器创建的节复制到进程内存映射中。装载器还会根据节的相同装载需求将链接器创建的节组合成段,装载器的段一般会携带多个拥有相同访问属性的节。
程序加载阶段
当识别出二级制格式以后,内核装载器就会派上用场。首先装载器会定位可执行二进制文件中的PT_INTERP段,用于动态加载阶段。
接着装载器会读取程序的二进制文件段的头,确定每个段的地址和字节长度。
在这个阶段装载器不会向程序的内存映射写入任何数据,真正从可执行文件复制段的操作是在程序启动之后才执行的。在执行复制的时候,分配给进程的物理内存页和程序内存映射表之间的虚拟内存映射关系已经建立好了,只有当内核在运行时需要某一程序段才会开始加载其对应的页。
-
程序执行入口点
从通常编程视角看,程序的入口点是main函数,而对于程序执行本身而言,在执行到达main函数之前,还会先执行一些函数,这些函数为程序的运行做准备。
-
装载器查找入口点
在程序装载完成后,装载器会查询ELF头的e_entry字段的值。这个值包含的程序内存地址指定了程序该从何处开始运行。
e_entry值只包含了.text节的首地址,这个地址通常是_start函数的首地址。
-
_start()函数
_start函数的作用是为接下来需要调用的_libc_start_main函数准备入参。
-
_libc_start_main()函数
在程序启动阶段,该函数会为程序设置好运行环境,并执行下面操作:
- 启动程序的线程。
- 调用_init()函数,该函数会在调用main()函数前完成必要的初始化操作。
- 注册_fini()和rtld_fini()函数,这些函数会在程序终止时调用。
最后,当所有准备操作都完成时,_libc_start_main()调用main()函数,启动程序。
-