進程的創建與可執行程序的加載

張軒 學號:SA*****232

實驗內容:

1.參考進程初探 編程實現fork(創建一個進程實體) -> exec(將ELF可執行文件內容加載到進程實體) -> running program
2.參照C代碼中嵌入彙編代碼示例及用匯編代碼使用系統調用time示例分析fork和exec系統調用在內核中的執行過程
3.注意task_struct進程控制塊,ELF文件格式與進程地址空間的聯繫,注意Exec系統調用返回到用戶態時EIP指向的位置。
4.動態鏈接庫在ELF文件格式中與進程地址空間中的表現形式

第一部分:fork() 和 exec()

1.使用fork()

  1. #include<sys/types.h>
  2. #include<unistd.h>
  3. pid_t fork(void);
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);

這個系統調用複製當前進程,在進程表中創建一個新的表項,新表項中的許多屬性與當前進程相同。但是新進程有自己的數據空間(堆和棧),環境和文件描述符。在父進程中的fork調用返回的是新的子進程的PID,而新進程返回的是0.程序代碼也靠這一點來區分父子進程。創建失敗返回-1.這邊在之前看到過有這麼一個解釋,相當與是一個鏈狀的進程序列,子進程沒有兒子了,所以0相當於指向爲空

以下爲示例

  1. #include<sys/types.h>
  2. #include<unistd.h>
  3. #include<stdlib.h>
  4. #include<stdio.h>
  5. int main()
  6. {
  7. pid_t pid;
  8. pid=fork();
  9. if(0==pid)
  10. {
  11. pid_t cpid=getpid();
  12. printf("this is the child thread,cpid=%d\n",cpid);
  13. }
  14. else if(pid>0)
  15. {
  16. pid_t ppid=getpid();
  17. printf("this is the parent thread,ppid=%d\n",ppid);
  18. }
  19. else
  20. printf("fork error\n");
  21. return 0;
  22. }
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>

int main()
{
    pid_t pid;
    pid=fork();
    if(0==pid)
    {
        pid_t cpid=getpid();
        printf("this is the child thread,cpid=%d\n",cpid);
    } 
    else if(pid>0)
    {
        pid_t ppid=getpid();
        printf("this is the parent thread,ppid=%d\n",ppid);
    } 
    else
        printf("fork error\n");
    return 0;
}


2.使用exec()

exec() 系列函數有一組相關的函數組成 ,exec函數可以把當前進程替換爲另一個新進程,新進程由path 或者file 參數指定。我們可以使用exec函數將程序的執行從一個程序切換到另一個程序。在新的程序啓動後,原來的程序就不再運行了。

  1. #include<unistd.h>
  2. char ** environ;
  3. int execl(constchar *path,constchar *arg0,...,(char *)0);
  4. int execlp(constchar *file,constchar *arg0,...,(char *)0);
  5. int execle(constchar *path,constchar *arg0,...,(char *)0,char *const envp[]);
  6. int execv(constchar *path,char *const argv[]);
  7. int execvp(constchar *file,char *const argv[]);
  8. int execve(constchar *path,char *const argv[],char *const envp[]);
#include<unistd.h>

char ** environ;

int execl(const char *path,const char *arg0,...,(char *)0);
int execlp(const char *file,const char *arg0,...,(char *)0);
int execle(const char *path,const char *arg0,...,(char *)0,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[]);
這些函數可以分爲兩大類。execl execlp execle 的參數個數可以變化,參數以一個空指針結束。execv execvp 的第二個參數是一個字符串數組。不管哪種情況,新程序在啓動時會把argv數組中給定的參數傳遞給main函數。

這些函數通常都是用execve實現的。我們來看下面的一個例子,在這個例子當中,直接指定了各個變量,並沒有從shell中讀入。

  1. #include<stdlib.h>
  2. #include<unistd.h>
  3. #include<stdio.h>
  4. int main(int argc,char *argv[],char *envp[])
  5. {
  6. pid_t pre_pid=getpid();
  7. printf("Before Running ps with execlp,pre_pid=%d\n",pre_pid);
  8. execlp("ps","ps","-l",0);
  9. pid_t after_pid=getpid();
  10. printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);
  11. exit(0);
  12. }
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>

