ELF文件的載入過程(load_elf_binary函數詳解)--Linux進程的管理與調度(十三)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/09/16/9655301.html
-Advertisement-
Play Games

載入和動態鏈接 從編譯/鏈接和運行的角度看,應用程式和庫程式的連接有兩種方式。 一種是固定的、靜態的連接,就是把需要用到的庫函數的目標代碼(二進位)代碼從程式庫中抽取出來,鏈接進應用軟體的目標映像中; 另一種是動態鏈接,是指庫函數的代碼並不進入應用軟體的目標映像,應用軟體在編譯/鏈接階段並不完成跟庫 ...


載入和動態鏈接

從編譯/鏈接和運行的角度看,應用程式和庫程式的連接有兩種方式。

一種是固定的、靜態的連接,就是把需要用到的庫函數的目標代碼(二進位)代碼從程式庫中抽取出來,鏈接進應用軟體的目標映像中;

另一種是動態鏈接,是指庫函數的代碼並不進入應用軟體的目標映像,應用軟體在編譯/鏈接階段並不完成跟庫函數的鏈接,而是把函數庫的映像也交給用戶,到啟動應用軟體目標映像運行時才把程式庫的映像也裝入用戶空間(並加以定位),再完成應用軟體與庫函數的連接。

這樣,就有了兩種不同的ELF格式映像。

  • 一種是靜態鏈接的,在裝入/啟動其運行時無需裝入函數庫映像、也無需進行動態連接。
  • 另一種是動態連接,需要在裝入/啟動其運行時同時裝入函數庫映像併進行動態鏈接。

Linux內核既支持靜態鏈接的ELF映像,也支持動態鏈接的ELF映像,而且裝入/啟動ELF映像必需由內核完成,而動態連接的實現則既可以在內核中完成,也可在用戶空間完成。

因此,GNU把對於動態鏈接ELF映像的支持作了分工:

把ELF映像的裝入/啟動入在Linux內核中;而把動態鏈接的實現放在用戶空間(glibc),併為此提供一個稱為”解釋器”(ld-linux.so.2)的工具軟體,而解釋器的裝入/啟動也由內核負責,這在後面我們分析ELF文件的載入時就可以看到

這部分主要說明ELF文件在內核空間的載入過程,下一部分對用戶空間符號的動態解析過程進行說明。

Linux可執行文件類型的註冊機制

在說明ELF文件的載入過程以前,我們先回答一個問題,就是:

為什麼Linux可以運行ELF文件?

內核對所支持的每種可執行的程式類型都有個struct linux_binfmt的數據結構,這個結構我們在前面的博文中我們已經提到, 但是沒有詳細講. 其定義如下

/*
  * This structure defines the functions that are used to load the binary formats that
  * linux accepts.
  */
struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump;     /* minimal dump size */
 };

linux_binfmt定義在include/linux/binfmts.h中

linux支持其他不同格式的可執行程式, 在這種方式下, linux能運行其他操作系統所編譯的程式, 如MS-DOS程式, 活BSD Unix的COFF可執行格式, 因此linux內核用struct linux_binfmt來描述各種可執行程式。

linux內核對所支持的每種可執行的程式類型都有個struct linux_binfmt的數據結構,

其提供了3種方法來載入和執行可執行程式

函數 描述
load_binary 通過讀存放在可執行文件中的信息為當前進程建立一個新的執行環境
load_shlib 用於動態的把一個共用庫捆綁到一個已經在運行的進程, 這是由uselib()系統調用激活的
core_dump 在名為core的文件中, 存放當前進程的執行上下文. 這個文件通常是在進程接收到一個預設操作為”dump”的信號時被創建的, 其格式取決於被執行程式的可執行類型

所有的linux_binfmt對象都處於一個鏈表中, 第一個元素的地址存放在formats變數中, 可以通過調用register_binfmt()和unregister_binfmt()函數在鏈表中插入和刪除元素, 在系統啟動期間, 為每個編譯進內核的可執行格式都執行registre_fmt()函數. 當實現了一個新的可執行格式的模塊正被裝載時, 也執行這個函數, 當模塊被卸載時, 執行unregister_binfmt()函數.

當我們執行一個可執行程式的時候, 內核會list_for_each_entry遍歷所有註冊的linux_binfmt對象, 對其調用load_binrary方法來嘗試載入, 直到載入成功為止.

其中的load_binary函數指針指向的就是一個可執行程式的處理函數。而我們研究的ELF文件格式的linux_binfmt結構對象elf_format, 定義如下, 在/fs/binfmt.c中

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,
    .hasvdso     = 1
};

要支持ELF文件的運行,則必須向內核登記註冊elf_format這個linux_binfmt類型的數據結構,加入到內核支持的可執行程式的隊列中。內核提供兩個函數來完成這個功能,一個註冊,一個註銷,即:

int register_binfmt(struct linux_binfmt * fmt)
int unregister_binfmt(struct linux_binfmt * fmt)

