郭垚 原創做品轉載請註明出處 《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指向動態連接器。