原创作品转载请注明出处 ,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-100002900
一、编译链接过程分解
1、下面是hello world代码
1 2 |
vi tmp.c #编写C代码 |
1 2 3 4 5 6 7 8 9 |
/* * file: tmp.c */ #include<stdio.h> int main() { printf("Hello\n"); return 0; } |
2、预处理
1 2 |
gcc -E tmp.c -o tmp.cpp #首先进行预处理,保存在tmp.cpp中,这里cpp不是C++代码的意思 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
cat tmp.cpp #由于#include<stdio.h>,所以预处理之后的内容会很多,下面只摘取部分 /* * file: tmp.cpp */ 838 extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; 839 840 841 extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); 842 # 943 "/usr/include/stdio.h" 3 4 843 844 # 2 "tmp.c" 2 845 int main() 846 { 847 printf("Hello\n"); 848 return 0; 849 } |
3、生成汇编代码
1 2 |
gcc -x cpp-output -S -o tmp.s tmp.cpp -m32 #cpp-output的意思是从预处理之后继续编译 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* * file: tmp.s */ 1 .file "tmp.c" 2 .section .rodata 3 .LC0: 4 .string "Hello" 5 .text 6 .globl main 7 .type main, @function 8 main: 9 .LFB0: 10 .cfi_startproc 11 pushl %ebp 12 .cfi_def_cfa_offset 8 13 .cfi_offset 5, -8 14 movl %esp, %ebp 15 .cfi_def_cfa_register 5 16 andl $-16, %esp 17 subl $16, %esp 18 movl $.LC0, (%esp) 19 call puts 20 movl $0, %eax 21 leave 22 .cfi_restore 5 23 .cfi_def_cfa 4, 4 24 ret 25 .cfi_endproc 26 .LFE0: 27 .size main, .-main 28 .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2" 29 .section .note.GNU-stack,"",@progbits |
4、生成二进制文件
1 2 |
gcc -x assembler -c tmp.s -o tmp.o -m32 #assembler,顾名思义,将汇编代码编译成二进制文件 |
5、链接生成可执行文件
1 |
gcc tmp.o -o tmp.out -m32 |
最后我们来看一下,两个二进制文件的差别:
1 2 3 |
$ file tmp.o tmp.out tmp.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped tmp.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=3f1c897c7484ca40258768cc6d389dde5fe07838, not stripped |
很明显,tmp.o 只是一个可重定位的二进制文件,而 tmp.out 才是真正的可执行文件。
下面是 readelf -h 的返回结果。如果用 readelf -d查看文件的依赖,那么 tmp.o 是没有任何依赖的,相反 tmp.out 则依赖了很多系统链接库。
(你可能需要向右拖动滚动条才能看到全部内容。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
ELF 头: |ELF 头: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 | Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 | Class: ELF32 Data: 2's complement, little endian | Data: 2's complement, little endian Version: 1 (current) | Version: 1 (current) OS/ABI: UNIX - System V | OS/ABI: UNIX - System V ABI Version: 0 | ABI Version: 0 Type: REL (可重定位文件) | Type: EXEC (可执行文件) Machine: Intel 80386 | Machine: Intel 80386 Version: 0x1 | Version: 0x1 入口点地址: 0x0 | 入口点地址: 0x8048320 程序头起点: 0 (bytes into file) | 程序头起点: 52 (bytes into file) Start of section headers: 276 (bytes into file) | Start of section headers: 4428 (bytes into file) 标志: 0x0 | 标志: 0x0 本头的大小: 52 (字节) | 本头的大小: 52 (字节) 程序头大小: 0 (字节) | 程序头大小: 32 (字节) Number of program headers: 0 | Number of program headers: 9 节头大小: 40 (字节) | 节头大小: 40 (字节) 节头数量: 13 | 节头数量: 30 字符串表索引节头: 10 | 字符串表索引节头: 27 |
二、在代码中使用动态链接和静态链接
这里直接贴出示例代码中的核心部分,版权信息有所删减。
1、动态链接库代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* * file: dllibexample.c */ #include <stdio.h> #include "dllibexample.h" #define SUCCESS 0 #define FAILURE (-1) /* * Dynamical Loading Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) */ int DynamicalLoadingLibApi() { printf("This is a Dynamical Loading libary!\n"); return SUCCESS; } |
2、静态链接库代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* * file: shlibexample.c */ #include <stdio.h> #include "shlibexample.h" /* * Shared Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) */ int SharedLibApi() { printf("This is a shared libary!\n"); return SUCCESS; } |
3、主函数部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* * file: main.c */ #include <stdio.h> #include "shlibexample.h" #include <dlfcn.h> /* * Main program * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) */ int main() { printf("This is a Main program!\n"); /* Use Shared Lib */ printf("Calling SharedLibApi() function of libshlibexample.so!\n"); SharedLibApi(); /* Use Dynamical Loading Lib */ 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); return SUCCESS; } |
4、编译运行
1 2 3 4 5 6 7 8 9 10 11 12 13 |
gcc dllibexample.c -o libdllibexample.so -shared -m32 gcc shlibexample.c -o libshlibexample.so -shared -m32 #首先生成相应的链接库 gcc main.c -o main.out -L . -l shlibexample -ldl -m32 #编译主函数 ./main.out #运行,以下为运行结果 This is a Main program! Calling SharedLibApi() function of libshlibexample.so! This is a shared libary! Calling DynamicalLoadingLibApi() function of libdllibexample.so! This is a Dynamical Loading libary! |
三、相关内核代码注解
1、sys_execve()系统调用,这里不必多言。
1 2 3 4 5 6 7 8 9 10 11 |
/* * file: linux-3.18.6/fs/exec.c */ 1604 SYSCALL_DEFINE3(execve, 1605 const char __user *, filename, 1606 const char __user *const __user *, argv, 1607 const char __user *const __user *, envp) 1608 { 1609 return do_execve(getname(filename), argv, envp); 1610 } /* 这里调用了函数do_execve(),代码见下方 */ |
2、do_execve()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * file: linux-3.18.6/fs/exec.c */ 1549 int do_execve(struct filename *filename, 1550 const char __user *const __user *__argv, 1551 const char __user *const __user *__envp) /* 传递的文件名、运行参数、环境变量 *这里和c语言函数调用传递的参数有点不同啊,但是好像讨论这个没什么意义 */ 1552 { 1553 struct user_arg_ptr argv = { .ptr.native = __argv }; 1554 struct user_arg_ptr envp = { .ptr.native = __envp }; 1555 return do_execve_common(filename, argv, envp); /* 这里又把任务传递给了函数do_execve_common(),怎么感觉像在踢皮球... */ 1556 } |
3、do_execve_common()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
/* * file: linux-3.18.6/fs/exec.c */ 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; 1435 struct file *file; 1436 struct files_struct *displaced; 1437 int retval; 1438 /* 下面省略的部分是一堆的合法性检验 */ ...... ...... 1474 file = do_open_exec(filename); /* 打开可执行文件 */ 1475 retval = PTR_ERR(file); 1476 if (IS_ERR(file)) 1477 goto out_unmark; 1479 sched_exec(); 1480 1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name; /* 开始填充结构体数据 */ 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; /* 上面连续几个copy_strings()操作,复制一些参数和环境变量 */ 1512 1513 retval = exec_binprm(bprm); /* 这里开始了关键过程,详细代码在下一部分 */ ...... ...... |
4、exec_binprm()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * file: linux-3.18.6/fs/exec.c */ 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(); 1416 ret = search_binary_handler(bprm); /* 这里是关键代码,搜索相应可执行文件的处理函数,详细内容在下面 */ ...... ...... |
5、 search_binary_handler()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* * file: linux-3.18.6/fs/exec.c */ 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; ...... ...... /* 下面的函数块,在链表中搜索可以处理对应可执行文件的模块 */ 1369 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); /* 这句是关键代码,实际调用的函数指针是load_elf_binary(),具体分析见下一个部分*/ ...... ...... |
6、load_elf_binary()相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/* * file: linux-3.18.6/fs/binfmt_elf.c */ 82 static struct linux_binfmt elf_format = { 83 .module = THIS_MODULE, 84 .load_binary = load_elf_binary, 85 .load_shlib = load_elf_library, 86 .core_dump = elf_core_dump, 87 .min_coredump = ELF_EXEC_PAGESIZE, 88 }; /* * 上面这段代码对elf_format结构体进行了赋值操作。 * 这个结构体作为链表中的一个node,是观察者模式或者说是发布订阅模式的体现 */ ...... ...... 571 static int load_elf_binary(struct linux_binprm *bprm) 572 { /* 该函数严格按照elf文件的格式来解析文件。 * 然后将可执行文件映射到相应的内存空间。 * (32bit elf程序总是被映射到0x8048000) */ ...... ...... 887 if (elf_interpreter) { /* 这里判断是否需要动态链接,如果需要,下面的elf_entry的指向的就是动态链接器(即ld) */ 888 unsigned long interp_map_addr = 0; 889 890 elf_entry = load_elf_interp(&loc->interp_elf_ex, 891 interpreter, 892 &interp_map_addr, 893 load_bias); 894 if (!IS_ERR((void *)elf_entry)) { 895 /* 896 * load_elf_interp() returns relocation 897 * adjustment 898 */ 899 interp_load_addr = elf_entry; 900 elf_entry += loc->interp_elf_ex.e_entry; 901 } 902 if (BAD_ADDR(elf_entry)) { 903 retval = IS_ERR((void *)elf_entry) ? 904 (int)elf_entry : -EINVAL; 905 goto out_free_dentry; 906 } 907 reloc_func_desc = interp_load_addr; 908 909 allow_write_access(interpreter); 910 fput(interpreter); 911 kfree(elf_interpreter); 912 } else { /* else分支,如果是静态链接程序,直接将可执行文件的入口赋值给elf_entry */ 913 elf_entry = loc->elf_ex.e_entry; 914 if (BAD_ADDR(elf_entry)) { 915 retval = -EINVAL; 916 goto out_free_dentry; 917 } 918 } ...... ...... 975 start_thread(regs, elf_entry, bprm->p); /* 这里使用上面准备好的elf_entry来启动新进程 * elf_entry是新程序的起点 */ 976 retval = 0; ...... ...... 2198 static int __init init_elf_binfmt(void) 2199 { 2200 register_binfmt(&elf_format); 2201 return 0; 2202 } /* 这里所谓的初始化过程就是将上面那个结构体变量注册在某个链表中 */ |
四、GDB实际调试过程
1、编译生成新的rootfs
1 2 3 4 |
cd menu make rootfs #这里已经clone好了最新的 MenuOS 代码; #直接make rootfs不仅会编译生成新的rootfs,还会自动启动qemu虚拟机 |
2、启动虚拟机
1 2 3 |
cd linux-3.18.6 qemu -kernel arch/x86/boot/bzImage -initrd ../rootfs.img -s -S #启动虚拟机,并打开远程调试端口 |
3、启动gdb开始调试
1 2 3 4 5 6 7 8 9 |
gdb linux-3.18.6/vmlinux #打开包含调试信息的内核文件(待调试程序) (gdb) target remote:1234 #连接调试端口 (gdb) b sys_execve #设置断点 (gdb) c #continue,完成虚拟机启动 |
4、部分调试过程摘录
(完整调试过程记录 gdb )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
Breakpoint 2, SyS_execve (filename=135050446, argv=-1080721152, envp=-1080715588) at fs/exec.c:1604 1604 SYSCALL_DEFINE3(execve, (gdb) s SYSC_execve (envp=<optimized out>, argv=<optimized out>, filename=<optimized out>) at fs/exec.c:1609 1609 return do_execve(getname(filename), argv, envp); (gdb) s SyS_execve (filename=135050446, argv=-1080721152, envp=-1080715588) at fs/exec.c:1604 1604 SYSCALL_DEFINE3(execve, (gdb) SYSC_execve (envp=<optimized out>, argv=<optimized out>, filename=<optimized out>) at fs/exec.c:1609 1609 return do_execve(getname(filename), argv, envp); (gdb) s do_execve (__envp=<optimized out>, __argv=<optimized out>, filename=<optimized out>) at fs/exec.c:1555 1555 return do_execve_common(filename, argv, envp); (gdb) do_execve_common (filename=0xc79cb000, argv=..., envp=...) at fs/exec.c:1439 1439 if (IS_ERR(filename)) (gdb) n ...... ...... 这里原来有一堆单步执行的代码输出,我把它们都删了。对,我就是这么残忍。 (gdb) n 1506 if (retval < 0) (gdb) 1509 retval = copy_strings(bprm->argc, argv, (gdb) 1513 retval = exec_binprm(bprm); #在这里我们见到了exec_binprm()那熟悉的面孔,下面step in (gdb) s exec_binprm (bprm=<optimized out>) at fs/exec.c:1513 1513 retval = exec_binprm(bprm); (gdb) s get_current () at ./arch/x86/include/asm/current.h:14 14 return this_cpu_read_stable(current_task); #请无视上面这行乱入的代码,我也不知道调试的时候怎么避免这样的坑 (gdb) n exec_binprm (bprm=<optimized out>) at fs/exec.c:1411 #从这里进入exec_binprm()函数内部 1411 old_pid = current->pid; (gdb) n 1413 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); (gdb) 1416 ret = search_binary_handler(bprm); #这里调用了 search_binary_handler(), 马上跟进去。 (gdb) s search_binary_handler (bprm=0xc7affd00) at fs/exec.c:1359 #OK,我们要开始找寻合适的处理模块了 1359 if (bprm->recursion_depth > 5) (gdb) n 1362 retval = security_bprm_check(bprm); (gdb) 1363 if (retval) (gdb) 1368 read_lock(&binfmt_lock); (gdb) 1369 list_for_each_entry(fmt, &formats, lh) { #好了,现在开始从链表里选择处理模块 (gdb) s 1370 if (!try_module_get(fmt->module)) (gdb) n 1372 read_unlock(&binfmt_lock); (gdb) 1373 bprm->recursion_depth++; (gdb) 1374 retval = fmt->load_binary(bprm); (gdb) s load_misc_binary (bprm=0xc7affd00) at fs/binfmt_misc.c:133 #当时看到这里都懵了,竟然是misc的载入程序,我可爱的elf呢? 133 if (!enabled) (gdb) n 124 { (gdb) 128 const char *iname_addr = iname; (gdb) 133 if (!enabled) (gdb) 137 read_lock(&entries_lock); (gdb) 138 fmt = check_file(bprm); (gdb) 141 read_unlock(&entries_lock); (gdb) 142 if (!fmt) (gdb) 227 } (gdb) 132 retval = -ENOEXEC; (gdb) 227 } (gdb) search_binary_handler (bprm=0xfffffff8) at fs/exec.c:1375 #现在又回到了模块搜索阶段 ...... ...... #这里略去了一些不重要的输出 (gdb) 1369 list_for_each_entry(fmt, &formats, lh) { (gdb) 1370 if (!try_module_get(fmt->module)) (gdb) 1372 read_unlock(&binfmt_lock); (gdb) 1373 bprm->recursion_depth++; (gdb) 1374 retval = fmt->load_binary(bprm); (gdb) s load_script (bprm=0xc7affd00) at fs/binfmt_script.c:25 #这又是什么gui?这是一个脚本的处理程序?真心不懂。。。 25 if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!')) (gdb) n 99 } (gdb) 26 return -ENOEXEC; (gdb) 99 } (gdb) search_binary_handler (bprm=0xfffffff8) at fs/exec.c:1375 #这家伙又回来了,看来是还没找到处理elf的模块。。。 ...... ...... #这里略去了一些不重要的输出 (gdb) 1374 retval = fmt->load_binary(bprm); (gdb) s load_elf_binary (bprm=0xc7affd00) at fs/binfmt_elf.c:593 #这货终于找到了“东家”。。。终于可以去干“正事”了 593 loc = kmalloc(sizeof(*loc), GFP_KERNEL); (gdb) n 572 { (gdb) 587 struct pt_regs *regs = current_pt_regs(); (gdb) 593 loc = kmalloc(sizeof(*loc), GFP_KERNEL); (gdb) 594 if (!loc) { (gdb) 593 loc = kmalloc(sizeof(*loc), GFP_KERNEL); ...... ...... #下面略去了无数行调试输出。。。 |
五、个人小结
相较于上周的fork调用分析,这次对于execve的分析似乎不是那么“痛苦”,感觉还是挺顺利的。(也许是因为上次那个task_struct结构体实在是太恐怖了。。。)
那么现在来谈一谈个人对于“Linux内核装载、启动可执行程序”的理解。
首先,用户态程序通过sys_execve()系统调用陷入内核,同时也传递了待运行的可执行文件的相关参数。然后内核开始为可执行文件准备环境,包括选择装载模块、分配内核空间、进程描述符和内存等工作。然后通过修改内核的EIP,使其指向新程序的起始地址,退出内核态,转交cpu控制权给处于用户态的新进程,也就是新进程“醒来了”。而之前“创造”该进程的父进程却处于“睡眠”状态,也就是老师所谓的“庄周梦蝶”。
好了,大概就说这么多了,该睡觉了了。