程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> C程序運行的背後(2)

C程序運行的背後(2)

編輯:關於C
話說上回說到,C程序運行之前,必須要加載到其進程地址空間中。今兒咱就扯扯這個加載到底是怎麼加載的。 一圖勝前言,這個圖簡單說明了可執行文件加載過程的邏輯流,在此只做粗粒度概要說明。需要准確描述的,請出門左轉,看源碼去吧。       1.  程序總是運行在進程上下文(context)中的,當輸入./memlayout時,shell會創建一個子進程。除每個進程獨有的專屬信息外,子進程會繼承父進程的大部分資源,如環境變量、進程空間映像等。也就是說,如果不重置子進程的內容,子進程會運行與父進程一樣的程序。為了讓子進程可以運行別的程序,就要通過execve這個系統調用來指定。     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  -> 用來保存切換到內核前用戶空間的寄存器值   就像春風秋雨一樣,原本一切都是那麼自然,自然到你忍不住向女神表白,然後女神跟你說“你是個好人”。當execve向do_execve說我要你時,卻憑空多出了一個變量struct pt_regs,這絕不不可以說不任性!這是因為,do_execve中的參數命令行參數指針、環境變量指針,會被內核用來設置子進程的用戶棧。而根據系統調用約定(calling conventions),Linux和Unix在系統調用時有一個不同,那就是Linux是用寄存器來傳遞系統調用參數的,而Unix是通過棧。所以在切換到內核時,就有必要保存傳遞到寄存器中的參數。而pt_regs這個結構體就是用來保存CPU的寄存器狀態的。原來還是那麼自然,只是女神已成路人。     // 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完成的填充任務。)     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還會設置好用戶棧:   調用setup_arg_pages,將linux_binprm.page中的參數、環境變量等字符串映射到用戶棧中;   調用create_elf_tables,將argc、argv、envc、envp以及一些的“輔助向量(auxiliary vector)”壓入到用戶棧中。   此時的用戶棧大概是這個樣子的:         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啟動代碼起始位置開始運行。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved