Linux内核分析(七)系统调用execve处理过程

本文的内容包括:

1. 用execve系统调用加载和执行一个可执行程序的代码演示

2. 用gdb跟踪系统调用execve的执行过程

3. execve系统调用处理过程分析

 

一、如何用execve系统调用加载一个可执行程序

下面的代码可以展示如何用execlp函数启动一个新的进程,execlp是对系统调用execve的一层封装。

Linux内核分析(七)系统调用execve处理过程_第1张图片

 其中第19行的输出是故意加上的。执行结果如下,可以看到第19行的输出根本没有显示出来,原因就是exec系列函数会用被加载的进程替换掉原来的进程。

Linux内核分析(七)系统调用execve处理过程_第2张图片

 

在Linux帮助手册中关于exec系列函数的说明也说明了这一点:

execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded. (execve函数执行成功后不会返回,而且代码段,数据段,bss段和调用进程的栈会被被加载进来的程序覆盖掉)

二、 用gdb跟踪execve系统调用的实验方法

    要用gdb调试execve系统调用,首先需要在我们的menuos中添加execve系统调用的入口,程序和上面的代码差不多,只是我们的menuos中还没有ls命令,所以我们需要做另外一个可执行程序让execve系统调用来加载。代码如下所示:

Linux内核分析(七)系统调用execve处理过程_第3张图片

其中的载入的hello程序是我们准备的一个hello world程序,他做的事情就是简单地输出一行Hello Linux Kernel!的文字。我们用静态编译的方式构造这个hello程序,然后放到我们的根文件系统的根目录下,使用的命令和执行效果如下所示:

Linux内核分析(七)系统调用execve处理过程_第4张图片

 

要调试我们的exec程序,只需要重新使用qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s启动,然后用另一个中断打开gdb远程调试就可以了。不需要使用-S参数,因为我们不需要调试启动过程,只需要等系统启动之后设置execve的断点,然后执行exec命令就可以追踪到了。

 

我们在下面几个函数中添加断点

1. do_execve

2. do_execve_common

3. exec_binprm

实验截图:

Linux内核分析(七)系统调用execve处理过程_第5张图片

 

三、 execve系统调用处理过程分析

从上面的实验可以看到,主要的处理过程都在do_execve_common() 函数中,我们来分析这个函数的代码。 为了更清楚的看到这个函数的结构,下面代码删掉了一些错误处理的部分。

1430 static int do_execve_common(struct filename *filename,

1431                                 struct user_arg_ptr argv,

1432                                 struct user_arg_ptr envp)

1433 {

1434         struct linux_binprm *bprm;  // 用于解析ELF文件的结构

1435         struct file *file;

1436         struct files_struct *displaced;

1437         int retval;

1456         current->flags &= ~PF_NPROC_EXCEEDED;  // 标记程序已被执行

1458         retval = unshare_files(&displaced);  // 拷贝当前运行进程的fd到displaced中

1463         bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);

1467         retval = prepare_bprm_creds(bprm);   // 创建一个新的凭证

1471         check_unsafe_exec(bprm);             // 必要的安全检查

1472         current->in_execve = 1;

1474         file = do_open_exec(filename);       // 打开要执行的文件

1479         sched_exec(); // 下面是Linux代码中对这个函数的解释:

// execve() is a valuable balancing opportunity, because at

// this point the task has the smallest effective memory and cache footprint.

1481         bprm->file = file;

1482         bprm->filename = bprm->interp = filename->name;

1484         retval = bprm_mm_init(bprm);       // 为ELF文件分配内存,其中的一些值还是

// 默认值,需要在后面的函数中修正

1488         bprm->argc = count(argv, MAX_ARG_STRINGS);

1492         bprm->envc = count(envp, MAX_ARG_STRINGS);

1496         retval = prepare_binprm(bprm);     // 从打开的可执行文件中读取信息,填充bprm结构

     // 下面的4句是将运行参数和环境变量都拷贝到bprm结构的内存空间中

1500         retval = copy_strings_kernel(1, &bprm->filename, bprm);

1504         bprm->exec = bprm->p;

1505         retval = copy_strings(bprm->envc, envp, bprm);

1509         retval = copy_strings(bprm->argc, argv, bprm);

     // 开始执行加载到内存中的ELF文件