當需要運行一個程式時,則掃描這個隊列,依次調用各個數據結構所提供的load處理程式來進行載入工作,ELF中載入程式即為load_elf_binary,內核中已經註冊的可運行文件結構linux_binfmt會讓其所屬的載入程式load_binary逐一前來認領需要運行的程式binary,如果某個格式的處理程式發現相符後,便執行該格式映像的裝入和啟動

內核空間的載入過程load_elf_binary

內核中實際執行execv()或execve()系統調用的程式是do_execve(),這個函數先打開目標映像文件,並從目標文件的頭部(第一個位元組開始)讀入若幹(當前Linux內核中是128)位元組(實際上就是填充ELF文件頭,下麵的分析可以看到),然後調用另一個函數search_binary_handler(),在此函數裡面,它會搜索我們上面提到的Linux支持的可執行文件類型隊列,讓各種可執行程式的處理程式前來認領和處理。如果類型匹配,則調用load_binary函數指針所指向的處理函數來處理目標映像文件。

在ELF文件格式中,處理函數是load_elf_binary函數,下麵主要就是分析load_elf_binary函數的執行過程(說明:因為內核中實際的載入需要涉及到很多東西,這裡只關註跟ELF文件的處理相關的代碼)

其流程如下:

  1. 填充並且檢查目標程式ELF頭部
  2. load_elf_phdrs載入目標程式的程式頭表
  3. 如果需要動態鏈接, 則尋找和處理解釋器段
  4. 檢查並讀取解釋器的程式表頭
  5. 裝入目標程式的段segment
  6. create_elf_tables填寫目標文件的參數環境變數等必要信息
  7. start_kernel巨集準備進入新的程式入口

填充並且檢查目標程式ELF頭部

struct pt_regs *regs = current_pt_regs();
struct {
    struct elfhdr elf_ex;
    struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;

loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {
    retval = -ENOMEM;
    goto out_ret;
}

/* Get the exec-header
    使用映像文件的前128個位元組對bprm->buf進行了填充  */
loc->elf_ex = *((struct elfhdr *)bprm->buf);

retval = -ENOEXEC;
/* First of all, some simple consistency checks
    比較文件頭的前四個位元組
    。*/
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
    goto out;
/*  還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共用庫  */
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
    goto out;

在load_elf_binary之前,內核已經使用映像文件的前128個位元組對bprm->buf進行了填充,563行就是使用這此信息填充映像的文件頭(具體數據結構定義見第一部分,ELF文件頭節),然後567行就是比較文件頭的前四個位元組,查看是否是ELF文件類型定義的“\177ELF”。除這4個字元以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共用庫。

load_elf_phdrs載入目標程式的程式頭表

elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
    goto out;

而這個load_elf_phdrs函數就是通過kernel_read讀入整個program header table。從函數代碼中可以看到,一個可執行程式必須至少有一個段(segment),而所有段的大小之和不能超過64K(65536u)

/**
 * load_elf_phdrs() - load ELF program headers
 * @elf_ex:   ELF header of the binary whose program headers should be loaded
 * @elf_file: the opened ELF binary file
 *
 * Loads ELF program headers from the binary file elf_file, which has the ELF
 * header pointed to by elf_ex, into a newly allocated array. The caller is
 * responsible for freeing the allocated data. Returns an ERR_PTR upon failure.
 */
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,
                                   struct file *elf_file)
{
    struct elf_phdr *elf_phdata = NULL;
    int retval, size, err = -1;

    /*
     * If the size of this structure has changed, then punt, since
     * we will be doing the wrong thing.
     */
    if (elf_ex->e_phentsize != sizeof(struct elf_phdr))
            goto out;

    /* Sanity check the number of program headers... */
    if (elf_ex->e_phnum < 1 ||
            elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr))
            goto out;

    /* ...and their total size. */
    size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
    if (size > ELF_MIN_ALIGN)
            goto out;

    elf_phdata = kmalloc(size, GFP_KERNEL);
    if (!elf_phdata)
            goto out;

    /* Read in the program headers */
    retval = kernel_read(elf_file, elf_ex->e_phoff,
                         (char *)elf_phdata, size);
    if (retval != size) {
            err = (retval < 0) ? retval : -EIO;
            goto out;
    }

    /* Success! */
    err = 0;
out:
    if (err) {
            kfree(elf_phdata);
            elf_phdata = NULL;
    }
    return elf_phdata;
}

如果需要動態鏈接, 則尋找和處理解釋器段

這個for迴圈的目的在於尋找和處理目標映像的”解釋器”段。

“解釋器”段的類型為PT_INTERP,

找到後就根據其位置的p_offset和大小p_filesz把整個”解釋器”段的內容讀入緩衝區。

“解釋器”段實際上只是一個字元串,

即解釋器的文件名,如”/lib/ld-linux.so.2”, 或者64位機器上對應的叫做”/lib64/ld-linux-x86-64.so.2”

有瞭解釋器的文件名以後,就通過open_exec()打開這個文件,再通過kernel_read()讀入其開關128個位元組,即解釋器映像的頭部。*

