Linux可执行文件的装载与进程2

最近打算抽空学习张绍文老师的《Android开发高手课》。
想要彻底理解本地监控APP内存的框架的实现原理。
发现理解起来都没有那么容易,在阅读代码的过程中,发现C++、linux、native hook、framework等方面的功底均有所不足。
张绍文老师说过:“看再多的文章,不去思考文章所讲的内容和意图也是没用的;思考再多,不去动手真正实践也是没用的。”
“把进阶的各个主题由点到线串联起来,但这背后必然少不了一些基础的、底层的知识进行支撑”。
这里就把空缺的知识进行补足。

进程栈初始化

我们知道进程刚开始启动的时候,须知道一些进程运行的环境,最基本的就是系统环境变量和进程的运行参数。很常见的一种做法是操作系统在进程启动前将这些信息提前保存到进程的虚拟空间的栈中(也就是 VMA 中的 Stack VMA)。让我们来看看 Linux 的进程初始化后栈的结构,我们假设系统中有两个环境变量:
在这里插入图片描述
比如我们运行该程序的命令行是:
在这里插入图片描述
并且我们假设堆栈段底部地址为 0xBF802000那么进程初始化后的堆栈就如图 6-12所示:
在这里插入图片描述

栈顶寄存器 esp 指向的位置是初始化以后堆栈的顶部,最前面的 4 个字节表示命行参数的数量,我们的例子里面是两个,即“prog”和“123”,紧接的就是分布指向这两个参数字符串的指针:后面跟了一个0:接着是两个指向环境变量字符串的指针,它们分别指向字符串“HOME=/home/user”和“PATH=/usr/bin”:
后面紧跟一个0表示结束。

进程在启动以后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给 main()函数,也就是我们熟知的 main()函数的两个argc 和argv 两个参数,这两个参数分别对应这里的命令行参数数量和命令行参数字符串指针数组。

Linux内核装载ELF的过程

当我们在 Linux系统的 bash 下输入一个命令执行某个ELF 程序时,Linux 系统是怎样装载这个ELF文件并且执行它的呢?
首先在用户层面,bash 进程会调用 fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的 ELF 文件,原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。execve()系统调用被定义在 unistd.h,它的原型如下:

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

它的三个参数分别是被执行的程序文件名、执行参数和环境变量。

Glibc 对 execvp()系统调用进行了包装,提供了
execl()、
execip()、
execle()、
execv()和
execp()
等 5 个不同形式的exec 系列API,它们只是在调用的参数形式上有所区别,但最终都会调用到execve0()这个系统中。下面是一个简单的使用 fork0)和execlp()实现的 minibash:
在这里插入图片描述

在进入 execve)系统调用之后,Linux 内核就开始进行真正的装载工作。在内核中execve系统调用相应的入口是 sys_execve,它被定义在 arch\i386\kernel\Process.c。

sys_execve进行一些参数的检查复制之后,调用 do_execve。do_execve会首先查找被执行的文件,如果找到文件,则读取文件的前 128 个字节。

为什么要这么做呢?

因为我们知道Linux 支持的可执行文件不止 ELF 一种,还有 a.out、Java 程序和以“#!”开始的脚本程序。Linux还可以支持更多的可执行文件格式,如果某一天 Linux 须支持 Windows PE的可执行文件格式,那么我们可以编写一个支持 PE 装载的内核模块来实现 Linux对 PE 文件的支持这里 do_execve()读取文件的前 128 个字节的目的是判断文件的格式。

每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4 个字节常常被称做魔数(Magic Number),通过对魔数的判断可以确定文件的格式和类型

比如 ELF 的可执行文件格式的头 4 个字节为0x7F、e 、l 、f ;
而Java 的可执行文格式的头4 个字节为c、a、f、e;
如果被执行的是 Shell 脚本 perlpython 等这种解释型语言的脚本,那么它的第一行往往是“#!/bin/sh”或“#!/usr/bin/perl”或“#!/usr/bin/python”。
这时候前两个字节#和!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序的路径。

当do_execve读取了这128 个字节的文件头部之后,然后调用 search_binary_handle去搜索和匹配合适的可执行文件装载处理过程。Linux 中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式并且用相应的装载处理过程。

比如,
ELF 可执行文件的装载处理过程叫做 load_elf_binary;
a.out 可执行文件的装载处理过程叫做 load_aout_binary();
而装载可执行脚本程序的处理过程叫做 lad_script。

这里我们只关心 ELF 可执行文件的装载步骤:

(1)查 ELF可执文件格式的有效性,比如数头表中(Segment)的数量。
(2)寻找动态链接的“.intep”段,设置动态链接器路(与动态链接有关,具体请参考下一篇文章动态链接)。
(3)根据 ELF可执行文的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据。
(4)初始化 ELF进程环境,比如进程启动时 EDX 存器的地址应该是DT_FINI的地址(参照动态链接)
(5)将系统调用的返回地址修改成 ELF 可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的 ELF 可行文件,这个入口就是 ELF 文件的文件头中e_entry 所指的地址;对于动态链接的 ELF 可执行文件,程序入口点是动态链接器。

当load_elf_binary()执行完毕,返回至do_execve()再返回至sys execve时,上面的第5步中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。所以当sys_execve(系统调用从内核态返回到用户态时,EIP 寄存器直接跳转到了 ELF 程序的入口地址,于是新的程序开始执行,ELF 可执行文件装载完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林树杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值