程序加载与运行过程

当我们在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可执行文件进程自身执行过程总结如下:

 

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了实现DR←[ADR]指令,我们可以设计一个新的操作码,例如34(H),并在微程序控制器中为该操作码编写一个微程序,首地址为64(H)。下面是该微程序的伪代码: ``` 地址 指令 控制信号 64(H) MAR←ADR MRW←1, I2~0←000, I8~6←000, I5~3←111 65(H) MDR←MDR MRW←1, I2~0←000, I8~6←000, I5~3←110 66(H) DR←MDR MRW←0, I2~0←000, I8~6←001, I5~3←000 ``` 注解: - 第一条微指令将ADR写入MAR寄存器,同时将MRW设置为1,表示从内存中读取数据到寄存器。I2~0为000,表示不进行ALU计算;I8~6和I5~3分别为000和111,表示将MAR寄存器的值加载到R7寄存器(ADR的地址)。 - 第二条微指令将内存中地址为ADR的数据读入MDR寄存器,同时将MRW设置为1,表示从内存中读取数据到寄存器。I2~0为000,表示不进行ALU计算;I8~6和I5~3分别为000和110,表示将MDR寄存器的值加载到R6寄存器。 - 第三条微指令将MDR寄存器的值写入DR寄存器,同时将MRW设置为0,表示将寄存器的值写回到内存。I2~0为000,表示不进行ALU计算;I8~6和I5~3分别为001和000,表示将DR寄存器的值写回到内存中地址为ADR的位置。 在微程序控制器模拟软件上实现该微程序,具体的实现方式可能因软件不同而有所不同。一般来说,可以通过构建微程序控制器的控制信号表或状态转移图的方式来实现该微程序,并通过模拟内存和寄存器等硬件来模拟CPU的执行过程。在调试时,可以逐条微指令地执行程序,并观察内存和寄存器的变化,以确保程序的正确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值