for (i = 0; i < loc->elf_ex.e_phnum; i++) {
        /*  3.1  檢查是否有需要載入的解釋器  */
        if (elf_ppnt->p_type == PT_INTERP) {
            /* This is the program interpreter used for
             * shared libraries - for now assume that this
             * is an a.out format binary
             */

            /*  3.2 根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區  */
            retval = kernel_read(bprm->file, elf_ppnt->p_offset,
                         elf_interpreter,
                         elf_ppnt->p_filesz);

            if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
                goto out_free_interp;
            /*  3.3 通過open_exec()打開解釋器文件 */
            interpreter = open_exec(elf_interpreter);



            /* Get the exec headers 
               3.4  通過kernel_read()讀入解釋器的前128個位元組,即解釋器映像的頭部。*/
            retval = kernel_read(interpreter, 0,
                         (void *)&loc->interp_elf_ex,
                         sizeof(loc->interp_elf_ex));


            break;
        }
        elf_ppnt++;
    }

可以使用readelf -l查看program headers, 其中的INTERP段標識了我們程式所需要的解釋器

readelf -l testelf_normal

readelf -l testelf_dynamic

readelf -l test_static

我們可以看到testelf_normal和testelf_dynamic都是動態鏈接的需要解釋器

而testelf_static則是靜態鏈接的不需要解釋器

檢查並讀取解釋器的程式表頭

如果需要載入解釋器, 前面經過一趟for迴圈已經找到了需要的解釋器信息elf_interpreter, 他也是當作一個ELF文件, 因此跟目標可執行程式一樣, 我們需要load_elf_phdrs載入解釋器的程式頭表program header table

/*   4.    檢查並讀取解釋器的程式表頭 */

/* Some simple consistency checks for the interpreter 
4.1  檢查解釋器頭的信息  */
if (elf_interpreter) {
retval = -ELIBBAD;
/* Not an ELF interpreter */

/* Load the interpreter program headers
   4.2  讀入解釋器的程式頭
 */
interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
                   interpreter);
if (!interp_elf_phdata)
    goto out_free_dentry;

至此我們已經把目標執行程式和其所需要的解釋器都載入初始化, 並且完成檢查工作, 也載入了程式頭表program header table, 下麵開始載入程式的段信息

裝入目標程式的段segment

這段代碼從目標映像的程式頭中搜索類型為PT_LOAD的段(Segment)。在二進位映像中,只有類型為PT_LOAD的段才是需要裝入的。當然在裝入之前,需要確定裝入的地址,只要考慮的就是頁面對齊,還有該段的p_vaddr域的值(上面省略這部分內容)。確定了裝入地址後,就通過elf_map()建立用戶空間虛擬地址空間與目標映像文件中某個連續區間之間的映射,其返回值就是實際映射的起始地址。

    */
    for(i = 0, elf_ppnt = elf_phdata;
    i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {

    /*  5.1   搜索PT_LOAD的段, 這個是需要裝入的 */
    if (elf_ppnt->p_type != PT_LOAD)
        continue;


        /* 5.2  檢查地址和頁面的信息  */
        ////////////
        // ......
        ///////////

     /*  5.3  虛擬地址空間與目標映像文件的映射
     確定了裝入地址後,
     就通過elf_map()建立用戶空間虛擬地址空間
     與目標映像文件中某個連續區間之間的映射,
     其返回值就是實際映射的起始地址 */
    error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
            elf_prot, elf_flags, total_size);

    }

填寫程式的入口地址

完成了目標程式和解釋器的載入, 同時目標程式的各個段也已經載入到記憶體了, 我們的目標程式已經準備好了要執行了, 但是還缺少一樣東西, 就是我們程式的入口地址, 沒有入口地址, 操作系統就不知道從哪裡開始執行記憶體中載入好的可執行映像

這段程式的邏輯非常簡單:
如果需要裝入解釋器,就通過load_elf_interp裝入其映像, 並把將來進入用戶空間的入口地址設置成load_elf_interp()的返回值,即解釋器映像的入口地址。

而若不裝入解釋器,那麼這個入口地址就是目標映像本身的入口地址。

if (elf_interpreter) {
    unsigned long interp_map_addr = 0;
    
    elf_entry = load_elf_interp(&loc->interp_elf_ex,
                interpreter,
                &interp_map_addr,
                load_bias, interp_elf_phdata);
    /*  入口地址是解釋器映像的入口地址  */
    } else {
    /*  入口地址是目標程式的入口地址  */
    elf_entry = loc->elf_ex.e_entry;
    }
}

create_elf_tables填寫目標文件的參數環境變數等必要信息

在完成裝入,啟動用戶空間的映像運行之前,還需要為目標映像和解釋器準備好一些有關的信息,這些信息包括常規的argc、envc等等,還有一些“輔助向量(Auxiliary Vector)”。這些信息需要複製到用戶空間,使它們在CPU進入解釋器或目標映像的程式入口時出現在用戶空間堆棧上。這裡的create_elf_tables()就起著這個作用。

    install_exec_creds(bprm);
    retval = create_elf_tables(bprm, &loc->elf_ex,
              load_addr, interp_load_addr);
    if (retval < 0)
        goto out;
    /* N.B. passed_fileno might not be initialized? */
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;