1513         retval = exec_binprm(bprm);

 

             /* 执行完成,清理并返回 */

1518         current->fs->in_exec = 0;

1519         current->in_execve = 0;

1520         acct_update_integrals(current);

1521         task_numa_free(current);

1522         free_bprm(bprm);

1523         putname(filename);

1524         if (displaced)

1525                 put_files_struct(displaced);

1526         return retval;

1547 }

 

    上面的过程中,最重要的莫过于1513行的exec_binprm()函数。下面来看exec_binprm的实现:

1405 static int exec_binprm(struct linux_binprm *bprm)

1406 {

1407         pid_t old_pid, old_vpid;

1408         int ret;

1409

1410         /* Need to fetch pid before load_binary changes it */

1411         old_pid = current->pid;

1412         rcu_read_lock();

1413         old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent))     ;

1414         rcu_read_unlock();

1415

1416         ret = search_binary_handler(bprm);

1417         if (ret >= 0) {

1418                 audit_bprm(bprm);

1419                 trace_sched_process_exec(current, old_pid, bprm);

1420                 ptrace_event(PTRACE_EVENT_EXEC, old_vpid);

1421                 proc_exec_connector(current);

1422         }

1423

1424         return ret;

1425 }

  其中,需要理解的就是search_binary_handler()函数,代码如下:

1349 /*

1350  * cycle the list of binary formats handler, until one recognizes the image

1351  */

1352 int search_binary_handler(struct linux_binprm *bprm)

1353 {

1354         bool need_retry = IS_ENABLED(CONFIG_MODULES);

1355         struct linux_binfmt *fmt;

1356         int retval;

1357

1358         /* This allows 4 levels of binfmt rewrites before failing hard. */

1359         if (bprm->recursion_depth > 5)

1360                 return -ELOOP;

1361

1362         retval = security_bprm_check(bprm);   // 检查用户是否有权限运行该文件

1363         if (retval)

1364                 return retval;

1365

1366         retval = -ENOENT;

1367  retry:

1368         read_lock(&binfmt_lock);

1369         list_for_each_entry(fmt, &formats, lh) { // 尝试每一种格式的解析函数,

// 支持的格式由__register_binfmt() 函数注册进来

1370                 if (!try_module_get(fmt->module))

1371                         continue;

1372                 read_unlock(&binfmt_lock);

1373                 bprm->recursion_depth++;

1374                 retval = fmt->load_binary(bprm); // 关键步骤,调用合适格式的处理函数加载该可执行文件

// 对ELF文件来说,这个处理函数是 load_elf_binary

1375                 read_lock(&binfmt_lock);

1376                 put_binfmt(fmt);

1377                 bprm->recursion_depth--;

1378                 if (retval < 0 && !bprm->mm) {

1379                         /* we got to flush_old_exec() and failed after it */

1380                         read_unlock(&binfmt_lock);

1381                         force_sigsegv(SIGSEGV, current);

1382                         return retval;

1383                 }

1384                 if (retval != -ENOEXEC || !bprm->file) {

1385                         read_unlock(&binfmt_lock);

1386                         return retval;

1387                 }

1388         }

1389         read_unlock(&binfmt_lock);

1390

1391         if (need_retry) {

1392                 if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&

1393                     printable(bprm->buf[2]) && printable(bprm->buf[3]))

1394                         return retval;

1395                 if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) <      0)

1396                         return retval;

1397                 need_retry = false;

1398                 goto retry;

1399         }

1400

1401         return retval;

1402 }

 

在load_elf_binary()函数中,加载进来的可执行文件将把当前正在执行的进程的内存空间完全覆盖掉,如果可执行文件是静态链接的文件,进程的IP寄存器值将被设置为main函数的入口地址,从而开始新的进程;而如果可执行文件是动态链接的,IP的值将被设置为加载器ld的入口地址,是程序的运行由该加载器接管,ld会处理一些依赖的动态链接库相关的处理工作,使程序继续往下执行,而不管哪种执行方式,当前的进程都会被新加载进来的程序完全替换掉,这也是我们最早的那个程序中第19行的信息没有在终端上显示的原因。

 

四、总结

简单总结一下execve系统调用的执行过程:

1. 陷入内核

2. 加载新的可执行文件并进行可执行性检查

3. 将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据

4. 将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址

5. 返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。

你可能感兴趣的:(Linux内核)