int main(int argc,char *argv[],char *envp[])
{
    pid_t pre_pid=getpid();
    printf("Before Running ps with execlp,pre_pid=%d\n",pre_pid);
    execlp("ps","ps","-l",0);
    pid_t after_pid=getpid();
    printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);
    exit(0);
}


我們發現一個很有趣的現象,

printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);

並沒有被執行。這是由於exec函數取代了原先的進程,一般情況下,exec函數是不會返回的,除非發生錯誤。出現錯誤時,exec函數返回-1,並設置錯誤變量errno。

特別要注意的一點,在原進程中已打開的文件描述符在新進程中仍將保持打開,除非它們的執行時關閉標誌被置位。


第二部分:fork和exec系統調用在內核中的執行過程

對fork函數進行反彙編

彙編的時候要注意設置斷點

如下:

  1. gcc -g forktest.c -o forktest
  2. gdb forktest
  3. b fork
  4. r
  5. disas fork
gcc -g forktest.c -o forktest
gdb forktest
b fork
r
disas fork


fork()函數在系統調用中,會執行do_fork()函數,其關鍵步驟如下:
1)查找pidmap_array位圖,爲子進程獲取一個新的PID;
2)調用copy_process()函數,這個函數會將子進程的各個數據結構進行分配並初始化,它返回子進程描述符,即task_struct結構的指針;
3)返回並返回子進程的PID;
copy_process()函數的執行過程:
1)爲子進程分配一個task_struct結構,將其指針暫存在局部變量tsk中,並繼續分配一個thread_info結構,將其指針暫存在局部變量ti中;
2)將current進程描述符地址複製到tsk指向的task_struct結構,將ti描述父地址複製到tsk指向的thread_info結構,並將當前進程的thread_info結構內容拷貝到ti指向結構中;(子進程複製父進程的進程描述符以及thread_info描述符)
3)用子進程PID更新task_struct中的pid字段;
4)分別創建並複製父進程的打開文件列表、文件系統描述符、信號描述符、內存描述符、命名空間等結構;
5)初始化子進程的內核棧,並將eax寄存器對應字段的值設爲0(這樣子進程返回時,系統調用返回值便爲0);
6)結束並返回子進程描述符指針tsk;


對exec函數進行反彙編


execlp分析
系統中存在一個formats鏈表,其鏈表結構分別對應一種可執行文件的執行方法,execlp()函數對應的系統調用sys_exece()函數會分配一個linux_binprm數據結構並將可執行文件的數據拷貝到其中,並依次掃描formats鏈表試圖執行這個可執行文件,一旦找到了就執行鏈表結構中的load_binary方法,其主要步驟爲:
1)將可執行文件的首部拷貝至內存;
2)根據動態鏈接程序路徑名將共享庫對應函數映射到內存;
3)釋放原進程的內存描述符、線性區描述符、所有頁框;
4)選擇線性區的佈局;
5)爲可執行文件的代碼段、數據段以及動態鏈接程序的代碼段、數據段分別進行內存映射;
6)修改內核態堆棧中eip、esp寄存器的值,使其分別指向程序的入口點以及新的用戶態堆棧頂並返回;