start_thread巨集準備進入新的程式入口

最後,start_thread()這個巨集操作會將eip和esp改成新的地址,就使得CPU在返回用戶空間時就進入新的程式入口。如果存在解釋器映像,那麼這就是解釋器映像的程式入口,否則就是目標映像的程式入口。那麼什麼情況下有解釋器映像存在,什麼情況下沒有呢?如果目標映像與各種庫的鏈接是靜態鏈接,因而無需依靠共用庫、即動態鏈接庫,那就不需要解釋器映像;否則就一定要有解釋器映像存在。

start_thread巨集是一個體繫結構相關的函數,請定義可以參照http://lxr.free-electrons.com/ident?v=4.6;i=start_thread

總結

簡單來說可以分成這幾步

  1. 讀取並檢查目標可執行程式的頭信息, 檢查完成後載入目標程式的程式頭表
  2. 如果需要解釋器則讀取並檢查解釋器的頭信息, 檢查完成後載入解釋器的程式頭表
  3. 裝入目標程式的段segment, 這些才是目標程式二進位代碼中的真正可執行映像
  4. 填寫程式的入口地址(如果有解釋器則填入解釋器的入口地址, 否則直接填入可執行程式的入口地址)
  5. create_elf_tables填寫目標文件的參數環境變數等必要信息
  6. start_kernel巨集準備進入新的程式入口

ELF文件中符號的動態解析過程

前面我們提到了內核空間中ELF文件的載入工作

內核的工作

  1. 內核首先讀取ELF文件頭部,再讀如各種數據結構,從這些數據結構中可知各段或節的地址及標識,然後調用mmap()把找到的可載入段的內容載入到記憶體中。同時讀取段標記,以標識該段在記憶體中是否可讀、可寫、可執行。其中,文本段是程式代碼,只讀且可執行,而數據段是可讀且可寫。
  2. 從PT_INTERP的段中找到所對應的動態鏈接器名稱,並載入動態鏈接器。通常是/lib/ld-linux.so.2.
  3. 內核把新進程的堆棧中設置一些標記對,以指示動態鏈接器的相關操作。
  4. 內核把控制權傳遞給動態鏈接器。

動態鏈接器的工作並不是在內核空間完成的, 而是在用戶空間完成的, 比如C語言程式則交給C運行時庫來完成, 這個並不是我們今天內核學習的重點, 而是由glic完成的,但是其一般過程如下

動態鏈接器的工作

  1. 動態鏈接器檢查程式對共用庫的依賴性,併在需要時對其進行載入。
  2. 動態鏈接器對程式的外部引用進行重定位,並告訴程式其引用的外部變數/函數的地址,此地址位於共用庫被載入在記憶體的區間內。動態鏈接還有一個延遲定位的特性,即只有在“真正”需要引用符號時才重定位,這對提高程式運行效率有極大幫助。
  3. 動態鏈接器執行在ELF文件中標記為.init的節的代碼,進行程式運行的初始化。
    動態鏈接器把控制傳遞給程式,從ELF文件頭部中定義的程式進入點(main)開始執行。在a.out格式和ELF格式中,程式進入點的值是顯式存在的,而在COFF格式中則是由規範隱含定義。
  4. 程式開始執行

具體的信息可以參照

Intel平臺下Linux中ELF文件動態鏈接的載入、解析及實例分析(一): 載入

Intel平臺下linux中ELF文件動態鏈接的載入、解析及實例分析(二): 函數解析與卸載

附錄(load_elf_binary函數註釋)

