《Linux內核分析》第七週學習筆記

《Linux內核分析》第七週學習筆記 可執行程序的裝載

郭垚 原創做品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000linux

【學習視頻時間:1小時35分鐘 實驗時間:1小時 撰寫博客時間:2小時40分鐘】shell

【學習內容:共享庫和動態連接、exec系統調用的執行過程、可執行程序的裝載】編輯器

1、預處理、編譯、連接和目標文件的格式

1.1 可執行程序如何得來?

過程:函數

  1. C源代碼(.c)通過編譯器預處理被編譯成彙編代碼(.asm)
  2. 彙編代碼由彙編器被編譯成目標代碼(.o)
  3. 將目標代碼連接成可執行文件(a.out)
  4. 可執行文件由操做系統加載到內存中執行

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 

注:學習

  • hello和hello.o都是ELF格式的文件
  • .static文件將全部用到C庫文件都放到某一可執行程序中,因此佔用空間較多

1.2 目標文件的格式ELF

1. .out是最古老的可執行文件,目前Windows系統上可能是PE,Linux系統上可能是ELF。ELF文件已是適應到某一種CPU體系結構的二進制兼容文件了spa

2. 目標文件的三種形式:操作系統

  • 可重定位文件.o,用來和其餘object文件一塊兒建立可執行文件和共享文件
  • 可執行文件,指出應該從哪裏開始執行
  • 共享文件,主要是.so文件,用來被連接編輯器和動態連接器連接

3. ELF格式命令行

  • ELF頭描述了該文件的組織狀況,程序投標告訴系統如何建立一個進程的內存映像,section頭表包含了描述文件sections的信息。當系統要執行一個文件的時候,理論上它會把程序段拷貝到虛擬內存中某個段
  • ELF文件的頭部規定了許多與二進制兼容性相關的信息。因此在加載ELF文件的時候,必須先加載頭部,分析ELF的具體信息
  • entry表明剛加載過新的可執行文件以後的程序的入口地址,頭部後是代碼和數據,進程的地址空間是4G,上面的1G是內核用,下面的3G是程序使用。默認的ELF頭加載地址是0x8048000

1.3 靜態連接的ELF可執行文件和進程的地址空間

1. 可執行文件加載到內存時:3d

  • 加載效果:將代碼段數據加載到內存中,再把數據加載到內存,默認從0x8048000地址開始加載
  • 啓動一個剛加載過可執行文件的進程時,可執行文件加載到內存以後執行的第一條代碼地址
  • 通常靜態連接會將全部代碼放在一個代碼段,而動態連接的進程會有多個代碼段

2. 流程指針

  • 分析頭部
  • 查看是否須要動態連接。若是是靜態連接的ELF文件,那麼直接加載文件便可。若是是動態連接的可執行文件,那麼須要加載的是動態連接器
  • 裝載文件,爲其準備進程映像
  • 爲新的代碼段設定寄存器以及堆棧信息

2、可執行程序、共享庫和動態連接

2.1 裝載可執行程序以前的工做

1. 可執行程序的執行環境

  • 通常執行一個程序的Shell環境,實驗中直接使用execve系統調用
  • 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. 命令行參數和環境串都放在用戶態堆棧中

  • fork子進程的時候徹底複製了父進程
  • 調用exec的時候,要加載的可執行程序把原來的進程環境覆蓋掉,用戶態堆棧也被清空
  • 命令行參數和環境變量進入新程序的堆棧時,把環境變量和命令行參數壓棧,至關於main函數啓動
  • shell程序——>execve——>sys_execve,而後在初始化新程序堆棧的時候拷貝進去
  • 先傳遞函數調用參數,再傳遞系統調用參數

2.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。

3、可執行程序的裝載

3.1 可執行程序的裝載相關關鍵問題分析

1. execve與fork是比較特殊的系統調用

  • execve用它加載的可執行文件把當前的進程覆蓋掉,返回以後就不是原來的程序而是新的可執行程序起點;
  • fork函數的返回點ret_ from_fork是用戶態起點

2. sys_ execve內核處理過程

  • do_ execve -> do_ execve_ common -> exec_ binprm -> search_ binary_handler,最後根據文件頭部信息尋找對應的文件格式處理模塊

3.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的內部處理過程

  • 打開file文件,找到文件頭部,把命令行參數和環境變量copy到結構體中
  • 尋找打開的可執行文件處理函數
  • 尋找可以解析當前可執行文件的模塊,load_ binary加載這個模塊,它實際調用的是binfmt_ elf.c
  • 須要動態連接的可執行文件先加載鏈接器ld;不然直接把ELF文件entry地址賦值給entry
  • start_ thread(regs, elf_ entry, bprm->p)將CPU控制權交給ld來加載依賴庫並完成動態連接。對於靜態連接的文件elf_entry是新程序執行的起點

3.3 使用gdb跟蹤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頭部

3.4 可執行程序的裝載與莊周夢蝶的故事

  莊周(調用execve的可執行程序)入睡(調用execve陷入內核),醒來(系統調用execve返回用戶態)發現本身是蝴蝶(被execve加載的可執行程序)

3.5 淺析動態連接的可執行程序的裝載

1. 動態連接的過程當中,內核作了什麼?

ldd test

ldd libfuse.so //可執行程序須要依賴動態連接庫,而這個動態連接庫可能會依賴其餘的庫,實際上動態連接庫的依賴關係會造成一個圖

 

2. 是由內核負責加載可執行程序依賴的動態連接庫嗎?

  • 動態連接器負責加載這些庫並進行解析當前的可執行文件,裝載全部須要的動態連接庫,動態連接庫的裝載過程是一個圖的遍歷(廣度)
  • 裝載和連接後ld將CPU的控制權交給可執行程序
  • 動態連接的過程主要由動態連接器完成,並非內核

總結

  經過對本週視頻的學習,我瞭解到exec系統調用的執行過程與fork有些不一樣。fork一個新進程時,子進程的堆棧和父進程徹底相同,寄存器信息也徹底相同,僅僅把系統調用的返回值eax清零。而這裏將寄存器清零,堆棧是全新分配的,對於eip,若是是靜態連接的可執行文件,那麼eip指向該elf文件的文件頭e_entry所指的入口地址;若是是動態連接,eip指向動態連接器。

相關文章
相關標籤/搜索