杨明辉 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
一、实验过程
1. 打开控制台,进入Linux目录下,然后输入命令rm menu -rf 删除menu,然后输入命令git clone https://github.com/mengning/menu.git 重新克隆一个menu,实验结果如图1所示。
图1
2. 输入make rootfs对程序进行编译,并启动MENUOS,我们可以看看在MENUOS中多了一个命令exec,实验结果如图2所示。
图2
3. 本次编译时使用了静态编译,并且自动的加载可执行hello可执行文件,Makefile文件的内容如图3所示。
图3
4.输入命令qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S启动MENUOS,然后打开gdb,输入命令file ../linux-3.18.6/vmlinux加载符号表,然后如何命令target remote :1234连接MENUOS。实验结果如图4所示。
图4
5.在gdb中输入命令 b execve,b load_elf_binary,b start_thread设置断点,然后利用gdb进行跟踪调试。实验结果如下图所示。
图5
跟踪调试:
图6
继续:
图7
图8
二、分析Linux内核装载和启动一个可执行程序的过程
1.使用下面的命令来创建一个可执行文件, 程序的 编译链接的过程如图9所示。
<span style="font-size:14px;">shiyanlou:~/ $ cd Code [9:27:05] shiyanlou:Code/ $ vi hello.c [9:27:14] shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c -m32 [9:34:55] shiyanlou:Code/ $ vi hello.cpp [9:35:04] shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32 [9:35:21] shiyanlou:Code/ $ vi hello.s [9:35:28] shiyanlou:Code/ $ gcc -x assembler -c hello.s -o hello.o -m32 [9:35:58] shiyanlou:Code/ $ vi hello.o [9:38:44] shiyanlou:Code/ $ gcc -o hello hello.o -m32 [9:39:37] shiyanlou:Code/ $ vi hello [9:39:44] shiyanlou:Code/ $ gcc -o hello.static hello.o -m32 -static [9:40:21] shiyanlou:Code/ $ ls -l [9:41:13]</span>
图9
2. ELF文件的头部如下所示。
shiyanlou:sharelib/ $ ldd main [21:25:56] linux-gate.so.1 => (0xf774e000) # 这个是vdso - virtual DSO:dynamically shared object,并不存在这个共享库文件,它是内核的一部分,为了解决libc与新版本内核的系统调用不同步的问题,linux-gate.so.1里封装的系统调用与内核支持的系统调用完全匹配,因为它就是内核的一部分嘛。而 libc里封装的系统调用与内核并不完全一致,因为它们各自都在版本更新。 libshlibexample.so => /home/shiyanlou/LinuxKernel/sharelib/libshlibexample.so (0xf7749000) libdl.so.2 => /lib32/libdl.so.2 (0xf7734000) libc.so.6 => /lib32/libc.so.6 (0xf7588000) /lib/ld-linux.so.2 (0xf774f000) shiyanlou:sharelib/ $ ldd /lib32/libc.so.6 [21:37:00] /lib/ld-linux.so.2 (0xf779e000) linux-gate.so.1 => (0xf779d000) # readelf -d 也可以看依赖的so文件 shiyanlou:sharelib/ $ readelf -d main [21:28:04] Dynamic section at offset 0xf04 contains 26 entries: 0x00000001 (NEEDED) 共享库:[libshlibexample.so] 0x00000001 (NEEDED) 共享库:[libdl.so.2] 0x00000001 (NEEDED) 共享库:[libc.so.6] 0x0000000c (INIT) 0x80484f0 0x0000000d (FINI) 0x8048804 0x00000019 (INIT_ARRAY) 0x8049ef8
3.本次实验在MENUOS中新增了一个exec命令,用于创建一个新的进程,新增了一个函数,函数的代码及分析如下。
代码:
分析:int Exec(int argc, char *argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ printf("This is Child Process!\n"); execlp("/hello","hello",NULL); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); } }
在程序中,我们可以看到,首先调用了fork()函数来创建一个子进程,然后在子进程中执行调用execlp()函数来执行hello程序。execlp函数是exec函数族中的一个,这个函数族提供了一个在进程中启动另外一个程序执行的方法。它根据执行的文件名或目录名找到可执行文件,并用它来替代当前的进程的执行映像。换句话说,execlp函数调用并没有生成新的进程,一个进程一旦调用exec函数,它本身就“死亡”了,系统把代码段替换成新程序的代码,放弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段,唯一保留的就是进程的ID。也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。
4. 可执行程序的可执行环境。
1.在启动一个可执行程序时,会将输入的命令行参数和环境串都放在用户态堆栈中。2.动态链接分为可执行程序装载时动态链接和运行时动态链接。3.可执行程序装载时的动态链接过程如下。
1.输入命令gcc -shared dllibexample.c -o libdlibexample.so -m32生成链接库。2.然后在程序中使用#include导入该头文件即可使用该链接库中的函数。
3.使用命令gcc main.c -o main -L /path/to/your/dir -l shlibexample -ldl -m32执行。
4.运行时动态链接过程如下。
1.同样使用使用上述命令生成动态链接库。
2.在程序中动态调用,调用代码及分析如下
void * handle = dlopen("libdllibexample.so",RTLD_NOW); if(handle == NULL) { printf("Open Lib libdllibexample.so Error:%s\n",dlerror()); return FAILURE; } int (*func)(void); char * error; func = dlsym(handle,"DynamicalLoadingLibApi"); if((error = dlerror()) != NULL) { printf("DynamicalLoadingLibApi not found:%s\n",error); return FAILURE; } printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n"); func(); dlclose(handle);
3.首先,我们使用函数dlopen打开动态链接库,然后定义一个函数指针,然后通过函数dlsym来找到我们想要的函数并附值给上面定义的函数指针,最后利用函数指针调用该函数。5.调用exec系统调用装载一个可执行程序的过程。
1. 首先在用户态调用exec*函数,对应的内核入口为sys_execve.
2. 在sys_execve中调用do_execve函数,
3. 在do_execve函数中调用open_execve打开可执行文件。
4. 调用copy_strings_kernel从系统空间拷贝。
5. 调用copy_strings从用户空间拷贝。
6. 调用exec_binprm函数,然后调用search_binary_handler函数。
7. 调用list_for_each_entry来搜寻formats队列中的成员来执行。
8. 调用copy_thread来附值进程控制快的相关信息。
9. 最后调用start_thread函数来设置新的程序执行的开始地址。
6. do_execve_commen关键代码和解析如下。
430static 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; 1435 struct file *file; 1436 struct files_struct *displaced; 1437 int retval; 1438 1439 if (IS_ERR(filename)) 1440 return PTR_ERR(filename); 1441 1442 /* 1443 * We move the actual failure in case of RLIMIT_NPROC excess from 1444 * set*uid() to execve() because too many poorly written programs 1445 * don't check setuid() return code. Here we additionally recheck 1446 * whether NPROC limit is still exceeded. 1447 */ 1448 if ((current->flags & PF_NPROC_EXCEEDED) && 1449 atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { 1450 retval = -EAGAIN; 1451 goto out_ret; 1452 } 1453 1454 /* We're below the limit (still or again), so we don't want to make 1455 * further execve() calls fail. */ 1456 current->flags &= ~PF_NPROC_EXCEEDED; 1457 1458 retval = unshare_files(&displaced); 1459 if (retval) 1460 goto out_ret; 1461 1462 retval = -ENOMEM; 1463 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); 1464 if (!bprm) 1465 goto out_files; 1466 1467 retval = prepare_bprm_creds(bprm); 1468 if (retval) 1469 goto out_free; 1470 1471 check_unsafe_exec(bprm); 1472 current->in_execve = 1; 1473 1474 file = do_open_exec(filename); 1475 retval = PTR_ERR(file); 1476 if (IS_ERR(file)) 1477 goto out_unmark; 1478 1479 sched_exec(); 1480 1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name; 1483 1484 retval = bprm_mm_init(bprm); 1485 if (retval) 1486 goto out_unmark; 1487 1488 bprm->argc = count(argv, MAX_ARG_STRINGS); 1489 if ((retval = bprm->argc) < 0) 1490 goto out; 1491 1492 bprm->envc = count(envp, MAX_ARG_STRINGS); 1493 if ((retval = bprm->envc) < 0) 1494 goto out; 1495 1496 retval = prepare_binprm(bprm); 1497 if (retval < 0) 1498 goto out; 1499 1500 retval = copy_strings_kernel(1, &bprm->filename, bprm); 1501 if (retval < 0) 1502 goto out; 1503 1504 bprm->exec = bprm->p; 1505 retval = copy_strings(bprm->envc, envp, bprm); 1506 if (retval < 0) 1507 goto out; 1508 1509 retval = copy_strings(bprm->argc, argv, bprm); 1510 if (retval < 0) 1511 goto out; 1512 1513 retval = exec_binprm(bprm); 1514 if (retval < 0) 1515 goto out; 1516 1517 /* execve succeeded */ 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; 1527 1528out: 1529 if (bprm->mm) { 1530 acct_arg_size(bprm, 0); 1531 mmput(bprm->mm); 1532 } 1533 1534out_unmark: 1535 current->fs->in_exec = 0; 1536 current->in_execve = 0; 1537 1538out_free: 1539 free_bprm(bprm); 1540 1541out_files: 1542 if (displaced) 1543 reset_files_struct(displaced); 1544out_ret: 1545 putname(filename); 1546 return retval; 1547}
7. list_for_each_entry关键代码如下
list_for_each_entry(fmt, &formats, lh) { 1370 if (!try_module_get(fmt->module)) 1371 continue; 1372 read_unlock(&binfmt_lock); 1373 bprm->recursion_depth++; 1374 retval = fmt->load_binary(bprm); 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 }
8. copy_thread关键代码
132int copy_thread(unsigned long clone_flags, unsigned long sp, 133 unsigned long arg, struct task_struct *p) 134{ 135 struct pt_regs *childregs = task_pt_regs(p); 136 struct task_struct *tsk; 137 int err; 138 139 p->thread.sp = (unsigned long) childregs; 140 p->thread.sp0 = (unsigned long) (childregs+1); 141 memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); 142 143 if (unlikely(p->flags & PF_KTHREAD)) { 144 /* kernel thread */ 145 memset(childregs, 0, sizeof(struct pt_regs)); 146 p->thread.ip = (unsigned long) ret_from_kernel_thread; 147 task_user_gs(p) = __KERNEL_STACK_CANARY; 148 childregs->ds = __USER_DS; 149 childregs->es = __USER_DS; 150 childregs->fs = __KERNEL_PERCPU; 151 childregs->bx = sp; /* function */ 152 childregs->bp = arg; 153 childregs->orig_ax = -1; 154 childregs->cs = __KERNEL_CS | get_kernel_rpl(); 155 childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED; 156 p->thread.io_bitmap_ptr = NULL; 157 return 0; 158 } 159 *childregs = *current_pt_regs(); 160 childregs->ax = 0; 161 if (sp) 162 childregs->sp = sp; 163 164 p->thread.ip = (unsigned long) ret_from_fork; 165 task_user_gs(p) = get_user_gs(current_pt_regs()); 166 167 p->thread.io_bitmap_ptr = NULL; 168 tsk = current; 169 err = -ENOMEM; 170 171 if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) { 172 p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr, 173 IO_BITMAP_BYTES, GFP_KERNEL); 174 if (!p->thread.io_bitmap_ptr) { 175 p->thread.io_bitmap_max = 0; 176 return -ENOMEM; 177 } 178 set_tsk_thread_flag(p, TIF_IO_BITMAP); 179 } 180 181 err = 0; 182 183 /* 184 * Set a new TLS for the child thread? 185 */ 186 if (clone_flags & CLONE_SETTLS) 187 err = do_set_thread_area(p, -1, 188 (struct user_desc __user *)childregs->si, 0); 189 190 if (err && p->thread.io_bitmap_ptr) { 191 kfree(p->thread.io_bitmap_ptr); 192 p->thread.io_bitmap_max = 0; 193 } 194 return err; 195}
9. start_thread关键代码
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) { set_user_gs(regs, 0); regs->fs = 0; regs->ds = __USER_DS; regs->es = __USER_DS; regs->ss = __USER_DS; regs->cs = __USER_CS; regs->ip = new_ip; regs->sp = new_sp; regs->flags = X86_EFLAGS_IF; /* * force it to the iret return path by making it look as if there was * some work pending. */ set_thread_flag(TIF_NOTIFY_RESUME); }
三、总结
1. exec函数族用于在fork()函数创建的子进程中加载一个新的可执行程序。2. exec函数加载的可执行程序会覆盖掉调用进程的代码段和数据段。3. 所有新加载的可执行程序映射de开始执行的地址为ox8048000。4. 在start_thread函数中设置新的可执行程序的入口地址。