注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。
Linux操作系统下,我们在bash进程中,输入命令后,bash进程调用fork创建新进程,新进程调用execve()系统调用执行指定ELF文件,原先的bash进程继续返回等待刚开启的进程结束,然后继续等待用户输入。新进程使用execve系统调用。这个调用的原型为:
int execve(const char *filename ,char *const argv[],char *const envp[])。参数即执行的程序文件名、执行参数和环境变量。Glibc对其进行了封装,提供了execl、execlp、execle、execv、execvp等不同形式的exec系列API。
以下是miniBash的代码。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
char buf[1024];
pid_t pid;
while(1){
printf("miniBash$");
scanf("%s",buf);
pid=fork();
if(pid==0){
if(execlp(buf,0)<0){
printf("exec error\n");
}
}else if(pid>0){
int status;
waitpid(pid,&status,0);
}else{
printf("fork error%d\n",pid);
}
}
return 0;
}
execve()系统调用的入口是sys_execve(),其在arch\i386\kernel\Process.c中,sys_execve进行参数检查复制后,调用do_execve()。其首先查找被执行的文件,如果找到,读取其前128字节,以判断文件是Java还是Windows PE,还是Linux ELF
等(通过魔数判断),例如ELF的头4个字节为0x7F,Java为cafe,Shell则由于其第一行一般是#!/bin/sh,所以前两个字节是#和!。
当do_execve获取这128字节文件头部后,调用search_binary_handle()搜索和匹配合适的可执行文件装载处理程序,通过上述判断魔数,获知调用对应的处理过程,比如ELF的处理过程为load_elf_binary();Shell脚本则是load_script()。
load_elf_binary被定义在fs/Binfmt_elf.c。
其主要步骤为:
1,检查ELF文件有效性
2,寻找动态链接的“.interp”段,设置动态链接器路径。
3,根据ELF程序头表,对ELF文件进行映射
4,初始化进程环境,比如EDX寄存器是否是DT_FINI的地址(动态链接)
5,将系统调用返回地址初始化为ELF文件入口地址,对于静态链接的ELF可执行文件即ELF文件头的e_entry,动态链接的ELF可执行文件,则是动态链接器。
当load_elf_binary返回、do_execve返回到sys_execve(),系统调用返回地址已经被第5步改成了ELF文件入口。sys_execve()返回到用户态,EIP寄存器是ELF程序入口,程序开始执行。