1. 程式人生 > >第7節 Linux核心如何裝載和啟動一個可執行程式【Linux核心分析】

第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來完成的。

參考資料