———部分取材自《程序员的自我修养》
一:知识储备
知道了用户态和内核态的概念,我们来看一下这两种状态的转换:
二:ELF的装载过程
一: execve();底层执行的过程
execve();被定义在unistd.h 函数原型为:
int execve(cosnt char *filename,char *const argv[ ],char *const envp[ ]);
它的三个参数分别是被执行的程序文件名,执行参数和环境变量。在进入execve();系统调用之后,Linux内核就开始进行真正的装载工作。
int sys_execve(char *filenamei,
char **argv,
char **envp,
struct pt_regs *regs);
其功能是进行一个参数的检查复制后调用do_execve();
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs) ;
而 do_execve();会分8步执行。
1)动态分配linux_binprm 结构体并用新的可执行文件的数据填充这个结构体:
bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_ret;
//对bprm进行一个检查之后使用memset函数将bprm全置为0。
memset(bprm, 0, sizeof(*bprm));
对于bprm这个结构体在函数前面有这样的定义:
struct linux_binprm *bprm;
那么我们再深入 linux_binprm这个结构体看看:
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
struct mm_struct *mm;
unsigned long p; /* current top of mem */
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
void *security;
int argc, envc;
char * filename;
char * interp;
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
这个结构体虽然成员很多,但我们只需要知道装载流程,所以只需要知道第一个成员为:
char buf[BINPRM_BUF_SIZE];
这里定义了一个字符数组,里面是一个宏:
#define BINPRM_BUF_SIZE 128
大小为128。这个会在后面魔数的传入时会用到。
2)现在我们回到bprm结构的动态分配。对bprm结构体初始化为0后,do_execve();会调用 open_exec();这个函数会调用path_lookup(),dentry_open(),path_release()以获得与可执行文件相关的目录项对象、文件对象和索引结点对象:
file = open_exec(filename);
retval = PTR_ERR(file);
下面这一步将对file进行检查,file也可能是错误码,所以调用IS_ERR(file)进行检查。
if (IS_ERR(file))
goto out_kfree;
3)在多处理器中,调用sched_exec以确定最小负载CPU以执行 新程序,并把上面获得的部分信息转移到bprm结构体中去。
sched_exec();//进行检查
//以下部分将获得的部分信息填入bprm结构体
bprm->p = PAGE_SIZE*MAX_ARG_PAGES - sizeof(void *);
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
bprm->mm = mm_alloc();
retval = -ENOMEM;
//参数检查
if (!bprm->mm)
goto out_file;
4)调用prepare_binprm();填充linux_binprm定义的brpm结构体。
这个函数很长所以我们只看一下这个函数的函数原型:
int prepare_binprm(struct linux_binprm *bprm)
这个函数大致上会干三件事:
1.检测可执行文件是否可执行
源代码:
if (!(mode & 0111))
return -EACCES;
if (bprm->file->f_op == NULL)
return -EACCES;
2.初始化e_uid 和 e_gid字段。后面会根据这两个值检测用户权限。
源代码:
bprm->e_uid = current->euid;
bprm->e_gid = current->egid;
3.用可执行文件的前128个字节填充linux_binprm结构的buf字段。这些字节包含的是适合于识别可执行文件的一个魔数和其他信息。
源代码:
memset(bprm->buf,0,BINPRM_BUF_SIZE);//先初始化为0
return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);
/* 返回时调用kernel_read();用可执行文件文件文档bprm->file
** 的前BINPRM_BUF_SIZE(128)个字节去填充bprm->buf,这就是
** 我们前面为什么要将这个buf的定义给出的原因了。
**/
5)把路径名,命令行参数,环境串复制到一个或者多个新分配的页框中,最终,它们会被分配给用户态地址空间。
源代码:
//复制路径名
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0) //在每次复制后都会进行一次检查
goto out;
//复制环境串
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out;
//复制命令行参数
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
6)用search_binary_handler();对formats链表进行扫描,并尽力应用每个元素的load_binary方法,把inxu_binprm数据结构传递给这个函数。只要load_binary方法成功应答了文件的可执行格式。对formats的扫描就终止。
search_binary_handler();函数还会通过判断文件头部的 buf里面的魔数确定文件的格式。
7)对于装载处理过程,这里对ELF说明:
ELF装载处理过程叫做load_elf_binary();其被定义在fs/Binfmt_elf.c,由于这个函数代码叫长这里只给出原型:
static int load_elf_binary
( struct linux_binprm * bprm,
struct pt_regs * regs );
其执行主要分为5步:
1.检查ELF可执行文件的有效性,比如魔数,程序头表中断(Segmemt)的数量。
2.寻找动态链接的“.interp”段设置动态链接器路径。
3.根据ELF可执行文件的程序头表的表述,对ELF文件进行映射,比如代码,数据,只读数据。
4.初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址。
5.将系统调用的返回地址修改为ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对静态链接的ELF可执行文件,这个程序入口点就是文件的文件头中e_entry所指地址。对于动态链接的ELF可执行文件,程序入口点是动态链接器。
8)在load_elf_binary();执行完毕,返回至do_execve();再返回至sys_execve();时上面的第5步中已经把系统调用的返回地址改为了被装载ELF程序的入口地址了,所以当sys_execve();系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始,ELF可执行程序装载完成。
三:对于魔数的一点了解
——以下摘自《程序员的自我修养》
概念
使用readelf -h 命令可查看ELF的文件头,在文件头中有一行16个字节叫做Magic的数据,这就是所谓的魔数,这16个字节被ELF标准规定用来表示ELF文件的平台属性。
详解
魔数的基本格式为:
7f 45 4c 46 01 01 01 00
其最开始的4个字节是所有的ELF 文件都必须相同的标识码,分别为:0x7F 0x45 0x3C 0x46,第一个字节对应的ASC||码里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASC||码,其实这4个字节才是真正的魔数,而后面的只是标识其他信息的。
第5个字节是用来标识ELF的文件类的,0x01为32位,0x02为64位。第6个字节是 字节序 即规定这个ELF文件是大端还是小端(有兴趣可参看《程序员的自我修养》附录A)第7个自己是规定ELF文件的主版本号。后面的字节ELF标准并没有定义。
查看魔数的方法
1)直接用readelf -h 方法查看文件头,Magic一行就是魔数
2)也可为了验证魔数是否是一个ELF文件最开头的几个字节,也可自己写一个读取字符的代码来验证。
给一个小代码:(随手写,不要介意)
#include
#include
#include
int main()
{
FILE *fp = NULL;
if ((fp = fopen("./a", "r")) == NULL)
{
perror("fopen error");
exit(0);
}
char buff[128] = { 0 };
if (fread(buff, 1, 127, fp) == 0)
{
perror("fread error");
exit(0);
}
int i;
for (i = 0; i < 16; i++)
{
printf("%x ", buff[i]);
}
printf("\n");
exit(0);
}
对于execve();这一类涉及底层源码系统调用的函数,只有在能看懂源码的基础上才能进行解析,但源码这个东西不好驾驭,很可能看的云里雾里,比如说我。所以我分析得很浅。只是对整个流程做了一个概述。