1. 程式人生 > >Linux核心分析:Linux核心如何裝載和啟動一個可執行程式

Linux核心分析:Linux核心如何裝載和啟動一個可執行程式

1.編譯連結的過程和ELF可執行檔案格式

從一個原始碼檔案到一個可執行程式檔案大概要經歷如下過程:
這裡寫圖片描述
以C程式碼為例子,有如下程式碼的一個hello.c檔案

//hello.c
#include <stdio.h>

int main()
{
    printf("hello world!");
    return 0;
}

在shell中,用gcc把其編譯,連結成一個可執行程式,可以分解為以下步驟:(-m32代表將其編譯成32位程式碼)
1. gcc -E -o hello.cpp hello.c -m32
引數-E是預處理,負責把include的檔案包含進來,以及巨集替換等工作。

  1. gcc -x cpp-output -S -o hello.s hello.cpp -m32
    引數-x cpp-output -S是編譯過程,將.cpp檔案編譯成彙編程式碼。

  2. gcc -x assembler -c hello.s -o hello.o -m32
    引數-x assembler -c是彙編過程,將.s檔案中的彙編程式碼彙編成二進位制目的碼。

  3. gcc -o hello hello.o -m32
    將hello.o動態連結成可執行程式。

  4. gcc -o hello-static hello.o -m32 -static
    引數-static代表將hello.o靜態連結成可執行程式。

最後得到的hello和hello-static檔案就是目標檔案。那麼目標檔案格式是怎麼樣的?

目標檔案中的內容至少有編譯後的機器指令程式碼、資料。沒錯,除了這些內容以外,目標檔案中還包括了連結時所須要的一些資訊,比如符號表、除錯資訊、字串等。

現在Linux下主要使用ELF格式(EXECUTABLE AND LINKABLE FORMAT)的目標檔案,它的具體格式如下圖所示:
這裡寫圖片描述
ELF頭描述了該檔案的組織情況,(使用命令readelf -h hello 可以檢視hello檔案的elf頭,其中一個很重要的項是Entry point address,它指定了該程式的程式碼起點地址。)其結構定義如下:
這裡寫圖片描述


text節:被編譯程式的機器程式碼。
rodata節:諸如printf語句中的格式串和switch語句的跳轉表等只讀資料。
data節:已初始化的全域性變數。
bss節(.comm 節):未初始化的全域性變數,在目標檔案中不佔實際的空間。

連結是一個收集、組織程式所需的不同程式碼和資料的過程,以便程式能被裝入記憶體並被執行。連結過程分為兩步:
-空間與地址分配
掃描所有的輸入目標檔案,獲得它們的各個段的長度、屬性和位置,並且將輸入目標檔案中的符號定義和符號引用收集起來,統一放到一個全域性符號表。這一步中,連結器將能獲得所有輸入目標檔案的段長度,並且將它們合併,計算出輸出檔案中各個段合併後的長度與位置,並建立對映關係。
-符號解析與重定位
使用上面第一步中收集到的所有資訊,讀取輸入檔案中段的資料、重定位資訊,並且進行符號解析與重定位、調整程式碼中的地址等。事實上第二步是連結過程的核心,特別是重定位過程。

可執行程式檔案,要被裝入記憶體才能執行,那麼其在記憶體中的映像與檔案中的映像是如何對應的呢?
這裡寫圖片描述
這裡寫圖片描述

2. 程式設計使用exec*庫函式載入一個可執行檔案,動態連結分為可執行程式裝載時動態連結和執行時動態連結,程式設計練習動態連結庫的這兩種使用方式

動態連結分為可執行程式裝載時動態連結和執行時動態連結,如下程式碼演示了這兩種動態連結。
1.先從本週第二節課程下載原始碼,放入一個目錄中:
這裡寫圖片描述

2.準備.so檔案
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
編譯成libshlibexample.so檔案
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
這裡寫圖片描述

dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
編譯成libdllibexample.so檔案
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
這裡寫圖片描述

分別以共享庫和動態載入共享庫的方式使用libshlibexample.so檔案和libdllibexample.so檔案

main.c (1.9 KB) - Main program
3.編譯main,注意這裡只提供shlibexample的-L(庫對應的介面標頭檔案所在目錄)和-l(庫名,如libshlibexample.so去掉lib和.so的部分),並沒有提供dllibexample的相關資訊,只是指明瞭-ldl
$ 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!
這裡寫圖片描述

使用的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;
}

裝入時動態連結:

就是在開頭include了shlibexample.h,那麼在裝入時,就會將libshlibexample.so中的SharedLibApi()裝入程序映像中。

執行時動態連結:

先使用dlopen(),開啟一個動態連結庫,並返回動態連結庫的控制代碼。他相當於Win32 API函式LoadLibrary()。

void * handle = dlopen("libdllibexample.so",RTLD_NOW);

然後定義了一個函式指標,其指標資料型別要與呼叫的so引出函式相吻合:

 int (*func)(void);

然後使用了dlsym(),根據 動態連結庫 操作控制代碼(handle)與符號(symbol),返回符號對應的地址。使用這個函式不但可以獲取函式地址,也可以獲取變數地址。相當於Win32 API函式GetProcAddress()。

func = dlsym(handle,"DynamicalLoadingLibApi");

這樣就可以函式指標func就來呼叫so函式。

func(); 

最後用dlclose()來解除安裝開啟的庫。

dlclose(handle);  

3. Linux系統載入可執行程式所需處理過程的理解

3.1 新的可執行程式是從哪裡開始執行的?

當execve()系統呼叫終止且程序重新恢復它在使用者態執行時,執行上下文被大幅度改變,要執行的新程式已被對映到程序空間,從elf頭中的程式入口點開始執行新程式。