static int load_elf_binary(struct linux_binprm *bprm)
{   

    struct file *interpreter = NULL; /* to shut gcc up */
    unsigned long load_addr = 0, load_bias = 0;
    int load_addr_set = 0;
    char * elf_interpreter = NULL;
    unsigned long error;
    struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
    unsigned long elf_bss, elf_brk;
    int retval, i;
    unsigned long elf_entry;
    unsigned long interp_load_addr = 0;
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long reloc_func_desc __maybe_unused = 0;
    int executable_stack = EXSTACK_DEFAULT;

    /*  從寄存器重獲取參數信息  */
    struct pt_regs *regs = current_pt_regs();
    struct {
        struct elfhdr elf_ex;
        struct elfhdr interp_elf_ex;
    } *loc;
    struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;

    loc = kmalloc(sizeof(*loc), GFP_KERNEL);
    if (!loc) {
        retval = -ENOMEM;
        goto out_ret;
    }
    /*  1  填充並且檢查ELF頭部  */
    /* Get the exec-header
       1.1   填充ELF頭信息
       在load_elf_binary之前
       內核已經使用映像文件的前128個位元組對bprm->buf進行了填充, 
       這裡使用這此信息填充映像的文件頭
     */
    loc->elf_ex = *((struct elfhdr *)bprm->buf);

    retval = -ENOEXEC;
    /* 
        1.2 First of all, some simple consistency checks 
       比較文件頭的前四個位元組,查看是否是ELF文件類型定義的"\177ELF"*/
    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
        goto out;
    /*  
        1.3 除前4個字元以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共用庫
    */
    if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
        goto out;

    /*  1.4 檢查特定的目標機器標識  */
    if (!elf_check_arch(&loc->elf_ex))
        goto out;
    if (!bprm->file->f_op->mmap)
        goto out;

    /* 
        2.   load_elf_phdrs 載入程式頭表
        load_elf_phdrs函數就是通過kernel_read讀入整個program header table
        從函數代碼中可以看到,一個可執行程式必須至少有一個段(segment),
        而所有段的大小之和不能超過64K。
    */
    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
    if (!elf_phdata)
        goto out;

    /*  bss段,brk段先初始化為0  */
    elf_ppnt = elf_phdata;
    elf_bss = 0;
    elf_brk = 0;

    /*  code代碼段 */
    start_code = ~0UL;
    end_code = 0;

    /*  data數據段 */
    start_data = 0;
    end_data = 0;
    /*
        3.   尋找和處理解釋器段
     這個for迴圈的目的在於尋找和處理目標映像的"解釋器"段。
     "解釋器"段的類型為PT_INTERP,
     找到後就根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區。
     "解釋器"段實際上只是一個字元串,
     即解釋器的文件名,如"/lib/ld-linux.so.2"。
     有瞭解釋器的文件名以後,就通過open_exec()打開這個文件,
    再通過kernel_read()讀入其開關128個位元組,即解釋器映像的頭部。*/
    for (i = 0; 
         i < loc->elf_ex.e_phnum;/*  e_phnumc存儲了程式頭表的數目*/
         i++) { 

        /*  3.1 解釋器"段的類型為PT_INTERP  */
        if (elf_ppnt->p_type == PT_INTERP) {
            /* This is the program interpreter used for
             * shared libraries - for now assume that this
             * is an a.out format binary
             */
            retval = -ENOEXEC;
            if (elf_ppnt->p_filesz > PATH_MAX || 
                elf_ppnt->p_filesz < 2)
                goto out_free_ph;

            retval = -ENOMEM;

            /* 為動態連接器分配空間並讀取載入 */
            elf_interpreter = kmalloc(elf_ppnt->p_filesz,
                          GFP_KERNEL);
            if (!elf_interpreter)
                goto out_free_ph;

            /*  3.2 根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區  */
            retval = kernel_read(bprm->file, elf_ppnt->p_offset,
                         elf_interpreter,
                         elf_ppnt->p_filesz);
            if (retval != elf_ppnt->p_filesz) {
                if (retval >= 0)
                    retval = -EIO;
                goto out_free_interp;
            }
            /* make sure path is NULL terminated */
            retval = -ENOEXEC;
            if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
                goto out_free_interp;
            /*  3.3 通過open_exec()打開解釋器文件
                內核把新進程的堆棧中設置一些標記對,
                以指示動態鏈接器的相關操作,詳見open_exec實現 */
            interpreter = open_exec(elf_interpreter);
            retval = PTR_ERR(interpreter);
            if (IS_ERR(interpreter))
                goto out_free_interp;

            /*
             * If the binary is not readable then enforce
             * mm->dumpable = 0 regardless of the interpreter's
             * permissions.
             */
            would_dump(bprm, interpreter);

            /* Get the exec headers 
               3.4  通過kernel_read()讀入解釋器的前128個位元組,即解釋器映像的頭部。*/
            retval = kernel_read(interpreter, 0,
                         (void *)&loc->interp_elf_ex,
                         sizeof(loc->interp_elf_ex));
            if (retval != sizeof(loc->interp_elf_ex)) {
                if (retval >= 0)
                    retval = -EIO;
                goto out_free_dentry;
            }

            break;
        }

        /* 迴圈檢查所有的程式頭看是否有動態連接器 */
        elf_ppnt++;
    }


    elf_ppnt = elf_phdata;
    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
        switch (elf_ppnt->p_type) {
        case PT_GNU_STACK:
            if (elf_ppnt->p_flags & PF_X)
                executable_stack = EXSTACK_ENABLE_X;
            else
                executable_stack = EXSTACK_DISABLE_X;
            break;

        case PT_LOPROC ... PT_HIPROC:
            retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,
                          bprm->file, false,
                          &arch_state);
            if (retval)
                goto out_free_dentry;
            break;
        }

    /*   4.    檢查並讀取解釋器的程式表頭 */

    /* Some simple consistency checks for the interpreter 
       4.1  檢查解釋器頭的信息  */
    /* 檢查是否由動態連接器,無論是否有動態連接器都會執行elf文件 */
    if (elf_interpreter) {
        retval = -ELIBBAD;
        /* Not an ELF interpreter */
        if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
            goto out_free_dentry;
        /* Verify the interpreter has a valid arch */
        if (!elf_check_arch(&loc->interp_elf_ex))
            goto out_free_dentry;

        /* Load the interpreter program headers
           4.2  讀入解釋器的程式頭
         */
        interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
                           interpreter);
        if (!interp_elf_phdata)
            goto out_free_dentry;

        /* Pass PT_LOPROC..PT_HIPROC headers to arch code */
        elf_ppnt = interp_elf_phdata;
        for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
            switch (elf_ppnt->p_type) {
            case PT_LOPROC ... PT_HIPROC:
                retval = arch_elf_pt_proc(&loc->interp_elf_ex,
                              elf_ppnt, interpreter,
                              true, &arch_state);
                if (retval)
                    goto out_free_dentry;
                break;
            }
    }

    /*
     * Allow arch code to reject the ELF at this point, whilst it's
     * still possible to return an error to the code that invoked
     * the exec syscall.
     */
    retval = arch_check_elf(&loc->elf_ex,
                !!interpreter, &loc->interp_elf_ex,
                &arch_state);
    if (retval)
        goto out_free_dentry;

    /*  Flush all traces of the currently running executable
        在此清除掉了父進程的所有相關代碼 */
    retval = flush_old_exec(bprm);
    if (retval)
        goto out_free_dentry;

    /* Do this immediately, since STACK_TOP as used in setup_arg_pages
       may depend on the personality.  */
    /* 設置elf可執行文件的特性 */
    SET_PERSONALITY2(loc->elf_ex, &arch_state);
    if (elf_read_implies_exec(loc->elf_ex, executable_stack))
        current->personality |= READ_IMPLIES_EXEC;

    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
        current->flags |= PF_RANDOMIZE;

    setup_new_exec(bprm);

    /* Do this so that we can load the interpreter, if need be.  We will
       change some of these later
    為下麵的動態連接器執行獲取內核空間page */
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);
    if (retval < 0)
        goto out_free_dentry;

    current->mm->start_stack = bprm->p;

    /* Now we do a little grungy work by mmapping the ELF image into
       the correct location in memory.
       5  裝入目標程式的段segment 
       這段代碼從目標映像的程式頭中搜索類型為PT_LOAD的段(Segment)。在二進位映像中,只有類型為PT_LOAD的段才是需要裝入的。

       當然在裝入之前,需要確定裝入的地址,只要考慮的就是頁面對齊,還有該段的p_vaddr域的值(上面省略這部分內容)。

       確定了裝入地址後,就通過elf_map()建立用戶空間虛擬地址空間與目標映像文件中某個連續區間之間的映射,其返回值就是實際映射的起始地址。
    */

    /* 按照先前獲取的程式頭表,迴圈將所有的可執行文件載入到記憶體中 */

    for(i = 0, elf_ppnt = elf_phdata;
        i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
        int elf_prot = 0, elf_flags;
        unsigned long k, vaddr;
        unsigned long total_size = 0;
        /*  5.1   搜索PT_LOAD的段, 這個是需要裝入的 */
        if (elf_ppnt->p_type != PT_LOAD)
            continue;

        if (unlikely (elf_brk > elf_bss)) {
            unsigned long nbyte;
            /* 5.2  檢查地址和頁面的信息  */
            /* There was a PT_LOAD segment with p_memsz > p_filesz
               before this one. Map anonymous pages, if needed,
               and clear the area.  */
            retval = set_brk(elf_bss + load_bias,
                     elf_brk + load_bias);
            if (retval)
                goto out_free_dentry;
            nbyte = ELF_PAGEOFFSET(elf_bss);
            if (nbyte) {
                nbyte = ELF_MIN_ALIGN - nbyte;
                if (nbyte > elf_brk - elf_bss)
                    nbyte = elf_brk - elf_bss;
                if (clear_user((void __user *)elf_bss +
                            load_bias, nbyte)) {
                    /*
                     * This bss-zeroing can fail if the ELF
                     * file specifies odd protections. So
                     * we don't check the return value
                     */
                }
            }
        }

        if (elf_ppnt->p_flags & PF_R)
            elf_prot |= PROT_READ;
        if (elf_ppnt->p_flags & PF_W)
            elf_prot |= PROT_WRITE;
        if (elf_ppnt->p_flags & PF_X)
            elf_prot |= PROT_EXEC;

        elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;

        vaddr = elf_ppnt->p_vaddr;
        if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
            elf_flags |= MAP_FIXED;
        } else if (loc->elf_ex.e_type == ET_DYN) {
            /* Try and get dynamic programs out of the way of the
             * default mmap base, as well as whatever program they
             * might try to exec.  This is because the brk will
             * follow the loader, and is not movable.  */
            load_bias = ELF_ET_DYN_BASE - vaddr;
            if (current->flags & PF_RANDOMIZE)
                load_bias += arch_mmap_rnd();
            load_bias = ELF_PAGESTART(load_bias);
            total_size = total_mapping_size(elf_phdata,
                            loc->elf_ex.e_phnum);
            if (!total_size) {
                retval = -EINVAL;
                goto out_free_dentry;
            }
        }

        /*  5.3  虛擬地址空間與目標映像文件的映射
         確定了裝入地址後,
         就通過elf_map()建立用戶空間虛擬地址空間
         與目標映像文件中某個連續區間之間的映射,
         其返回值就是實際映射的起始地址 */
        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                elf_prot, elf_flags, total_size);
        if (BAD_ADDR(error)) {
            retval = IS_ERR((void *)error) ?
                PTR_ERR((void*)error) : -EINVAL;
            goto out_free_dentry;
        }

        if (!load_addr_set) {
            load_addr_set = 1;
            load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
            if (loc->elf_ex.e_type == ET_DYN) {
                load_bias += error -
                         ELF_PAGESTART(load_bias + vaddr);
                load_addr += load_bias;
                reloc_func_desc = load_bias;
            }
        }
        k = elf_ppnt->p_vaddr;
        if (k < start_code)
            start_code = k;
        if (start_data < k)
            start_data = k;

        /*
         * Check to see if the section's size will overflow the
         * allowed task size. Note that p_filesz must always be
         * <= p_memsz so it is only necessary to check p_memsz.
         */
        if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
            elf_ppnt->p_memsz > TASK_SIZE ||
            TASK_SIZE - elf_ppnt->p_memsz < k) {
            /* set_brk can never work. Avoid overflows. */
            retval = -EINVAL;
            goto out_free_dentry;
        }

        k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

        if (k > elf_bss)
            elf_bss = k;
        if ((elf_ppnt->p_flags & PF_X) && end_code < k)
            end_code = k;
        if (end_data < k)
            end_data = k;
        k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
        if (k > elf_brk)
            elf_brk = k;
    }

    /* 更新讀入記憶體中相關信息的記錄 */
    loc->elf_ex.e_entry += load_bias;
    elf_bss += load_bias;
    elf_brk += load_bias;
    start_code += load_bias;
    end_code += load_bias;
    start_data += load_bias;
    end_data += load_bias;

    /* Calling set_brk effectively mmaps the pages that we need
     * for the bss and break sections.  We must do this before
     * mapping in the interpreter, to make sure it doesn't wind
     * up getting placed where the bss needs to go.
     */
    /* 使用set_brk調整bss段的大小 */
    retval = set_brk(elf_bss, elf_brk);
    if (retval)
        goto out_free_dentry;
    if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
        retval = -EFAULT; /* Nobody gets to see this, but.. */
        goto out_free_dentry;
    }
    /*
     6  填寫程式的入口地址

     這段程式的邏輯非常簡單:

     如果需要裝入解釋器,就通過load_elf_interp裝入其映像, 
     並把將來進入用戶空間的入口地址設置成load_elf_interp()的返回值,
     即解釋器映像的入口地址。

     而若不裝入解釋器,那麼這個入口地址就是目標映像本身的入口地址。
     */
    if (elf_interpreter) {
    /*  存在動態鏈接器
        內核把控制權傳遞給動態鏈接器。
        動態鏈接器檢查程式對共用庫的依賴性,
        併在需要時對其進行載入,由load_elf_interp完成 
        unsigned long interp_map_addr = 0;

        elf_entry = load_elf_interp(&loc->interp_elf_ex,
                        interpreter,
                        &interp_map_addr,
                        load_bias, interp_elf_phdata);
        if (!IS_ERR((void *)elf_entry)) {
            /*
             * load_elf_interp() returns relocation
             * adjustment
             */
            interp_load_addr = elf_entry;
            elf_entry += loc->interp_elf_ex.e_entry;
        }
        if (BAD_ADDR(elf_entry)) {
            retval = IS_ERR((void *)elf_entry) ?
                    (int)elf_entry : -EINVAL;
            goto out_free_dentry;
        }
        reloc_func_desc = interp_load_addr;

        allow_write_access(interpreter);
        fput(interpreter);
        kfree(elf_interpreter);
    } else {
        elf_entry = loc->elf_ex.e_entry;
        if (BAD_ADDR(elf_entry)) {
            retval = -EINVAL;
            goto out_free_dentry;
        }
    }

    kfree(interp_elf_phdata);
    kfree(elf_phdata);

    set_binfmt(&elf_format);

