C程序運行的背後(2)

話說上回說到,C程序運行以前,必需要加載到其進程地址空間中。今兒咱就扯扯這個加載究竟是怎麼加載的。 一圖勝前言,這個圖簡單說明了可執行文件加載過程的邏輯流,在此只作粗粒度概要說明。須要準確描述的,請出門左轉,看源碼去吧。
html

1.  程序老是運行在進程上下文(context)中的,當輸入./memlayout時,shell會建立一個子進程。除每一個進程獨有的專屬信息外,子進程會繼承父進程的大部分資源,如環境變量、進程空間映像等。也就是說,若是不重置子進程的內容,子進程會運行與父進程同樣的程序。爲了讓子進程能夠運行別的程序,就要經過execve這個系統調用來指定。linux

int   execve( char *pathname, char *argv[], char *envp[] )
int   do_execve( char *pathname, char *argv[], char *envp[] ,struct pt_regs *regs ) // pathname -> 可執行文件名指針 // argv -> 參數指針 // envp -> 環境變量指針 // pt_regs -> 用來保存切換到內核前用戶空間的寄存器值

就像春風秋雨同樣,本來一切都是那麼天然,天然到你忍不住向女神表白,而後女神跟你說「你是個好人」。當execvedo_execve說我要你時,卻憑空多出了一個變量struct pt_regs,這毫不不能夠說不任性!這是由於,do_execve中的參數命令行參數指針、環境變量指針,會被內核用來設置子進程的用戶棧。而根據系統調用約定(calling conventions),Linux和Unix在系統調用時有一個不一樣,那就是Linux是用寄存器來傳遞系統調用參數的,而Unix是經過棧。因此在切換到內核時,就有必要保存傳遞到寄存器中的參數。而pt_regs這個結構體就是用來保存CPU的寄存器狀態的。原來仍是那麼天然,只是女神已成路人。shell

// include/asm-i386/ptrace.h
 struct pt_regs { long ebx;    // pathname
    long ecx;     // argv
    long edx;     // envp
    long esi; long edi; long eax; long eip; …… }

2.  那麼do_execve要大發神威了吧?切莫急先。物理學告訴咱們,力都是有一個做用對象的,就比如咱們追的都是女神。而do_execve的操做對象是一個叫struct linux_binprm的結構體,它用來保存要執行的文件的相關信息。do_execve會調用load_binprm,將須要的可執行文件信息都加載到linux_binprm中,包括可執行文件的ELF頭信息、路徑名、參數字符串、環境變量字符串等等。(注意:load_binprm並非一個真正意義上的函數,爲了方便理解,用它來歸納表示由do_execve完成的填充任務。)app

struct linux_binprm {   char buf[BINPRM_BUF_SIZE]; //保存可執行文件的頭128字節
  struct page *page[MAX_ARG_PAGES]; // 保存參數、環境變量    struct mm_struct *mm;   unsigned long p;    //當前內存頁最高地址
  int sh_bang;   struct file * file;     //要執行的文件
  int e_uid, e_gid;    //要執行的進程的有效用戶ID和有效組ID 
  kernel_cap_t cap_inheritable, cap_permitted, cap_effective;   void *security;   int argc, envc;     //命令行參數和環境變量數目
  char * filename;    //要執行的文件的名稱
  char * interp;      //要執行的文件的真實名稱,一般和filename相同 
  unsigned interp_flags;   unsigned interp_data;   unsigned long loader, exec; 
}

接下來,do_execve調用search_binary_handler()來查找可執行文件內容的處理程序。對於ELF格式的文件,則調用相應的load_elf_binary(),若是是a.out格式,則調用load_aout_binary()。在根據可執行文件中的section信息並將它們加載到進程空間前,load_elf_binary會首先將進程空間清空,而後將可執行文件映像加載到進程空間中。另外,load_elf_binary還會設置好用戶棧:xss

調用setup_arg_pages,將linux_binprm.page中的參數、環境變量等字符串映射到用戶棧中;ide

調用create_elf_tables,將argc、argv、envp以及一些的「輔助向量(auxiliary vector)」壓入到用戶棧中。函數

輔助向量,是內核向用戶空間的應用程序傳遞信息的機制之一,主要供動態連接器(ld-linux.so)使用。ui

以前的圖是從《深刻理解計算機系統》拷過來的,並不詳細,因而從新畫了一個:spa

 argc下面的地址纔是棧真正開始的地方。命令行

 

3.  終於草原已經準備好了,能夠策馬奔騰啦。一切又回到load_elf_binary()中,不過接下來就是見證神奇的時候啦,由於load_elf_binary要調用start_thread啦。不要小瞧這個調用,它不亞於數學老師跟咱們說」我要變形了「的效果。

#define start_thread(regs, new_eip, new_esp) do {          \ __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ set_fs(USER_DS); \ regs->xds = __USER_DS; \ regs->xes = __USER_DS; \ regs->xss = __USER_DS; \ regs->xcs = __USER_CS; \ regs->eip = new_eip; \ regs->esp = new_esp; \ } while (0)

start_thread的實際調用是這樣的start_thread(regs,elf_entry, bprm->p),其中__USER_*是前面提到的進程切換到內核前的寄存器值;elf_entry就是可執行文件的入口點,也就是C啓動代碼的入口位置,賦給eip;bprm->p就是用戶棧的棧頂位置,賦給esp。接下來就會跳轉到eip指向的C啓動代碼起始位置開始運行。

 

至於從C啓動代碼到main的距離,咱之後再表。

 

參考:

1 do_execve的具體內容:http://wenku.baidu.com/view/e97820ee4afe04a1b071de32.html

2 ELF文件加載詳細流程:http://www.longene.org/techdoc/0328130001224576708.html

3 輔助向量:http://www.tuicool.com/articles/MNRJVj

相關文章
相關標籤/搜索