7.2. 操作系统加载ELF文件
在LoongArch架构中,操作系统加载并运行ELF文件,通常由execve()系统调用触发。整个流程先在内核态完成解析和加载,再由用户态动态链接器接管,最后跳转到程序入口点。
7.2.1. 系统调用入口
在Shell中输入命令后,Shell会通过fork()创建新进程,并在新进程中调用execve(),同时传入程序路径和参数。进入内核后,该调用会经过一系列函数,最终到达内核的二进制格式处理程序(binfmt)。
execve()调用链:sys_execve() -> do_execve() -> search_binary_handler() -> load_elf_binary()。
在这一过程中,Linux内核会销毁当前进程原有地址空间,并用新程序内容替换。
7.2.2. 内核空间加载与分析
内核获得新程序文件后,开始执行实际加载工作。
识别与解析:内核首先读取ELF文件头(Header),检查文件开头的魔数(
\x7fELF),确认文件是合法ELF格式。映射(Map)内存段:内核根据ELF程序头表(Program Header Table)解析类型为
PT_LOAD的段。这些段包含程序代码(.text)和数据(.data),内核通过mmap相关机制把它们映射到新进程的内存空间。处理解释器(PT_INTERP):主程序加载后,内核会检查程序头中是否存在
PT_INTERP段。该段主要保存动态链接器路径。存在
PT_INTERP段:说明这是动态链接可执行文件。内核会自动加载该段指定的动态链接器,例如LoongArch下的ld-linux-loongarch-lp64d.so.1,并准备把控制权交给它。不存在
PT_INTERP段:对于静态链接可执行文件,内核加载工作到此完成,并直接跳转到ELF文件指定的入口点(e_entry)。
内核会为新进程设置栈空间,并把一些关键辅助信息(Auxiliary Vector)放到栈上,例如程序头地址(
AT_PHDR)、动态链接器基址(AT_BASE)等,供用户态程序和动态链接器使用。
7.2.3. 用户态的动态链接器 (ld.so)
如果程序采用动态链接,CPU会跳转到动态链接器入口点开始执行。
自我初始化:动态链接器首先完成自身重定位,确保自己能够正常运行。
加载共享库:动态链接器读取主程序的
.dynamic段,找到所有依赖共享库(DT_NEEDED条目),再递归加载并映射这些库。重定位和符号解析:这是动态链接的核心步骤。动态链接器会解析所有未定义符号,并把它们绑定到正确内存地址,包括初始化GOT(全局偏移表)和PLT(过程链接表)。默认情况下,函数地址解析采用延迟绑定(Lazy Binding),即函数第一次被调用时才解析;也可以设置环境变量
LD_BIND_NOW=1,让链接器在程序启动时一次性解析全部符号。移交控制权:完成链接工作后,动态链接器从栈中取得主程序入口点地址,并跳转过去,程序正式开始运行。
7.2.4. 程序入口与最终启动
无论静态链接还是动态链接,程序最终都会跳转到ELF头定义的入口点(e_entry)。在基于glibc的C/C++程序中,入口点不是main函数,而是名为_start的汇编例程。
LoongArch架构中的_start主要完成以下工作:
按照LoongArch ELF ABI规范,准备传给
__libc_start_main的参数,例如main函数地址、命令行参数argc/argv、环境变量等。调用glibc的
__libc_start_main函数。由
__libc_start_main初始化C运行环境,如I/O和线程等,然后调用main函数。
总体而言,操作系统加载ELF文件可分为几个层次:内核负责底层文件解析、内存映射和安全检查;动态链接器负责库依赖处理和重定位;最后由_start为main函数运行做好准备。