#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
    retval = arch_setup_additional_pages(bprm, !!elf_interpreter);
    if (retval < 0)
        goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */

    /*   7  create_elf_tables填寫目標文件的參數環境變數等必要信息
    在完成裝入,啟動用戶空間的映像運行之前,還需要為目標映像和解釋器準備好一些有關的信息,這些信息包括常規的argc、envc等等,還有一些"輔助向量(Auxiliary Vector)"。
    這些信息需要複製到用戶空間,使它們在CPU進入解釋器或目標映像的程式入口時出現在用戶空間堆棧上。這裡的create_elf_tables()就起著這個作用。
    */
    install_exec_creds(bprm);
    /* 在記憶體中生成elf映射表 */
    retval = create_elf_tables(bprm, &loc->elf_ex,
              load_addr, interp_load_addr);
    if (retval < 0)
        goto out;
    /*  N.B. passed_fileno might not be initialized? 
        調整記憶體映射內容 */
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;

    if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
        current->mm->brk = current->mm->start_brk =
            arch_randomize_brk(current->mm);
#ifdef compat_brk_randomized
        current->brk_randomized = 1;
#endif
    }

    if (current->personality & MMAP_PAGE_ZERO) {
        /* Why this, you ask???  Well SVr4 maps page 0 as read-only,
           and some applications "depend" upon this behavior.
           Since we do not have the power to recompile these, we
           emulate the SVr4 behavior. Sigh. */
        error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
                MAP_FIXED | MAP_PRIVATE, 0);
    }

