張軒 學號: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()
- #include<sys/types.h>
- #include<unistd.h>
- pid_t fork(void);
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
這個系統調用複製當前進程,在進程表中創建一個新的表項,新表項中的許多屬性與當前進程相同。但是新進程有自己的數據空間(堆和棧),環境和文件描述符。在父進程中的fork調用返回的是新的子進程的PID,而新進程返回的是0.程序代碼也靠這一點來區分父子進程。創建失敗返回-1.這邊在之前看到過有這麼一個解釋,相當與是一個鏈狀的進程序列,子進程沒有兒子了,所以0相當於指向爲空
以下爲示例
- #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;
- }
#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函數將程序的執行從一個程序切換到另一個程序。在新的程序啓動後,原來的程序就不再運行了。
- #include<unistd.h>
- char ** environ;
- int execl(constchar *path,constchar *arg0,...,(char *)0);
- int execlp(constchar *file,constchar *arg0,...,(char *)0);
- int execle(constchar *path,constchar *arg0,...,(char *)0,char *const envp[]);
- int execv(constchar *path,char *const argv[]);
- int execvp(constchar *file,char *const argv[]);
- 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中讀入。
- #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);
- }
#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函數進行反彙編
彙編的時候要注意設置斷點
如下:
- gcc -g forktest.c -o forktest
- gdb forktest
- b fork
- r
- disas fork
gcc -g forktest.c -o forktest
gdb forktest
b fork
r
disas fork
對exec函數進行反彙編
系統中存在一個formats鏈表,其鏈表結構分別對應一種可執行文件的執行方法,execlp()函數對應的系統調用sys_exece()函數會分配一個linux_binprm數據結構並將可執行文件的數據拷貝到其中,並依次掃描formats鏈表試圖執行這個可執行文件,一旦找到了就執行鏈表結構中的load_binary方法,其主要步驟爲:
動態鏈接執行程序的過程
圖六
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文件格式中與進程地址空間中的表現形式,如下如所示: