作者:吴乐 山东师范大学
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
实验目的:通过对一个简单的可执行程序用gdb进行代码的跟踪,剖析linux内核是如何动态和静态装载和启动程序的,进而总结linux内核可执行程序加载的过程。
一、实验过程
1、编写一个简单的Exec的创建进程的函数
2、打开gdb,并设置好如下断点
3、开始跟踪,找到第一个断点。
(主程序还未创建子进程)
4、继续在此断点处逐步跟踪
5、找到设置的第二个断点,并列出
6、跟踪到装载new_ip处,查看其地址
7、明显看到,此处加载的IP地址与程序入口地址相同
8、结束跟踪,观察其他断点方法类似。
二、可执行文件的加载和运行
1、execve()系统调用的入口是sys_execve().代码如下:
int sys_execve(struct pt_regs regs)
{
int error;
char * filename; //将用户空间的第一个参数(也就是可执行文件的路径)复制到内核
filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
®s);
if (error == 0) {
task_lock(current);
current->ptrace &= ~PT_DTRACE;
task_unlock(current);
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
//释放内存
putname(filename);
out:
return error;
}
由此可见进行系统调用时,把参数依次放在ebx,ecx,edx,esi,edi,ebp寄存器.
注意其中第一个参数为可执行文件路径,第二个参数为参数的个数,第三个参数为可执行文件对应的参数.
2、do_execve()是这个系统调用的主要部分,它的代码如下:
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
//linux_binprm:保存可执行文件的一些参数
struct linux_binprm *bprm;
struct file *file;
unsigned long env_p;
int retval; retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_ret; //在内核中打开这个可执行文件
file = open_exec(filename);
retval = PTR_ERR(file);
//如果打开失败
if (IS_ERR(file))
goto out_kfree; sched_exec(); bprm->file = file;
bprm->filename = filename;
bprm->interp = filename; //bprm初始化,主要是初始化bprm->mm
retval = bprm_mm_init(bprm);
if (retval)
goto out_file; //计算参数个数
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc)
goto out_mm; //环境变量个数
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc)
goto out_mm; retval = security_bprm_alloc(bprm);
if (retval)
goto out; //把要加载文件的前128 读入bprm->buf
retval = prepare_binprm(bprm);
if (retval
goto out;
//copy第一个参数filename
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval
goto out;
//bprm->exec:参数的起始地址(从上往下方向)
bprm->exec = bprm->p;
//copy环境变量
retval = copy_strings(bprm->envc, envp, bprm);
if (retval
goto out;
//环境变量存放的起始地址
env_p = bprm->p;
//copy可执行文件所带参数
retval = copy_strings(bprm->argc, argv, bprm);
if (retval
goto out;
//环境变量的长度
bprm->argv_len = env_p - bprm->p; //到链表中寻找合适的加载模块
retval = search_binary_handler(bprm,regs);
if (retval >= 0) {
/* execve success */
free_arg_pages(bprm);
security_bprm_free(bprm);
acct_update_integrals(current);
kfree(bprm);
return retval;
} out:
free_arg_pages(bprm);
if (bprm->security)
security_bprm_free(bprm); out_mm:
if (bprm->mm)
mmput (bprm->mm); out_file:
if (bprm->file) {
allow_write_access(bprm->file);
fput(bprm->file);
}
out_kfree:
kfree(bprm); out_ret:
return retval;
}
3、在加载可执文件的时候,需要遍历formats这个链表,search_binary_handler()实现了这一功能。代码如下:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
int try,retval;
struct linux_binfmt *fmt;
#ifdef __alpha__
/* handle /sbin/loader.. */
{
struct exec * eh = (struct exec *) bprm->buf; if (!bprm->loader && eh->fh.f_magic == 0x183 &&
(eh->fh.f_flags & 0x3000) == 0x3000)
{
struct file * file;
unsigned long loader; allow_write_access(bprm->file);
fput(bprm->file);
bprm->file = NULL; loader = bprm->vma->vm_end - sizeof(void *); file = open_exec("/sbin/loader");
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval; /* Remember if the application is TASO. */
bprm->sh_bang = eh->ah.entry bprm->file = file;
bprm->loader = loader;
retval = prepare_binprm(bprm);
if (retval
return retval;
/* should call search_binary_handler recursively here,
but it does not matter */
}
}
#endif
retval = security_bprm_check(bprm);
if (retval)
return retval; /* kernel module loader fixup */
/* so we don't try to load run modprobe in kernel space. */
set_fs(USER_DS); retval = audit_bprm(bprm);
if (retval)
return retval; retval = -ENOENT;
//这里会循环两次.待模块加载之后再遍历一次
for (try=0; try
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
//加载函数
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
continue;
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock); //运行加载函数,如果加载末成功,则继续遍历
retval = fn(bprm, regs); //加载成功了
if (retval >= 0) {
put_binfmt(fmt);
allow_write_access(bprm->file);
if (bprm->file)
fput(bprm->file);
bprm->file = NULL;
current->did_exec = 1;
proc_exec_connector(current);
return retval;
}
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (retval != -ENOEXEC || bprm->mm == NULL)
break;
if (!bprm->file) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
//所有模块加载这个可执行文件失败,则加载其它模块再试一次
if (retval != -ENOEXEC || bprm->mm == NULL) {
break;
//CONFIG_KMOD:动态加载模块标志
#ifdef CONFIG_KMOD
}else{
#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20
if (printable(bprm->buf[0]) &&
printable(bprm->buf[1]) &&
printable(bprm->buf[2]) &&
printable(bprm->buf[3]))
break; /* -ENOEXEC */
request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
#endif
}
}
return retval;
}
4、唤醒父进程的过程以及栈空间的布局代码如下.
static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
……
……
current->mm->start_stack =
(unsigned long) create_aout_tables((char __user *) bprm->p, bprm);
#ifdef __alpha__
regs->gp = ex.a_gpvalue;
#endif
start_thread(regs, ex.a_entry, current->mm->start_stack);
……
}
Creat_aout_tables()代码如下:
static unsigned long __user *create_aout_tables(char __user *p, struct linux_binprm * bprm)
{
char __user * __user *argv;
char __user * __user *envp;
unsigned long __user *sp;
//可执行文件的参数个数
int argc = bprm->argc;
//环境变量的个数
int envc = bprm->envc; //sp初始化成p,也即bprm->p
sp = (void __user *)((-(unsigned long)sizeof(char *)) & (unsigned long) p);
#ifdef __sparc__
/* This imposes the proper stack alignment for a new process. */
sp = (void __user *) (((unsigned long) sp) & ~7);
if ((envc+argc+3)&1) --sp;
#endif
#ifdef __alpha__
/* whee.. test-programs are so much fun. */
put_user(0, --sp);
put_user(0, --sp);
if (bprm->loader) {
put_user(0, --sp);
put_user(0x3eb, --sp);
put_user(bprm->loader, --sp);
put_user(0x3ea, --sp);
}
put_user(bprm->exec, --sp);
put_user(0x3e9, --sp);
#endif
sp -= envc+1;
envp = (char __user * __user *) sp;
sp -= argc+1;
argv = (char __user * __user *) sp;
#if defined(__i386__) || defined(__mc68000__) || defined(__arm__) || defined(__arch_um__)
put_user((unsigned long) envp,--sp);
put_user((unsigned long) argv,--sp);
#endif
put_user(argc,--sp);
current->mm->arg_start = (unsigned long) p; while (argc-->0) {
char c;
put_user(p,argv++);
do {
get_user(c,p++);
} while (c);
}
put_user(NULL,argv);
current->mm->arg_end = current->mm->env_start = (unsigned long) p;
while (envc-->0) {
char c;
put_user(p,envp++);
do {
get_user(c,p++);
} while (c);
}
put_user(NULL,envp);
current->mm->env_end = (unsigned long) p;
return sp;
}
ip这里已经指向main函数入口地址了,此后的工作都由start_thread()函数完成。具体过程可参见我的另一片博客:
http://www.cnblogs.com/wule/p/4404504.html
三、总结linux内核可执行程序加载的过程
首先创建父进程,然后通过调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件。 主进程继续返回等待新进程执行结束,然后重新等待用户输入命令。execve()系统调用被定义在unistd.h,它的原型如下:
int execve(const char *filenarne, char *const argv[], char *const envp[]);
它的三个参数分别是被执行的程序文件名、执行参数和环境变最。Glibc对execvp()系统调用进行了包装,提供了execl(), execlp(), execle(), execv()和execvp()等5个不同形式的exec系列API,它们只是在调用的参数形式上有所区别,但最终都会调用到execve()这个系统中。
调用execve()系统调用之后,再调用内核的入口sys_execve()。 sys_execve()进行一些参数的检查复制之后,调用do_execve()。 因为可执行文件不止ELF一种,还有java程序和以“#!”开始的脚本程序等, 所以do_execve()会首先检查被执行文件,读取前128个字节,特别是开头4个字节的魔数,用以判断可执行文件的格式。 如果是解释型语言的脚本,前两个字节“#!"就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定程序解释器的路径。
当do_execve()读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理过程。如ELF用load_elf_binary(),a.out用load_aout_binary(),脚本用load_script()。其中ELF装载过程的主要步骤是:
①检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
②寻找动态链接的”.interp”段(该段保存可执行文件所需要的动态链接器的路径),设置动态链接器路径。
③根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
④初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(结束代码地址)。
⑤将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_enEry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。
当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。在load_elf_binary()中(第5步)系统调用的返回地址已经被改成ELF程序的入口地址了。 所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。