摘要:在本文的這個部分,針對 Linux 系統是如何來辨別這些不同的可執行檔,以及整體的執行流程來作一個說明。 程序啟動的流程 在 linux 的環境中最常見的可執行檔的種類包括了 Script 檔、Aout 格式的執行檔、ELF 格式的執行檔。在本文的這個部分,我會針對 Linux 系統是如何來辨別這些不同的可執行檔,以及整體的執行流程來作一個說明。 我在此大略說明一下程序啟動的流程,當我們在 shell 中輸入指令時,會先去系統的路徑中來尋找是否有該可執行檔存在,如果找不到的話,就會顯示出找不到該可執行檔的訊息。如果找到的話,就會去呼叫 execve()來執行該檔案,接下來 execve() 會呼叫 System Call sys_execv(),這是在Linux 中 User Mode 透過 80 號中斷(int 80 ah=11)進入 Kernel Mode 所執行的第一個指令,之後在 Kernel 中陸續執行 do_exec()、 prepare_binprm()、read_exec()、search_binary_handler(),而在 search_binary_handler() 函式中,會逐一的去檢查目前所執行檔案的型態(看看是否為Script File、aout 或 ELF 檔),不過 Linux 所采用的方式是透過各個檔案格式的處理程序來決定目前的執行檔所屬的處理程序。 如下圖,會先去檢驗檔案是否為 Script 檔,若是直進入 Script 檔的處理程序。若不是,則再進入 Aout 檔案格式的處理程序,若該執行檔為 Aout 的檔案格式便交由 Aout檔案格式的處理程序來執行。如果仍然不是的話,便再進入 ELF 檔案格式的處理程序,如果都找不到的話,則傳回錯誤訊息。 [[The No.1 Picture.]] 由這種執行的流程來看的話,如果 Linux Kernel 想要加入其他的執行檔格式的話,就要在 search_binary_handler() 加入新的執行檔的處理程序,這樣一旦新的執行檔格式產生後,在 Linux 下要執行時,因為在do_load_script、do_load_aout_binary、do_load_elf_binary都會傳回錯誤,因此只有我們自己的 do_load_xxxx_binary 函式可以正確的接手整個執行檔的處理流程,因此便可以達成新的檔案格式置入的動作哩。 在函式 do_load_elf_binary () 執行時,首先會去檢視目前的檔案是否為 ELF 格式,如下程序碼 if (elf_ex.e_ident[0] != 0x7f' ' strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0) goto out; 便是去檢查該檔的前四個 bytes 是否為 0x7f 加上 “ELF” (0x 45 0x4c 0x46),若非,則結束 do_load_elf_binary 的執行。之後,便是去檢視我們之前提過的 e_type 屬性,來得知是否為 ET_EXEC(Executable File) 或是ET_DYN(Shared Object File) 這兩個值的其中之一 if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN) goto out; 如果都不是這兩個值之一,便結束 do_load_elf_binary 的執行之後便是一連串讀取 ELF 檔表格的動作,在此就不多說,有興趣的讀者可以自行參閱/usr/src/linux/fs/binfmt_elf.c 的內容即可。 在此我們檢視一個執行檔由啟動到結束的完整流程,首先這個執行檔具有如下的程序碼 #include int main() { printf(" test "); } 然後,透過如下的編程過程 gcc test.c ˉo test 我們如果檢視執行檔的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式庫中以下的函式 printf __deregister_frame_info __libc_start_main __register_frame_info 接下來,我們便把程序的執行流程大略整理如下,而 execve("./test", ["./test"], []) 執行的流程,就是剛剛我們所提到的內容,若不熟悉的讀者,可以再回頭看看剛剛的內容,即可對 execve("./test", ["./test"], []) 的執行流程有大略的了解。在這裡,我們會把整個執行流程更完整的來檢視一遍。 首先,我們所在的執行環境會透過 execve("./test", ["./test"], []) 的函式呼叫來啟動 test 執行檔。 呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯讀模式開啟 ld.so.cache,這個檔案的功能是作為動態函式庫的快取,它會記錄了目前系統中所存在的動態函式庫的資訊以及這些函式庫所存在的位置。所以說,如果我們在系統中安裝了新的函式庫時,我們便需要去更新這個檔案的內容,以使新的函式庫可以在我們的 Linux 環境中發生作用,我們可以透過 ldconfig 這個指令來更新 ld.so.cache 的內容。 呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 檔案映射到記憶體中,mmap 函式的宣告為 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在筆者的電腦上 ld.so.cache 的檔案大小為 9937 bytes,PROT_READ代表這塊記憶體位置是可讀取的,MAP_PRIVATE 則表示產生一個行程私有的 copy-on-write 映射,因此這個呼叫會把整個 ld.so.cache 檔案映射到記憶體中,在筆者電腦上所傳回的映射記憶體起始位置為 0x40013000。 注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我們要求在檔案 fd中,起始位置為offset去映射 length 長度的資料,到記憶體位置 start ,而 prot 是用來描述該記憶體位置的保護權限(例如:讀、寫、執行),flags用來定義所映射物件的型態,例如這塊記憶體是否允許多個 Process 同時映射到,也就是說一旦有一個 Process 更改了這個記憶體空間,那所有映射到這塊記憶體的Process 都會受到影響,或是 flag 設定為 Process 私有的記憶體映射,這樣就會透過 copy-on-write 的機制,當這塊記憶體被別的 Process 修改後,會自動配置實體的記憶體位置,讓其他的 Process 所映射到的記憶體內容與原本的相同。(有關mmap的其它應用,可參考本文最後的注一) 呼叫 open("/lib/libc.so.6", O_RDONLY),開啟 libc.so.6。 呼叫 read(3, "177ELF111331250202"..., 4096) 讀取libc.so.6的檔頭。 呼叫 mmap(0, 993500, PROT_READPROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到記憶體中,由檔頭開始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的讀者或許會好奇 libc.so.6 所 link 到的檔案 libc-2.1.2.so 大小不是 4118715 bytes 嗎? 其實原本 RedHat 所附的 libc.so.6 動態函式庫是沒有經過 strip 過的,如果經過 strip 後,大小會變為 1052428 bytes,而 libc.so.6 由檔頭開始在 993500 bytes 之後都是一些版本的資訊,筆者猜想應該是這樣的原因,所以在映射檔時,並沒有把整個 libc.so.6 檔案映射到記憶體中,只映射前面有意義的部分。與映射 ld.so.cache 不同的是,除了 PROT_READ 屬性之外,libc.so.6 的屬性還多了PROT_EXEC,這代表了所映射的這塊記憶體是可讀可執行的。在筆者的電腦中,libc.so.6 所映射到的記憶體起始位置為 0x40016000。 呼叫 mprotect(0x40101000, 30940, PROT_NONE),用來設定記憶體的使用權限,而 PROT_NONE 屬性是代表這塊記憶體區間(0x40101000—0x401088DC)是不能讀取、寫入與執行的。 呼叫 mmap(0x40101000, 16384, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到記憶體位置 0x40101000。 呼叫 mmap(0x40105000, 14556, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXEDMAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全為 0 的記憶體區塊。 呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的記憶體解除映射(此時已把執行檔所需的動態函式庫都映射到記憶體中了)。 呼叫 personality(0),可以設定目前 Process 的執行區間(execution domain),換個說法就是 Linux 支援了多個執行區間,而我們所設定的執行區間會告訴 Linux 如何去映射我們的訊息號碼(signal numbers)到各個不同的訊息動作(signal actions)中。這執行區間的功能,允許 Linux 對其它 Unix-Like 的操作系統,提供有限度的二進位檔支援。如這個例子中,personality(0) 的參數為 0,就是指定為 PER_LINUX 的執行區間(execution domain)。 #define PER_MASK (0x00ff) #define PER_LINUX (0x0000) #define PER_LINUX_32BIT (0x0000 ADDR_LIMIT_32BIT) #define PER_SVR4 (0x0001 STICKY_TIMEOUTS) #define PER_SVR3 (0x0002 STICKY_TIMEOUTS) #define PER_SCOSVR3 (0x0003 STICKY_TIMEOUTS WHOLE_SECONDS) #define PER_WYSEV386 (0x0004 STICKY_TIMEOUTS) #define PER_ISCR4 (0x0005 STICKY_TIMEOUTS) #define PER_BSD (0x0006) #define PER_XENIX (0x0007 STICKY_TIMEOUTS) #define PER_LINUX32 (0x0008) #define PER_IRIX32 (0x0009 STICKY_TIMEOUTS) /* IRIX5 32-bit */ #define PER_IRIXN32 (0x000a STICKY_TIMEOUTS) /* IRIX6 new 32-bit */ #define PER_IRIX64 (0x000b STICKY_TIMEOUTS) /* IRIX6 64-bit */ 呼叫 getpid(),取得目前 Process 的 Process ID。 呼叫 mmap(0, 4096, PROT_READPROT_WRITE, MAP_PRIVATEMAP_ANONYMOUS, -1, 0),傳回值為 0x400130,MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全為 0 的記憶體區塊。 呼叫 write(1, " test ", 6),顯示字串在畫面上。 呼叫 munmap(0x40013000, 4096),解除記憶體位置0x40013000的記憶體映射。 呼叫 _exit(6),結束程序執行。 在這段所舉的例子,只用到了一個函式庫 libc.so.6,我們可以舉像是 RedHat 中 Telnet 指令為例,首先檢視他的 ELF Header ==>libncurses.so.4 tgetent ==>libc.so.6 strcpy ioctl printf cfgetospeed recv connect ............┅ sigsetmask __register_frame_info close free 它主要呼叫了函式庫 libncurses.so.4 的函式 tgetent,以及函式庫 libc.so.6 中為數不少的函式,當然我們也可以去檢視它執行的流程,與之前只呼叫了 libc.so.6 的printf 函式來比較,我們可以發現它主要的不同就是去載入了 libncurses.so.4 open("/usr/lib/libncurses.so.4", O_RDONLY) ; fstat(3, {st_mode=S_IFREG0755, st_size=274985, ...}) ; read(3, "177ELF111331340335"..., 4096) ; mmap(0, 254540, PROT_READPROT_EXEC, MAP_PRIVATE, 3, 0); mprotect(0x40048000, 49740, PROT_NONE); mmap(0x40048000, 36864, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXED, 3, 0x31000); mmap(0x40051000, 12876, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXEDMAP_ANONYMOUS, -1, 0) ; close(3);