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文件可分为几个层次:内核负责底层文件解析、内存映射和安全检查;动态链接器负责库依赖处理和重定位;最后由_startmain函数运行做好准备。