#ifdef ELF_PLAT_INIT
    /*
     * The ABI may specify that certain registers be set up in special
     * ways (on i386 %edx is the address of a DT_FINI function, for
     * example.  In addition, it may also specify (eg, PowerPC64 ELF)
     * that the e_entry field is the address of the function descriptor
     * for the startup routine, rather than the address of the startup
     * routine itself.  This macro performs whatever initialization to
     * the regs structure is required as well as any relocations to the
     * function descriptor entries when executing dynamically links apps.
     */
    ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
    /*
     8  最後,start_thread()這個巨集操作會將eip和esp改成新的地址,就使得CPU在返回用戶空間時就進入新的程式入口。如果存在解釋器映像,那麼這就是解釋器映像的程式入口,否則就是目標映像的程式入口。那麼什麼情況下有解釋器映像存在,什麼情況下沒有呢?如果目標映像與各種庫的鏈接是靜態鏈接,因而無需依靠共用庫、即動態鏈接庫,那就不需要解釋器映像;否則就一定要有解釋器映像存在。
       對於一個目標程式, gcc在編譯時,除非顯示的使用static標簽,否則所有程式的鏈接都是動態鏈接的,也就是說需要解釋器。由此可見,我們的程式在被內核載入到記憶體,內核跳到用戶空間後並不是執行我們程式的,而是先把控制權交到用戶空間的解釋器,由解釋器載入運行用戶程式所需要的動態庫(比如libc等等),然後控制權才會轉移到用戶程式。
       */
    /* 開始執行程式,這時已經是子進程了 */
    start_thread(regs, elf_entry, bprm->p);
    retval = 0;
