Linux中可執行程序的裝載和執行

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

1 可執行程序的由來

一個.c源文件是如何變爲一個可執行文件的?
大致的步驟如下(以linux下的gcc爲例):

  1. 預處理:gcc -E -o hello.cpp hello.c -m32,這一步主要是進行一些宏的替換,得到的仍然是一個文本文件
  2. 編譯:gcc -x cpp-ouput -S -o hello.s hello.cpp -m32,這一步會將c代碼翻譯成彙編語言
  3. 彙編:gcc -x assembler -c hello.s -o hello.o -m32,將編譯階段生成的彙編代碼(.S文件)轉變爲目標
    文件(.o),這一步得到的目標文件已經是ELF格式的二進制文件了,但還不能直接運行,需要和庫文件鏈接在
    一起纔可以運行,默認使用動態庫進行鏈接
  4. 鏈接:gcc -o hello-static hello.o -m32 -static,將多個目標文件(.o文件)鏈接成ELF格式的可執行文件

可以總結成下圖(省略了預編譯的過程):

可執行文件的生成過程

2 可執行文件的格式

爲了更好的瞭解可執行程序的裝載與執行,這裏我們需要了解一下可執行文件的格式。

在不同的操作系統中,可執行文件的格式可能會有不同,比如在window下,可執行文件的格式爲PE(裝過系統的同學對這個肯定不會陌生,全稱爲Portable Executable),而在linux下,則爲ELF(Executeable and linkable format) ,這裏我們重點討論ELF格式,上面提到過的.o目標文件和linux下的可執行文件都是屬於這種格式的。

我們可以在linux下的命令行中使用 readelf -h excuteableFile 來查看一個ELF文件的頭部信息:

ELF文件頭部信息

這裏我們使用的是靜態鏈接的方式編譯成的 hello 可執行文件,可以看到它的入口地址在 0x8048d0a。當運行一個可執行文件時,第一件事就是要將該文件加載到內存中去,從理論上來講,系統會將該文件拷貝到一個虛擬的內存空間段裏面去,在拷貝的過程中,可執行文件的格式和進程的地址空間存在如下的映射關係:

映射空間

上圖的右半部分即對應進程的虛擬地址空間,對於32爲x86的機器而言,這個虛擬地址空間大小爲4G,其中上面1G空間爲內核使用,用戶態不能訪問,只有下面的3G空間可以進行代碼和數據的存儲。

ELF可執行文件默認被加載到內存0x8048000這個位置,即從這個位置開始加載。先加載ELF可執行文件的頭部信息,再加載代碼部分,但因不同文件頭部大小不一樣,第一行代碼(即程序的實際入口地址)的位置也會有所不同,圖中程序的入口地址爲0x8048300。

3 shell程序啓動可執行程序的過程

在linux下,我們一般都是在shell的環境下來運行一個可執行文件,那麼它到底是如何實現的?下面有一個簡化版的“shell”,不過對於理解shell的工作原理足夠了:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid<0) 
    { 
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid==0) 
    {
        /*   child process   */
        execlp("/bin/ls","ls",NULL);
    } 
    else 
    {  
        /*     parent process  */
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!");
        exit(0);
    }
}

可以看到,在shell環境下運行一個可執行文件(比如 “ls”),實際上就是shell fork出來的子進程執行execve系統調用(ls 作爲參數傳遞),執行完系統調用後,shell的子進程就變爲了可執行程序 ls。

注意:一般的系統調用返回後,都是返回到原來的調用程序(調用程序 調用 系統調用),但是 execve和fork則是兩個“奇葩”,execve通過修改 執行int 0x80 指令時壓入的調用者的sp,ip信息等,來實現調用返回時,執行 ls 可執行程序,這與 莊周夢蝶的故事有點相似,莊周(調用execve的shell子進程)入睡(調用execve陷入內核),醒來(系統調用execve返回用戶態)發現自己是蝴蝶(被execve加載的ls可執行程序);至於 fork系統調用,可以參考我的前面幾篇文章。

那麼Shell是如何將 ls 所需要的參數和環境變量傳遞給它的呢?
結論:先是通過函數調用參數進行傳遞,再通過系統調用進行參數傳遞,具體的調用流程如下:
Shell程序 -> execve -> sys_execve,然後在初始化新程序堆棧時將參數和環境變量拷貝進去

4 sys_execve內核處理過程

現在讓我們深入內核,來看一看sys_execve的具體的實現過程。具體的調用順序如下所示:

這裏寫圖片描述

若要看具體的代碼實現,可以參看下面這篇文章:
http://www.jianshu.com/p/84d96a6385b0

5 實驗截圖

最後放上實驗的截圖:

這裏寫圖片描述

上圖中的紅色標註部分是menu程序中的 Makefile 文件需要修改的地方;另外,需要在 test.c 文件中新增 Exec 函數,並使用 MenuCongfig 進行命令的添加;還有一點要注意的是,要新建一個 hello.c 源文件。

實驗的運行結果如下圖:

這裏寫圖片描述

調試過程圖:

這裏寫圖片描述

上圖所示爲,先在 sys_execve 處打斷點,然後繼續運行

這裏寫圖片描述

上圖爲 在load_elf_binary 和 start_thread 出打斷點並運行

這裏寫圖片描述

上圖爲start_thread處的斷點

這裏寫圖片描述

上圖爲:對傳遞進 start_thread 的參數,new_ip ,也就是 上文提到過的 elf_entry 轉換爲16進制格式

這裏寫圖片描述

從上圖可以看到,待裝載的可執行文件 hello 的入口地址 0x8048d0a 和 上面調試程序中 new_ip 的值是一樣的,這也印證了我們的猜想。

參考文章:
http://www.jianshu.com/p/84d96a6385b0

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