原创作品转载请注明出处 ,《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控制权给处于用户态的新进程,也就是新进程“醒来了”。而之前“创造”该进程的父进程却处于“睡眠”状态,也就是老师所谓的“庄周梦蝶”。
好了,大概就说这么多了,该睡觉了了。