程序加载与运行过程

本文详细阐述了在Linux环境下,当我们输入shell命令如`ls`时,程序如何通过fork和execve系统调用运行起来。从创建子进程、加载elf二进制文件到动态链接器的工作,再到最终执行_start函数,全面剖析了整个过程,涵盖了进程创建、内存映射、动态链接等多个关键环节。
摘要由CSDN通过智能技术生成

当我们在shell环境中输入ls命令时程序是怎么运行起来了呢?在底层做了哪些事情才输出了我们想要的结果?我们通过对程序的加载与运行过程分析来梳理整个流程,让我们对linux程序的运行过程理解更透彻。

整体流程

  1. 当我们在bash下输入ls命令时,bash进程首先通过fork系统调用创建子进程。如果我们不做任何改变,fork出的子进程将执行父进程后面的代码。
  2. 在子进程中我们调用execve系统调用执行我们想要执行的ls进程。原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

execve系统调用流程

execve函数的形式如下:

int execve(const char *filename, char *const argv[], char *const envp[]);

 它一共有三个参数,分别是需要执行程序的文件名、执行参数和环境变量。进入execve系统调用后,linux内核就开始进行真正的装载工作。

execve系统调用的函数调用流程如下:

execve--->sys_exeve--->do_execve--->search_binary_handle--->load_elf_binary
  • 在do_execve函数中,首先读取可执行文件的头结构,即前128字节。通过该头结构可判断可执行文件的格式,毕竟linux支持多种格式可执行文件。

elf文件的头4个字节分别为0x4f、e、l、f,如下:

  • 根据获取的可执行文件格式,调用search函数获取该可执行文件对应的状态处理接口。如elf格式文件的装载处理接口为load_elf_binary。

load_elf_binary函数的主要步骤如下:

  1. 检查elf可执行文件格式的有效性,如魔数、程序头中段的数量等。
  2. 解析.interp段获取动态链接器的位置,设置动态链接器的路径。
  3. 根据可执行文件的程序头表的描述,对elf文件进行映射,如代码、数据、只读数据。
  4. 初始化elf进程环境。
  5. 将系统调用的返回地址修改为可执行文件的入口地址,移交cpu的执行权。针对可执行文件类型的不同,其入口地址有两类。
  • 若elf可执行文件为静态链接可执行文件,则入口地址即为可执行文件中头结构的entry point address指向地址(即为可执行文件中的_start函数)。

elf文件头结构entry point address:

 入口地址0x08048615对应的函数_start:

  • 若可执行文件为动态链接可执行文件,入口地址为动态链接器。

可执行文件的.interp段中保存了动态链接器的路径,如下:

当load_elf_binary执行完毕,依次返回至do_execve、sys_execve时,由于上面的步骤5已经将系统调用的返回地址修改为可执行文件的入口地址,因此当完成系统调用从内核态返回到用户态时,cpu将执行elf程序的入口地址,即开始执行新程序,至此elf可执行文件的装载完成。

execve流程总结如下:

从操作系统角度看可执行文件装载

从操作系统角度看可执行文件的加载,该过程主要分如下三步:

  1. 创建进程的虚拟地址空间。通常就是创建进程的页表,此时还不需要建立虚拟内存到物理内存的映射,到真正使用时再建立映射关系。
  2. 根据elf可执行文件的程序头(program header)建立可执行文件到虚拟内存的映射,这种映射关系保存到进程task_struct结构体中的vma_area_struct结构中。每一段虚拟内存段称为虚拟内存区域(VMA,virtual memory area)。这一步也是真正意义上的装载。              相关结构体层次关系:                                                                                                                     task_struct(描述进程)                                                                                                           --->  mm_struct(描述进程的用户态内存空间,包括代码段、数据段位置等信息)                 ---> vma_area_struct(描述一个虚拟内存段,包括该虚拟内存段的起始地址、大小,以及对应的文件和文件中偏移。mmap时通常需要创建一个独立的vma_area_struct结构进行映射)                                                                                                                                            只有完成了这种elf文件到虚拟内存的映射关系,当程序运行发生缺页异常时,陷入内核后操作系统才能根据这种映射关系找到该地址对应的具体elf文件位置,这样内核分配了物理内存后将该部分文件读取到内存中,并建立虚拟内存到对应物理内存的映射关系。最后返回到用户空间后程序才能继续执行下去。
  3. 将CPU的指令寄存器设置为elf可执行文件的入口地址,即操作系统将控制权交给可执行文件的入口,开始运行新的程序。

