郭垚 原创做品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000linux
【学习视频时间:1小时35分钟 实验时间:1小时 撰写博客时间:2小时40分钟】shell
【学习内容:共享库和动态连接、exec系统调用的执行过程、可执行程序的装载】编辑器
过程:函数
vi hello.c gcc -E -o hello.cpp hello.c -m32 //预处理.c文件,预处理包括把include的文件包含进来以及宏替换等工做 vi hello.cpp gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码.s vi hello.s gcc -x assembler -c hello.s -o hello.o -m32 //将汇编代码.s编译成二进制目标文件.o(不可读,含有部分机器指令但不可执行) vi hello.o gcc -o hello hello.o -m32 //将目标文件连接成可执行二进制文件hello vi hello gcc -o hello.static hello.o -m32 -static
注:学习
1. .out是最古老的可执行文件,目前Windows系统上可能是PE,Linux系统上可能是ELF。ELF文件已是适应到某一种CPU体系结构的二进制兼容文件了spa
2. 目标文件的三种形式:操作系统
3. ELF格式命令行
1. 可执行文件加载到内存时:3d
2. 流程指针
1. 可执行程序的执行环境
Shell自己不限制命令行参数的个数,命令行参数的个数受限于命令自身,如:
int main(int argc, char *argv[]) int main(int argc, char argv[], char envp[])//envp是shell的执行环境
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
2. 命令行参数和环境串都放在用户态堆栈中
1. 动态连接分为可执行程序装载时动态连接和运行时动态连接,大部分使用可执行程序装载时动态连接。
2. 共享库的动态连接
准备.so文件(在Linux下动态连接文件格式,在Windows中是.dll)
#ifndef _SH_LIB_EXAMPLE_H_ #define _SH_LIB_EXAMPLE_H_ #define SUCCESS 0 #define FAILURE (-1) #ifdef __cplusplus extern "C" { #endif /* * Shared Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int SharedLibApi();//内容只有一个函数头定义 #ifdef __cplusplus } #endif #endif /* _SH_LIB_EXAMPLE_H_ */ /*------------------------------------------------------*/ #include <stdio.h> #include "shlibexample.h" int SharedLibApi() { printf("This is a shared libary!\n"); return SUCCESS; }/* _SH_LIB_EXAMPLE_C_ */
编译成.so文件
gcc -shared shlibexample.c -o libshlibexample.so -m32
3. 动态加载库
#ifndef _DL_LIB_EXAMPLE_H_ #define _DL_LIB_EXAMPLE_H_ #ifdef __cplusplus extern "C" { #endif /* * Dynamical Loading Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int DynamicalLoadingLibApi(); #ifdef __cplusplus } #endif #endif /* _DL_LIB_EXAMPLE_H_ */ /*------------------------------------------------------*/ #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; }
4. main.c
#include <stdio.h> #include "shlibexample.h" //只include了共享库 #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();//能够直接调用,由于include了这个库的接口 /* 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);//与dlopen函数配合,用于卸载连接库 return SUCCESS; }
dlsym函数与上面的dlopen函数配合使用,经过dlopen函数返回的动态库句柄(由dlopen打开动态连接库后返回的指针handle)以及对应的符号返回符号对应的指针。
5. 编译main.c
$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 $ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,不然main找不到依赖的库文件,固然也能够将库文件copy到默认路径下。 $ ./main 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!
注:这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并无提供dllibexample的相关信息,只是指明了-ldl。
1. execve与fork是比较特殊的系统调用
2. sys_ execve内核处理过程
1. exec通常和fork调用,常规用法是fork出一个子进程,而后在子进程中执行exec,替换为新的代码。
2. do_exec函数
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { return do_execve_common(filename, argv, envp); } static int do_execve_common(struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp) { // 检查进程的数量限制 // 选择最小负载的CPU,以执行新程序 sched_exec(); // 填充 linux_binprm结构体 retval = prepare_binprm(bprm); // 拷贝文件名、命令行参数、环境变量 retval = copy_strings_kernel(1, &bprm->filename, bprm); retval = copy_strings(bprm->envc, envp, bprm); retval = copy_strings(bprm->argc, argv, bprm); // 调用里面的 search_binary_handler retval = exec_binprm(bprm); // exec执行成功 } static int exec_binprm(struct linux_binprm *bprm) { // 扫描formats链表,根据不一样的文本格式,选择不一样的load函数 ret = search_binary_handler(bprm); // ... return ret; }
由以上代码可知,do_ execve调用了do_ execve_ common,而do_ execve_ common又主要依靠了exec_ binprm,在exec_ binprm中又有一个相当重要的函数,叫作search_ binary_ handler。
3. sys_execve的内部处理过程
1. 开始先更新内核,再用test_exec.c将test.c覆盖掉
2. test.c文件中增长了exec系统调用,Makefile文件中增长了gcc -o hello hello.c -m32 -static
3. 启动内核并验证execv函数
4. 启动gdb调试
5. 先停在sys_execve处,再设置其它断点
6. 进入函数单步执行
7. new_ip是返回到用户态的第一条指令
8. 退出调试状态后输入redelf -h hello能够查看hello的EIF头部
庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现本身是蝴蝶(被execve加载的可执行程序)
1. 动态连接的过程当中,内核作了什么?
ldd test ldd libfuse.so //可执行程序须要依赖动态连接库,而这个动态连接库可能会依赖其余的库,实际上动态连接库的依赖关系会造成一个图
2. 是由内核负责加载可执行程序依赖的动态连接库吗?
经过对本周视频的学习,我了解到exec系统调用的执行过程与fork有些不一样。fork一个新进程时,子进程的堆栈和父进程彻底相同,寄存器信息也彻底相同,仅仅把系统调用的返回值eax清零。而这里将寄存器清零,堆栈是全新分配的,对于eip,若是是静态连接的可执行文件,那么eip指向该elf文件的文件头e_entry所指的入口地址;若是是动态连接,eip指向动态连接器。