Linux內核分析(七)

Linux 內核分析——【實驗七:如何裝載和啓動一個可執行程序】
一 什麼是可執行文件(程序)
在windows環境下,我們都知道只要雙擊一個.exe的文件就可以執行一個程序,這個以.exe結尾的文件就是一個可執行文件。在andriod系統下,一個.apk的文件就是一個可執行文件,那麼在linux系統下,可執行文件是怎麼樣的呢?實際上,可執行文件在linux環境下並沒有什麼特殊的後綴標記,只是在生成該文件時,它的屬性設置了可執行(就是‘x’),那麼他就是屬於可執行文件。

二 可執行文件的格式
linux系統中,可執行文件的格式爲elf(Executable and Linking Format)格式。
1 ELF文件有三種類型:
(1)可重定位文件
也就是通常稱的目標文件,後綴爲.o。鏈接器將它作爲輸入,經鏈接處理後,生成一個可執行的對象文件 (Executable file) 或者一個可被共享的對象文件。
(2)共享文件
這些就是所謂的動態庫文件,也即 .so 文件。如果拿前面的靜態庫來生成可執行程序,那每個生成的可執行程序中都會有一份庫代碼的拷貝。如果在磁盤中存儲這些可執行程序,那就會佔用額外的磁盤空間;另外如果拿它們放到Linux系統上一起運行,也會浪費掉寶貴的物理內存。如果將靜態庫換成動態庫,那麼這些問題都不會出現。
(3)可執行文件

2 elf 文件的格式
7-1
爲什麼會有兩種不同的格式呢?
(1) Linking View: 組成不同的可重定位文件,以參與可執行文件或者可被共享的對象文件的鏈接構建;
(2) Execution View: 組成可執行文件或者可被共享的對象文件,以在運行時內存中進程映像的構建。

我們從Execution View進行分析:
(1) ELF頭部結構Elf32_Ehdr

typedef struct
{
    unsigned char e_ident[EI_NIDENT];     /* 魔數和相關信息 */
    Elf32_Half    e_type;                 /* 目標文件類型 */
    Elf32_Half    e_machine;              /* 硬件體系 */
    Elf32_Word    e_version;              /* 目標文件版本 */
    Elf32_Addr    e_entry;                /* 程序進入點 */
    Elf32_Off     e_phoff;                /* 程序頭部偏移量 */
    Elf32_Off     e_shoff;                /* 節頭部偏移量 */
    Elf32_Word    e_flags;                /* 處理器特定標誌 */
    Elf32_Half    e_ehsize;               /* ELF頭部長度 */
    Elf32_Half    e_phentsize;            /* 程序頭部中一個條目的長度 */
    Elf32_Half    e_phnum;                /* 程序頭部條目個數  */
    Elf32_Half    e_shentsize;            /* 節頭部中一個條目的長度 */
    Elf32_Half    e_shnum;                /* 節頭部條目個數 */
    Elf32_Half    e_shstrndx;             /* 節頭部字符表索引 */
} Elf32_Ehdr;

e_ident[0]-e_ident[3]包含了文件的魔數 依次是 0x7f, 'E', 'L', 'F'
e_ident[4] 表示硬件的位數 1表示32位, 2表示64位
e_ident[5] 表示數據編碼方式

下面是ELF頭部結構中對應的數據類型。
7-2

用readelf 可以看可執行文件的ELF信息

~$ readelf -h  hello   #查看hello文件的頭部結構

7-3

(2) ELF頭的是程序表

typedef struct {
      Elf32_Word  p_type;     /* 段類型 */
      Elf32_Off   p_offset;   /* 段位置相對於文件開始處的偏移量 */
      Elf32_Addr  p_vaddr;    /* 段在內存中的地址 */
      Elf32_Addr  p_paddr;    /* 段的物理地址 */
      Elf32_Word  p_filesz;   /* 段在文件中的長度 */
      Elf32_Word  p_memsz;    /* 段在內存中的長度 */
      Elf32_Word  p_flags;    /* 段的標記 */
      Elf32_Word  p_align;    /* 段在內存中對齊標記 */
  } Elf32_Phdr;

用readelf 可以看ELF頭的是程序表信息

~$ readelf -l hello    #查看hello的程序表

7-4

注意:更多的readelf命令可以使用:

~$ readelf --help

三 使用exec*庫函數加載一個可執行程序
1 exec* 庫函數

#include <unistd.h>
int execl(const char *path, const char *arg, ...);  
int execlp(const char *file, const char *arg, ...);   
int execle(const char *path, const char *arg, ..., char *const envp[]); 
int execv(const char *path, char *const argv[]);  
int execvp(const char *file, char *const argv[]);  
int execve(const char *path, char *const argv[], char *const envp[]); 