操作系统角度看可执行文件加载流程总结如下:

动态链接器链接步骤

针对动态链接的可执行文件,其运行时需要动态链接器先帮忙完成依赖的动态库加载与重定位,这样该可执行程序才能开始运行。动态链接器的链接步骤如下:

  1. 实现动态链接器自举,完成动态链接器自身的全局变量和静态变量重定位工作。
  2. 根据可执行文件的.dynamic段递归获取所有依赖的动态库,并将这些动态库映射到虚拟地址空间,将它们的动态符号表合并到全局符号表。(类似于静态链接的“空间与地址分配”)
  3. 完成引用的外部符号的解析与重定位,根据动态库的.init段进行初始化,如C++中的全局/静态对象的构造函数。类似的.finit段中代码用来实现C++全局对象的析构操作。(这一步类似于静态链接的“符号解析与重定位”)
  4. 完成了上面3步后,动态链接器将控制权交给elf可执行文件的入口,即elf可执行文件头结构的Entry point address地址。(该地址实际指向_start函数)

动态链接器的链接步骤总结如下:

进程自身执行过程

当历经千辛万苦终于将控制权转交到elf可执行文件的入口地址后,可执行程序终于要开始正式运行了。在glibc中,elf可执行程序的入口地址为_start函数。这个入口地址由ld连接器的默认脚本指定,我们可以通过相关参数进行设置。

_start函数由汇编实现,其主要完成了_libc_start_main函数的调用。__libc_start_main函数的形式如下:

 该函数总共有7个参数,含义分别如下:

  • 参数main:即我们通常写的main函数入口地址。
  • 参数argc:main函数的参数个数。
  • 参数argv:main函数的参数列表。main函数的环境变量参数列表紧跟其后,因此可以通过argv顺带获取。
  • 参数init:执行main函数前调用,用于完成一些初始化工作,实际填充的是__libc_csu_init函数。如执行调用C++的构造函数、声明为__attribute((constructor))类型的构造函数。
  • 参数fini:执行完main函数调用,用于完成一些结束收尾工作,时机填充的是__libc_csu_fini函数。如执行调用C++的析构函数、声明为__attribute((destructor))类型的析构函数。
  • 参数rtld_fini:和动态加载相关的收尾工作。该函数和fini函数一样在main函数执行完后调用。rtld是runtime loader缩写。(即为动态链接器的析构函数)
  • stack_end:栈底地址,即栈的最高地址(sp寄存器)。

__libc_start_main函数的主要执行流程如下:

  1. 调用__cxa_atexit函数(等同于atexit函数)将rtld_fini函数添加到对应链表中,用于在exit函数中调用。(main函数结束后会调用exit函数)
  2. 执行相关运行环境的初始化工作,如堆、I/O、线程等等。
  3. 调用__cxa_atexit函数将fini函数添加到对应链表中,用于在exit函数中调用(main函数结束后会调用exit函数)
  4. 调用init函数执行进程相关的构造函数。
  5. 调用main函数,执行用户的流程。
  6. 在main函数执行完后调用exit函数。在exit函数中逐个调用所有相关的析构函数,如上面步骤1、3通过__cxa_atexit函数注册的rtld_fini、fini函数。

相关函数调用流程如下:

elf可执行文件进程自身执行过程总结如下:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值