Linux内核如何装载和启动一个可执行程序
2016-04-09 19:06
513 查看
杨明辉 + 原创作品转载请注明出处 + 《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函数中设置新的可执行程序的入口地址。
相关文章推荐
- CentOS更改yum源与更新系统
- centos/fedora 配置ntfs支持
- linux远程复制
- Linux中SSH的使用
- Linux下vim编辑器设置自动添加作者信息
- vim自动补全插件clang_complete
- LInux下设置Gnome顶部透明
- 《Linux操作系统分析》之分析Linux内核如何装载和启动一个可执行程序
- linux系统安装yum服务
- linux基础学习之 gSOAP文件夹中的README中文翻译
- linux中用户创建与删除以及文件权限查看和修改
- linux常用操作
- linux命令--less|more
- linux内核分析第七周-Linux内核如何装载和启动一个可执行程序
- Linux内核分析——第七章 链接
- Linux文件之inode
- 20135239 益西拉姆 linux内核分析 可执行程序的装载
- Linux内核如何装载和启动一个可执行程序
- Centos 6.5 64位操作系统,更新火狐浏览器到最新版本
- Linux Free命令各数字含义及Buffer和Cache的区别