out:
    kfree(loc);
out_ret:
    return retval;

    /* error cleanup */
out_free_dentry:
    kfree(interp_elf_phdata);
    allow_write_access(interpreter);
    if (interpreter)
        fput(interpreter);
out_free_interp:
    kfree(elf_interpreter);
out_free_ph:
    kfree(elf_phdata);
    goto out;
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 項目需求 asp.net core 讀取log目錄下的.log文件,.log文件的內容如下: xxx.log begin 寫入時間:2018-09-11 17:01:48 userid=1000 golds=10 end 一個 begin end 為一組,同一個.log文件里 userid 相同的, ...
  • 單元測試能夠幫助開發人員確保所開發的模塊、類以及類中的方法等的正確性,在項目開發過程中,及時進行單元測試能夠避免不必要的BUG以及提高測試效率。 在本文中,我們會分別來學習如何使用MSTest、xUnit以及NUnit這些流行的.NET測試框架來對.NET Core項目進行測試。 一、項目創建 首先 ...
  • 一、完全公平調度演算法 完全公平調度 CFS 的出發點基於一個簡單的理念:進程調度的效果應該如同系統具備一個理想中的完美多任務處理器。在這種系統中,每個進程能夠獲得 1/n 的處理器時間(n 為可運行進程數)。同時,我們可以調度給它們無限小的時間周期,所以,在任何可測量周期內,我們給予 n 個進程中每 ...
  • 在工作中,我們往往需要遠程伺服器,經常會遇到以下這兩個麻煩事。 一、遠程桌面的連接數限制,超出系統就會提示超過連接數。 二、遠程桌面連接時,同一個用戶不能同時遠程2個桌面連接。 為瞭解決這兩個麻煩事情,我們只需要配置Server 2008 R2的遠程桌面授權服務即可。實驗操作配置如下: 此次實驗的前 ...
  • 關於什麼是服務掃描不多介紹,通俗來看: 我已經掃描到目標機器某個埠開放,接下來我需要知道開放這個埠的是什麼應用 情景: 我的Kali機器IP地址:192.168.22.130 我要掃描的Metasploitable機器IP地址:192.168.22.129 1.先介紹一個小工具:不強大,但是可以 ...
  • 虛擬機Linux與本地虛擬網卡配置 NAT鏈接方式 **********這是我親自嘗試多次實踐出來的結果,不是複製粘貼************************* 首先進行初始化,這樣避免有些設置會有影響 第二步,如圖中1,取消本地DHCP服務,2、這裡的子網ip要與虛擬機內部的網段相同,最後 ...
  • VPN英文全稱是“Virtual Private Network”,就是“虛擬專用網路”。可以遠程幫助用戶、分公司、商業伙伴及供應商同公司的內部網建立可信的安全連接,用於經濟有效地連接到商業伙伴和用戶的安全外聯網虛擬專用網。 搭建環境: 伺服器系統:Windows server 2008 R2 客戶 ...
  • 一.概述 在linux中,很多程式和腳本都通過環境變數來獲取系統信息,存儲臨時數據,配置信息。環境變數是指用來存儲有關shell會話和工作環境信息,允許你在記憶體中存儲數據,以便程式或shell中運行的腳本能夠輕鬆訪問到它們。也是存儲持久數據的一種簡便方法。在bash shell中,環境變數分為:全局 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...