学号后三位<168>
原创作品转载请注明出处https://github.com/mengning/linuxkernel/
1.分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构
Linux中创建进程一共有三个函数
- fork,创建子进程
- vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
- clone,主要用于创建线程
进程创建过程:
1 YSCALL_DEFINE0(fork) 2 { 3 return do_fork(SIGCHLD, 0, 0, NULL, NULL); 4 } 5 #endif 6 7 SYSCALL_DEFINE0(vfork) 8 { 9 return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 10 0, NULL, NULL); 11 } 12 13 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, 14 int __user *, parent_tidptr, 15 int __user *, child_tidptr, 16 int, tls_val) 17 { 18 return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr); 19 }
do_fork 代码分析:
1 long do_fork(unsigned long clone_flags, 2 unsigned long stack_start, 3 unsigned long stack_size, 4 int __user *parent_tidptr, 5 int __user *child_tidptr) 6 { 7 struct task_struct *p; 8 int trace = 0; 9 long nr; 10 11 // ... 12 13 // 复制进程描述符,返回创建的task_struct的指针 14 p = copy_process(clone_flags, stack_start, stack_size, 15 child_tidptr, NULL, trace); 16 17 if (!IS_ERR(p)) { 18 struct completion vfork; 19 struct pid *pid; 20 21 trace_sched_process_fork(current, p); 22 23 // 取出task结构体内的pid 24 pid = get_task_pid(p, PIDTYPE_PID); 25 nr = pid_vnr(pid); 26 27 if (clone_flags & CLONE_PARENT_SETTID) 28 put_user(nr, parent_tidptr); 29 30 // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 31 if (clone_flags & CLONE_VFORK) { 32 p->vfork_done = &vfork; 33 init_completion(&vfork); 34 get_task_struct(p); 35 } 36 37 // 将子进程添加到调度器的队列,使得子进程有机会获得CPU 38 wake_up_new_task(p); 39 40 // ... 41 42 // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 43 // 保证子进程优先于父进程运行 44 if (clone_flags & CLONE_VFORK) { 45 if (!wait_for_vfork_done(p, &vfork)) 46 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 47 } 48 49 put_pid(pid); 50 } else { 51 nr = PTR_ERR(p); 52 } 53 return nr; 54 }
do_fork处理了以下内容:
- 调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
- 初始化vfork的完成处理信息(如果是vfork调用)
- 调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
- 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。
2.使用gdb跟踪分析一个fork系统调用内核处理函数do_fork ,验证您对Linux系统创建一个新进程的理解
ret_from_fork;决定了新进程的第一条指令地址。 在ret_from_fork之前,也就是在copy_thread()函数中childregs = current_pt_regs();该句将父进程的regs参数赋值到子进程的内核堆栈, *childregs的类型为pt_regs,里面存放了SAVE ALL中压入栈的参数 故在之后的RESTORE ALL中能顺利执行下去
3.理解编译链接的过程和ELF可执行文件格式
编译过程
编译->汇编->链接 1)预处理 gcc -E -o hello.cpp hello.c -m32 2)编译为汇编代码 gcc -x cpp-output -S -o hello.s hello.cpp -m32 3)汇编代码编译为目标代码 gcc -x assembler -c hello.s -o hello.o -m32 4)链接 gcc -o hello hello.o -m32 使用共享库的编译,libc, printf 5) 静态编译(所依赖的都放在hello.static内部) gcc -o hello.static hello.o -m32 -static
文件格式:
a.out
COFF
PE
ELF(EXECUTABLE AND LINKABLE FORMAT)
三种目标文件:
- 可重定位文件 .o文件
- 可执行文件
- 共享目标文件 .so文件
用来被两个链接器链接:链接编辑器,可以和其他可重定位和共享文件object来创建其他object(静态链接); 动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像 .
ELF文件加载内存(静态链接,所有代码放在一个段),形成进程,默认是加载到以0x8048000开始处
gcc -shared shlibexample.c -o libshlibexample.so -m32 gcc -shared dllibexample.c -o libdllibexample.so -m32 gcc main.c -o main -L$PWD -lshlibexample -ldl -m32 //-ldl动态加载库 export LD_LIBRARY_PATH=$PWD //当前目录加入到库搜索路径
4.编程使用exec*库函数加载一个可执行文件可执行文件的装载
- 执行一个程序的shell环境,直接使用execve系统调用
- shell不限制命令行个数,取决于命令本身
- shell调用execve将命令行参数和环境参数传递给可执行程序的main函数
- int execve(const char filename, char const argv[], char * const envp[])
- 库函数exec*是系统调用execve的封装例程
- sys_execve会解析可执行文件格式
- do_execve -> do_execve_common -> exec_binprm
- search_binary_handler符合寻找文件格式对应的解析模块(根据文件头部信息寻找对应的文件格式处理模块
list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; retval = fmt->load_binary(bprm);//解析elf文件格式的执行位置 read_lock(&binfmt_lock);
- 对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
- Linux内核是如何支持多种不同的可执行文件格式的?
static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, }; static int __init init_elf_binfmt(void) { register_binfmt(&elf_format); return 0; }
五、总结
1、进程创建
fork创建一个子进程,父进程在运行中间过程中fork,产生一个子进程,子进程不会从头开始运行代码,而会从fork的开始的后面的代码开始运行。exec的调用即是替换掉子进程,进而替换上有用的想要执行的进程,避免父子进程完全一样的浪费,若是替换成功,则不会返回。
fork和exec系统调用最终都是通过int 0x80软中断 + EAX寄存器(存储对应的系统调用号)进入内核,在内核中fork和exec对应找到sys_fork/do_fork和sys_exec/do_exec。do_fork主要的工作就是创建一个新进程,创建的方法是拷贝当前进程、分配新的进程pid、插入进程相关链表队列中等。do_exec的工作较为复杂,它的主要目标是将一个可执行程序加载到当前进程中来,返回到用户态时EIP指向可执行程序的入口位置(即0x08048000)。
2、可执行程序的加载过程
可执行程序的加载过程可以分为两种情况:一种是加载静态编译的ELF文件,只需要将代码段加载到0x08048000的位置,其他的数据也根据规则加载即可;另一种情况更常见需要动态链接。
用共享库来动态链接的过程,共享库就是为了解决这一问题,共享库是一个目标模块,在运行时可被加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程叫做动态链接,是由动态链接器的程序来完成的。
大致步骤:
(1)源程序文件和头文件等被翻译器生成可重定位的目标文件;
(2)链接器把可重定位目标文件和共享库的重定位和符号表的信息经过链接生成部分链接的可执行目标文件;
(3)加载时,由动态链接器把部分链接的可执行文件和共享库的代码、数据完全链接成完全可执行文件。