Linux內核如何裝載和啓動一個可執行程序

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


    學習過程其實就是模仿老師的過程,萬一足夠熟練了,就變成自己的了。內核代碼部分的確有些痛苦,好在本週回到了用戶shell層面,畢竟有些瞭解。將整個學習過程記錄如下,也是自己的成長經歷。

    

一、可執行文件的生成過程。我們說的可執行文件是給cpu執行的二進制代碼,那它又是我們人所編輯的。這個過程我就簡單點,有下面的shell命令說明一下。結果如圖。

  245  vi hello.c
  246  gcc -E -o hello.cpp hello.c -m32
  247  gcc -x cpp-output -S -o hello.s hello.cpp -m32
  248  gcc -x assembler -c hello.s -o hello.o -m32
  249  gcc -0 hello hello.o -m32
  250  gcc -o hello hello.o -m32
  251  gcc -o hello.static hello.o -m32 -static

wKiom1UvrPOAmTUnAAHur_bXTzc580.jpg

我們在windows上的可執行文件是PE文件。在linux下是elf格式。

文本編輯器編輯源代碼文件->預編譯處理->彙編成.s文件->編譯生成目標.o文件->鏈接程序將目標文件連接成可執行文件。


二、對於靜態鏈接的elf文件,基本上在加載時對應加上程序入口地址,將相應的代碼數據加載到對應的內存空間中,然後逐步執行代碼。以下是我的ELF Header情況。

wKioL1UvtEKj9qreAANbzIAsmK0653.jpg

我用gdb跟一下看看執行時的代碼入口地址。由於實際上我們的main函數是由_start調用的,所以我們設置斷點 break _start。結果如下:

wKiom1UvtlfwSy6zAAHP1T1CPl4233.jpg


三、通常的linux下的可執行程序都是在shell下執行的,所以會進行相應的執行前處理過程。比如/bin/ls -l這個命令,就是先fork出一個進程,調用execve系統調用,然後再調用具體的execlp調用將相關的命令行參數傳遞給程序的main函數的。


四、通常我們的程序還需要使用動態鏈接庫。分爲裝載時動態鏈接和運行時動態鏈接。演示代碼

    通過這樣的命令生成共享庫:

gcc -shared shlibexample.c -o libshlibexample.so -m32

    以下命令生成運行時鏈接庫:

gcc -shared dllibexample.c -o libdllibexample.so -m32

生成文件及運行情況如下:

wKiom1UvwPXiBp1xAASMH3nWbE8396.jpg

五、execve系統調用和fork系統調用一樣,都是特殊的系統調用。fork系統調用返回兩次,一次返回父進程執行,一回返回特定的點ret_from_fork執行,然後返回到用戶態。execve系統調用在內核中將當前的可執行程序覆蓋掉了,當返回時不再是原來的可執行程序了。

Shell會調用execve將命令行參數和環境參數傳遞給可執行程序的main函數:
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
而所有的庫函數exec*都是execve的封裝例程。

系統調用sys_execve會解析可執行文件格式do_execve->do_execve_common->exec_binprm然後執行search_binary_handler找到符合文件頭部標明的文件格式的解析模塊。對於linux下的ELF文件,fmt->load_binary(bprm)實際執行的就是static int load_elf_binary(struct linux_binprm *bprm)。

執行start_thread(regs, elf_entry, bprm->p)時,如果是靜態鏈接的,elf_entry就是文件頭部標明的入口。進入內核後

start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    set_user_gs(regs, 0);
    regs->fs        = 0;
    regs->ds        = __USER_DS;
    regs->es        = __USER_DS;
    regs->ss        = __USER_DS;
    regs->cs        = __USER_CS;
    regs->ip        = new_ip;
    regs->sp        = new_sp;
    regs->flags        = X86_EFLAGS_IF;
    /*
     * force it to the iret return path by making it look as if there was
     * some work pending.
     */
    set_thread_flag(TIF_NOTIFY_RESUME);
}

execve在返回前用新的ip和sp更新了進程的ip和sp。對於需要動態鏈接的程序,elf_entry就會加載動態鏈接器ld的入口地址。


總結,在linux環境下,可執行文件是以ELF格式存在的,文件頭部標明瞭文件在加載到內存中需要的相關信息,隨後的部分是以段的形式存在的代碼和數據,段的劃分主要依據加載到內存中的讀寫屬性。系統調用execve負責可執行文件的調度工作,先進行相關參數的傳遞和調用前環境的處理,然後加載可執行文件的信息,查找相應的可執行文件解析模塊,對於ELF格式的可執行文件,按照格式要求加載到內存中相應的地址空間,如果是靜態鏈接的就將文件頭部標明的入口地址作爲開始;如果是依賴動態鏈接庫的可執行文件則需要將動態鏈接器ld的入口地址作爲開始。

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