如果這個新程式是靜態連結的,那麼這個程式就可以獨立執行,elf頭中的這個入口地址就是本程式的入口地址。

如果這個新程式是動態連結的,那麼此時還需要裝載共享庫,elf頭中的這個入口地址是動態連結器ld的入口地址。

3.2 為什麼execve系統呼叫返回後新的可執行程式能順利執行?

首先我們看,新的可執行程式執行,需要哪些東西:
1. 它所需要的庫函式。
2. 屬於它的程序空間:程式碼段,資料段,核心棧,使用者棧等。
3. 它所需要的執行引數。
4. 它所需要的系統資源。
如果滿足以上4個條件,那麼新的可執行程式就會處於可執行態,只要被排程到,就可以正常執行。我們一個一個看這幾個條件能不能滿足。
條件1:如果新程序是靜態連結的,那麼庫函式已經在可執行程式檔案中,條件滿足。如果是動態連結的,新程序的入口地址是動態連結器ld的起始地址,可以完成對所需庫函式的載入,也能滿足條件。
條件2:execve系統呼叫通過大幅度修改執行上下文,將使用者態堆疊清空,將老程序的程序空間替換為新程序的程序空間,新程序從老程序那裡繼承了所需要的程序空間,條件滿足。
條件3:我們一般在shell中,輸入可執行程式所需要的引數,shell程式把這些引數用函式引數傳遞的方式傳給給execve系統呼叫,然後execve系統呼叫以系統呼叫引數傳遞的方式傳給sys_execve,最後sys_execve在初始化新程式的使用者態堆疊時,將這些引數放在main函式取引數的位置上。條件滿足。
條件4:如果當前系統中沒有所需要的資源,那麼新程序會被掛起,直到資源有了,喚醒新程序,變為可執行態,條件可以滿足。
綜上所述,新的可執行程式可以順利執行。

3.3 對於靜態連結的可執行程式和動態連結的可執行程式execve系統呼叫返回時會有什麼不同?

execve系統呼叫會呼叫sys_execve,然後sys_execve呼叫do_execve,然後do_execve呼叫do_execve_common,然後do_execve_common呼叫exec_binprm,在exec_binprm中:

ret = search_binary_handler(bprm);//尋找符合檔案格式的對應解析模組(如ELF)
...
//
一個迴圈:
    retval = fmt->load_binary(bprm);
...

對於ELF檔案格式,fmt函式指標實際會執行load_elf_binary,load_elf_binary會呼叫start_thread,在start_thread中通過修改核心堆疊中EIP的值,使其指向elf_entry,跳轉到elf_entry執行。
對於靜態連結的可執行程式,elf_entry是新程式的執行起點。對於動態連結的可執行程式,需要先載入連結器ld,
elf_entry = load_elf_interp(…)
將CPU控制權交給ld來載入依賴庫,再由ld在完成載入工作後將CPU控制權還給新程序。

4. 使用gdb跟蹤分析一個execve系統呼叫核心處理函式sys_execve ,驗證您對Linux系統載入可執行程式所需處理過程的理解

首先從github下載最新的程式碼,然後進入menu目錄:
這裡寫圖片描述
然後用test_exec.c 替換test.c,再重新編譯生成根檔案系統:
這裡寫圖片描述
然後啟動除錯核心:
這裡寫圖片描述
再開一個shell,開啟gdb,並載入符號表,連線到埠1234:
這裡寫圖片描述
然後在gdb,中設定斷點:這裡我設定了sys_execve,load_elf_binary,
start_thread三處斷點。
這裡寫圖片描述
這裡寫圖片描述
然後在menu介面中輸入命令:exec ,觸發了斷點。
這裡寫圖片描述
發現程式第一個斷點停在了sys_execve處:
這裡寫圖片描述
按s,單步執行,發現接下來會執行do_execve:
這裡寫圖片描述
按c繼續執行,發現接下來停在了load_elf_binary處:
這裡寫圖片描述
按c繼續執行,發現接下來停在了start_thread處,這時使用命令:
po new_ip 檢視new_ip的值,它等於0x8048d0a,再另外開啟一個shell,使用命令readelf -h hello檢視hello的elf頭,可以看到elf頭中的程式入口點地址正是0x8048d0a
這裡寫圖片描述
按s,單步執行,可以看到在start_thread中對程序棧的修改。
這裡寫圖片描述

5. 自己對“Linux核心裝載和啟動一個可執行程式”的理解

可執行檔案是一個普通的檔案,它描述瞭如何初始化一個新的執行上下文,也就是如何開始一個新的計算。可執行檔案類別有很多,在核心中有一個連結串列,在init的時候會將支援的可執行程式解析程式註冊新增到連結串列中,那麼在對可執行檔案進行解析時,就從連結串列頭開始找,找到匹配的處理函式就可以對其進行解析。
在shell中啟動一個可執行程式時,會建立一個新程序,它通過覆蓋父程序(也就是shell程序)的程序環境,並將使用者態堆疊清空,獲得說需要的執行上下文環境。
命令列引數和環境變數會通過shell傳遞給execve,excve通過系統呼叫引數傳遞,傳遞給sys_execve,最後sys_execve在初始化新程序堆疊的時候拷貝進去。
load_elf_binary->start_thread(…)通過修改核心堆疊中EIP的值作為新程式的起點。
如果新程式的動態連結的,那麼就需要載入所需要的庫函式,動態聯結器ld會負責載入過程,動態連結庫的裝載過程類似於一個圖的廣度優先遍歷過程,裝載完成後,ld將CPU控制權交給可執行程式,繼續執行可執行程式。

參考資料