其中,只有execve是真正意義上的系統調用,其它都是在此基礎上經過包裝的庫函數。exec函數族的作用是根據指定的文件名找到可執行文件,並用它來取代調用進程的內容,換句話說,就是在調用進程內部執行一個可執行文件。這裏的可執行文件既可以是二進制文件,也可以是任何Linux下可執行的腳本文件。
(1)函數名與參數的關係:
細看一下,這6個函數都是以exec開頭(表示屬於exec函數組),前3個函數接着字母l的,後3個接着字母v的,我的理解是l表示list(列舉參數),v表示vector(參數向量表)。
(2)區別
execv開頭的函數是以”char *argv[]”(vector)形式傳遞命令行參數,而execl開頭的函數採用了羅列(list)的方式,把參數一個一個列出來,然後以一個NULL表示結束。這裏的NULL的作用和argv數組裏的NULL作用是一樣的。
字母p是指在環境變量PATH的目錄裏去查找要執行的可執行文件。2個以p結尾的函數execlp和execvp,看起來,和execl與execv的差別很小,事實也如此,它們的區別從第一個參數名可以看出:除execlp和execvp之外的4個函數都要求,它們的第1個參數path必須是一個完整的路徑,如”/bin/ls”;而execlp和execvp 的第1個參數file可以僅僅只是一個文件名,如”ls”,這兩個函數可以自動到環境變量PATH指定的目錄裏去查找。
字母e是指給可執行文件指定環境變量。在全部6個函數中,只有execle和execve使用了char *envp[]傳遞環境變量,其它的4個函數都沒有這個參數,這並不意味着它們不傳遞環境變量,這4個函數將把默認的環境變量不做任何修改地傳給被執行的應用程序。而execle和execve用指定的環境變量去替代默認的那些。
(3)返回值
與一般情況不同,exec函數族的函數執行成功後不會返回,因爲調用進程的實體,包括代碼段,數據段和堆棧等都已經被新的內容取代,只有進程ID等一些表面上的信息仍保持原樣。調用失敗時,會設置errno並返回-1,然後從原程序的調用點接着往下執行。
(4)常見的錯誤
與其他系統調用比起來,exec很容易失敗,被執行文件的位置,權限等很多因素都能導致調用失敗。因此,使用exec函數族時,一定要加錯誤判斷語句。
a.找不到文件或路徑,此時errno被設置爲ENOENT;
b.數組argv和envp忘記用NULL結束,此時errno被設置爲EFAULT;
c.沒有對要執行文件的運行權限,此時errno被設置爲EACCES。

2 exec*()函數和fork()函數的區別
(1)fork
fork函數創建一個新的進程,這個進程是當前進程的一個拷貝:子進程和父進程使用相同的代碼段,子進程複製父進程的堆棧段和數據段。但是,他們屬於兩個進程,只不過執行的代碼一樣罷了。
(2)execve
execve()是對當前進程的替換,替換者爲一個指定的程序,其參數包括替換者文件名(filename)、參數列表(argv)以及環境變量(envp)。替換者的執行會中止當前進程,而且替換者處理其他任務,不必和父進程執行一樣的任務。

3 使用gdb跟蹤exec*函數的執行過程
(1)配置實驗環境(與實驗三相似)
a.下載文件menu
b.解壓,修改makefile文件(如下)
7-5
c. 運行make rootfs
d. 使用gdb調試:

qemu -kernel ../../Lab3/linux-3.18.6/arch/x86/boot/bzImage -initrd ./rootfs.img -s -S

(2)設置斷點,並運行
7-6
在QEMU模擬器中輸入以下命令

MenuOS>> exec

gdb中將停在斷點處,如下
7-7
(3)進行跟蹤
7-8
7-9
7-10
7-11
7-12
7-13
大致的運行流程如下:

// 文件路徑: linux-3.18.6/fs/exec.c
sys_execve(){ //系統調用execve
    do_execve(){
        do_execve_common(){
            exec_binprm(){
                search_binary_handler(){
                    load_elf_binary(){
                        start_thread(){

                        }
                    }
                }
            }
        }

    }

}

倒數第三張圖中,在load_elf_binary函數中,會設置程序靜態鏈接活動態鏈接的入口地址elf_entry,從最後兩張圖中,可以看到爲進程設置了新的ip(也就是elf_entry)和sp,之後返回用戶態就會從這裏設置的ip開始執行。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章