動態鏈接執行程序的過程
在傳統的靜態鏈接中,程序中用到的每個庫函數,都會在鏈接器的鏈接過程中,將全部代碼複製到文本段中,這種方式的原理十分簡單,但是會使得程序的文本段過於龐大,對於緊缺的內存資源來說,是一種巨大的浪費。
而動態鏈接的過程,並不需要將各個庫函數的代碼分別進行復制,只需要在程序中靜態指明其鏈接的目標庫函數在庫文件的位置就可以了,在程序真正開始執行之前,動態鏈接器會根據文件中的重定位信息將其鏈接的庫函數映射到內存中。而庫文件因爲是被“映射”到內存中的,所以每個庫只有一個庫文件,且可以同時被幾個不同的程序進行映射。其過程基本如下:
1)參照圖五中各個段的信息,其中.interp段存放了動態鏈接器的路徑,程序運行之前,會首先通過這裏找到動態鏈接器,並加載和運行這個動態鏈接器。
2).got中存放了全局偏移量表GOT,每個被此程序鏈接到的全局數據都有一個對應的條目,靜態編譯時會在其中存放各個重定位記錄,在加載過程中,鏈接器會對其中的各個記錄依次進行重定位,使其含有固定的地址,從此不再變化。
3).plt存放了過程鏈接表PLT,每個被此程序鏈接到的全局函數都有一個對應的條目,此外,每個全局函數在GOT表中也有一個對應條目。執行過程中,每當初次調用其中的某個函數,則會通過PLT表跳轉到GOT表,計算出函數地址後,會重定位GOT表中的條目。之後便不再計算函數地址,直接跳轉得到函數地址。
在鏈接器加載程序運行時,會在一個3GB的用戶虛擬內存空間中進行內存映射(3GB-4GB爲內核空間),鏈接器會根據ELF文件的頭部與段頭部表中的信息,分別對各個段進行處理,並將程序的代碼段從地址0x08048000開始向上映射,緊隨其後的是數據段,堆緊隨數據段之後,以供malloc進行動態內存分配,而用戶態堆棧從最大合法用戶態空間地址向下增長,在堆棧與堆之間是共享庫的鏈接函數內存映射空間,此時內存空間的影響基本如下所示:

圖六
之後加載器轉到程序的入口點,即.text中的_start的位置,這裏的幾行彙編代碼在所有程序加載過程中都是一樣的,它們分別會進行一些初始化例程,並註冊一些退出程序時應執行的例程,最後會執行callmain命令,此時開始執行程序正文。
 
第三部分 實驗總結 

1)進程控制塊task_struct:

    task_struct,就是進程描述符(process descriptor),該數據結構中包含了與一個進程相關的所有信息,比如包含衆多描述進程屬性的字段,以及指向其他與進程相關的結構體的指針。其中有指向mm_struct結構體的指針mm,這個結構體是對該進程用戶空間的描述;也有指向fs_struct結構體的指針fs,這個結構體是對進程當前所在目錄的描述;也有指向files_struct結構體的指針files,這個結構體是對該進程已打開的所有文件進行描述;另外還有一個小型的進程描述符thread_info,結構如下圖所示。


2)ELF文件格式與進程地址空間的聯繫:

    當子進程調用exec時,啓動加載器,加載器刪除子進程已有的虛擬存儲器段,按照path路徑所指向的可執行文件段頭表的指導,將ELF可執行文件的相關內容加載到了當前子進程的上下文中(代碼段和數據段等),它會覆蓋當前子進程的地址空間,從而實現文件組塊與進程空間地址的映射。


3)動態鏈接庫在ELF文件格式中與進程地址空間中的表現形式:

    應用程序通常都需要使用動態鏈接庫,當在 shell 中敲入一個命令要執行時,內核會創建一個新的進程,它在往這個新進程的進程空間裏面加載進可執行程序的代碼段和數據段後,也會加載進動態連接器(在Linux裏面通常就是 /lib/ld-linux.so 符號鏈接所指向的那個程序,它本省就是一個動態庫)的代碼段和數據。在這之後,內核將控制傳遞給動態鏈接庫裏面的代碼。動態連接器接下來負責加載該命令應用程序所需要使用的各種動態庫。加載完畢,動態連接器纔將控制傳遞給應用程序的main函數。如此,應用程序才得以運行。

    爲了讓動態連接器能成功的完成動態鏈接過程,在前面運行的連接編輯器需要在應用程序可執行文件中生成數個特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。

    ELF文件裏面,每一個 sections 內都裝載了性質屬性都一樣的內容,比方:

(1) .text section 裏裝載了可執行代碼;

(2) .data section 裏面裝載了被初始化的數據;

(3) .bss section 裏面裝載了未被初始化的數據;

(4) 以 .rec 打頭的 sections 裏面裝載了重定位條目;

(5) .symtab 或者 .dynsym section 裏面裝載了符號信息;

(6) .strtab 或者 .dynstr section 裏面裝載了字符串信息;

(7) 其他還有爲滿足不同目的所設置的section,比方滿足調試的目的、滿足動態鏈接與加載的目的等等。

   動態鏈接庫在ELF文件格式中與進程地址空間中的表現形式,如下如所示:

 

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