第7節 Linux核心如何裝載和啟動一個可執行程式【Linux核心分析】
一、實驗要求
分析exec*函式對應的系統呼叫處理過程
二、實驗內容
- 理解編譯連結的過程和ELF可執行檔案格式,詳細內容參考本週第一節;
- 程式設計使用exec*庫函式載入一個可執行檔案,動態連結分為可執行程式裝載時動態連結和執行時動態連結,程式設計練習動態連結庫的這兩種使用方式,詳細內容參考本週第二節;
- 使用gdb跟蹤分析一個execve系統呼叫核心處理函式sys_execve ,驗證您對Linux系統載入可執行程式所需處理過程的理解,詳細內容參考本週第三節;推薦在實驗樓Linux虛擬機器環境下完成實驗。
- 特別關注新的可執行程式是從哪裡開始執行的?為什麼execve系統呼叫返回後新的可執行程式能順利執行?對於靜態連結的可執行程式和動態連結的可執行程式execve系統呼叫返回時會有什麼不同?
三、實驗環境
本地linux環境(ubuntu14.04 64bit)
主要優點:使用方便,方便儲存,不受網路影響。
四、實驗過程
1.可執行檔案的生成過程。
首先引用一張孟老師的圖,來說明可執行檔案生成的過程,總結的很贊
可執行檔案是給計算機中的cpu執行的二進位制程式碼。按照老師上課使用的shell命令演示一下。
hello.static 是靜態連結編譯的可執行檔案,可以很看到,比動態連結編譯的hello檔案要大得多,相差100倍,原因也很簡單,就是靜態連結是將程式中需要使用的庫都放在了靜態編譯的檔案中,導致檔案大小劇增。
演示很簡單,兩個的效果表面是看不出來的
2.檢視elf檔案相關資訊
對於靜態連結的elf檔案,基本上在載入時對應加上程式入口地址,將相應的程式碼資料載入到對應的記憶體空間中,然後逐步執行程式碼。以下是我的ELF Header情況。
下面來檢視elf頭資訊,從圖中可以看出真正的程式碼從0x8048320開始,這是程式載入記憶體被執行的真正入口
3.動態連結庫
通常我們的程式還需要使用動態連結庫。分為裝載時動態連結和執行時動態連結。
生成共享庫和執行時連結庫:
gcc -shared shlibexample.c -o libshlibexample.so -m32
gcc -shared dllibexample. c -o libdllibexample.so -m32
生成的庫檔案如下,並執行
4. 跟蹤execlp的呼叫
這次跟蹤分析一個execve系統呼叫核心處理函式sys_execve。
1.將test.c替換為test_exec.c,為MenuOS增加exec功能。
cd LinuxKernel
cd menu
mv test_exec.c test.c
新的test.c的main函式中為介面增加了exec的選項。
檢視exec本身程式碼內容。就是一個簡單的程式。子程序執行了hello。
2.除錯MenuOS
編譯執行menu,因為前面幾個實驗都做了無數遍了,就不多浪費篇幅。加上-s –S執行,然後利用gdb跟蹤,設3個斷點:sys_execve、load_elf_binary、start_thread。
continue繼續執行,啟動menu過程中會觸發斷點,直接繼續執行。menu啟動完成後,輸入exec命令,這時會停在第一個斷點。
繼續執行,停在第二個斷點,是裝載的過程。
繼續執行,停在第三個斷點。
這是注意到有一個新的ip地址new_ip作為引數傳遞過來。列印後發現這個地址是0x8048d2a,其內容目前無法訪問。
這時另開一個終端視窗,列印hello的標頭檔案,發現就是hello可執行程式的起始位置,即使用者態第一條指令的位置。
執行到start_thread程式碼。
下面是start_kernel的原始碼
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的入口地址。
繼續執行,程式就將進入使用者態執行
五、程式碼分析
execve和前面博文分析的fork系統一樣,是一種特殊的系統呼叫。fork的特殊在於系統呼叫後兩次返回,生成了新程序,而不單單是在原來程式的系統呼叫的下一條語句。而execve的特殊在於它返回之後,執行的是一個新的程式了(例如返回程式的main入口,修改的是elf_entry),而不是以前呼叫execve的程序shell了。
核心處理函式sys_execve內部會解析可執行檔案格式,它的內部執行流程是do_execve -> do_execve_common -> exec_binprm。
gdb斷點設定:b sys_execve ;停到該位置,繼續設定斷點 b load_elf_binary; b start_thread。
其中的一些函式解釋:
1)search_binary_handler符合尋找檔案格式對應的解析模組,如下:
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
對於ELF格式的可執行檔案fmt->load_binary(bprm);執行的應該是load_elf_binary
2)Linux核心是如何支援多種不同的可執行檔案格式的?
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}
elf_format 和 init_elf_binfmt,就是觀察者模式中的觀察者。
3)可執行檔案開始執行的起點在哪裡?如何才能讓execve系統呼叫返回到使用者態時執行新程式?
load_elf_binary -> start_thread中通過修改核心堆疊中的EIP的值作為新程式的起點。即修改一開始int 0x80壓入核心堆疊的EIP。start_thread中的new_ip是返回到使用者態第一條指令的地址,與可執行程式的頭中的入口地址相同。
六、總結
新的可執行程式是從new_ip開始執行,start_thread實際上是把返回到使用者態的位置從Int 0x80的下一條指令,變成了規定的新載入的可執行檔案的入口位置,即修改核心堆疊的EIP的值作為新程式的起點。
當執行到execve系統呼叫時,陷入核心態,用execve載入的可執行檔案覆蓋當前程序的可執行程式,當execve系統呼叫返回時,返回新的可執行程式的執行起點(main函式位置),所以execve系統呼叫返回後新的可執行程式能順利執行。
對於靜態連結的可執行程式和動態連結的可執行程式execve系統呼叫返回時,如果是靜態連結,elf_entry指向可執行檔案規定的頭部(main函式對應的位置0x8048***);如果需要依賴動態連結庫,elf_entry指向動態連結器的起點。動態連結主要是由動態連結器ld來完成的。