1. 程式人生 > >linux可執行檔案建立過程 淺析

linux可執行檔案建立過程 淺析

特色:
- 加入動態演示,去驗證exec過程
- 加入自己理解的資料結構圖示(自創/網路引用)

linux世界很大,我想去看看

一. 可執行檔案的建立

基礎

預處理、編譯和連結 實踐

shiyanlou:~/ $ cd Code                                                [9:27:05]
shiyanlou:Code/ $ vi hello.c                                          [9:27:14]
shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c
-m32 #替換巨集 shiyanlou:Code/ $ vi hello.cpp #可以看到預處理的中間檔案 shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32 [9:35:21] shiyanlou:Code/ $ vi hello.s #asm code shiyanlou:Code/ $ gcc -x assembler -c hello.s
-o hello.o -m32 [9:35:58] shiyanlou:Code/ $ vi hello.o #object code ,binary file shiyanlou:Code/ $ gcc -o hello hello.o -m32 shiyanlou:Code/ $ vi hello #executive file (link dynamic library..) shiyanlou:
Code/ $ gcc -o hello.static hello.o -m32 -static [9:40:21] #executive file (link static library..) shiyanlou:Code/ $ ls -l -rwxrwxr-x 1 shiyanlou shiyanlou 7292 3\u6708 23 09:39 hello #with dyanmic -rw-rw-r-- 1 shiyanlou shiyanlou 64 3\u6708 23 09:30 hello.c -rw-rw-r-- 1 shiyanlou shiyanlou 17302 3\u6708 23 09:35 hello.cpp -rw-rw-r-- 1 shiyanlou shiyanlou 1020 3\u6708 23 09:38 hello.o -rw-rw-r-- 1 shiyanlou shiyanlou 470 3\u6708 23 09:35 hello.s -rwxrwxr-x 1 shiyanlou shiyanlou 733254 3\u6708 23 09:41 hello.static #with static lib ,more space, aoubt 100 times than dyanmic version

目標檔案及連結
abi 二進位制相容,已經適應某一計算機架構

ELF檔案格式

可以參考ELF檔案格式 :(中文翻譯版),以下是檔案格式的概要圖(by morphad)

elf共分為三種:

一個可重定位(relocatable)檔案儲存著程式碼和適當的資料,用來和其他的object檔案一起來建立一個可執行檔案或者是一個共享檔案。
一個可執行(executable)檔案儲存著一個用來執行的程式;該檔案指出了exec(BA_OS)如何來建立程式程序映象。
 一個共享object檔案儲存著程式碼和合適的資料,用來被下面的兩個連結器連結。第一個是連線編輯器[請參看ld(SD_CMD)],可以和其他的可重定位和共享object檔案來建立其他的object。第二個是動態連結器,聯合一個可執行檔案和其他的共享object檔案來建立一個程序映象。

檢視ELF檔案的頭部

shiyanlou:Code/ $ readelf -h hello

靜態elf 的程序地址空間

感謝:程式設計師修養書中圖,page 167

當前最基礎的是要認識:

  • 程式碼段其實地址: 0x804 8000
  • 程序地址空間分佈,按照地址遞減來看,是 stack->heap->data->code

二. 可執行程式的執行環境

  • 命令列引數和shell環境,一般我們執行一個程式的Shell環境,我們的實驗直接使用execve系統呼叫。

    • $ ls -l /usr/bin 列出/usr/bin下的目錄資訊
    • Shell本身不限制命令列引數的個數,命令列引數的個數受限於命令自身
    • 例如,int main(int argc, char *argv[])
    • 又如, int main(int argc, char *argv[], char *envp[])
    • Shell會呼叫execve將命令列引數和環境引數傳遞給可執行程式的main函式
    • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
    • 庫函式exec*都是execve的封裝例程
  • 命令列引數和環境串都放在使用者態堆疊中

    • what:你會看到引數塊/環境塊的指標
    • who did it: shell->execve->sys_execve在程式初始化的時候把以上環境搭建好的

三. 可執行程式的裝載

shell相關

  • 命令列引數和shell環境,一般我們執行一個程式的Shell環境,我們的實驗直接使用execve系統呼叫。
    Shell本身不限制命令列引數的個數,命令列引數的個數受限於命令自身
    • 例如,int main(int argc, char *argv[])
    • 又如, int main(int argc, char *argv[], char *envp[])
  • Shell會呼叫execve將命令列引數和環境引數傳遞給可執行程式的main函式
    • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
    • 庫函式exec*都是execve的封裝例程
小結 引數個數; execv* ->main

sys_execv相關

sys_execve內部會解析可執行檔案格式
- do_execve -> do_execve_common -> exec_binprm

  • search_binary_handler符合尋找檔案格式對應的解析模組,如下:
1369    list_for_each_entry(fmt, &formats, lh) {
1370        if (!try_module_get(fmt->module))
1371            continue;
1372        read_unlock(&binfmt_lock);
1373        bprm->recursion_depth++;
1374        retval = fmt->load_binary(bprm);
1375        read_lock(&binfmt_lock);
  • 對於ELF格式的可執行檔案fmt->load_binary(bprm);執行的應該是load_elf_binary其內部是和ELF檔案格式解析的部分需要和ELF檔案格式標準結合起來閱讀
    Linux核心是如何支援多種不同的可執行檔案格式的?
82static struct linux_binfmt elf_format = {
83  .module     = THIS_MODULE,
84  .load_binary    = load_elf_binary,//函式指標
85  .load_shlib = load_elf_library,
86  .core_dump  = elf_core_dump,
87  .min_coredump   = ELF_EXEC_PAGESIZE,
88};
2198 static int __init init_elf_binfmt(void)
2199{
2200 register_binfmt(&elf_format);#註冊
2201    return 0;
2202}

四. 動態連結的過程

動態連結的過程核心做了什麼?可執行檔案依賴的動態連結庫(共享庫)是由誰負責載入以及如何遞迴載入的?

基礎

首先要理解elf格式,

會發現: a.so –>b.so…(動態連結庫 呼叫另外一個動態連結庫)

實踐:ldd test
動態連結形成的樹

程式碼

解釋的過程,就用到動態解析器,
格式上就要關注 elf 格式中 .interp 和 .dynamic
程式碼就看load_elf_binary

load_elf_binary(...)
{
...
kernel_read();//其實就是檔案解析
...
//對映到程序空間 0x804 8000地址
elf_map();//
...
if(elf_interpreter) //依賴動態庫的話
{...
//裝載ld的起點  #獲得動態聯結器的程式起點
elf_entry=load_elf_interp(...);
...
}
else //靜態連結
{...
elf_entry = loc->elf_ex.e_entry;
...
}
...
//static exe: elf_entry: 0x804 8000
//exe with dyanmic lib: elf_entry: ld.so addr
start_thread(regs,elf_entry,bprm->p);

}

實際上,裝載過程是 一個 bfs ,廣度遍歷,遍歷的物件是“依賴樹”

主要過程是動態連結器( in libc)完成,使用者態完成。
簡言之,本次學習最基礎就是要知道和靜態連結的區別,具體執行過程可以以後專題再深入。

五. do_execve 程式碼情景分析

流程是:do_execve -> do_execve_common -> exec_binprm

do_execve_common

fs/exec.c 檔案裡
可執行程式的裝載視訊(第二個)詳細解釋
do_execve //命令列引數填充結構體
do_execve_common
{
...
//開啟可執行檔案
do_open_exec  
//填充頭部
bprm->file = file;
bprem->filename = bprm->interp = filename->name;

//填充環境變數 命令列引數

//關鍵
execv_binrm(bprm);
...
}

exec_binprm

static int exec_binprm(struct linux_binprm *bprm)
{
...
//尋找對應可執行檔案格式 的 處理函式
search_binary_handle(bprm);
...

}

search_binary_handle

int search_binary_handle(struct linux_binprm *bprm)
{
...
//在連結串列裡尋找可解析的方案
list_for_each_entry(fmt, &format, lh);//line 1369

...
}

list_for_each_entry

list_for_each_entry(fmt, &formats, lh){
...
//載入可執行檔案的處理函式
//實際是load_elf_binary for elf
retval = fmt->load_binary(bprm);
...
}

load_elf_binary

//此前可以關注 elf_format結構體內的賦值
//這個結構體被放到連結串列裡面,可以看作是觀察者模式/多型的應用
//初始化也有對應的api

load_elf_binary(...)
{
...
kernel_read();//其實就是檔案解析
...
//對映到程序空間 0x804 8000地址
elf_map();//
...
if(elf_interpreter) //依賴動態庫的話
{...
//裝載ld的起點
elf_entry=load_elf_interp(...);
...
}
else //靜態連結
{...
elf_entry = loc->elf_ex.e_entry;
...
}
...
//static exe: elf_entry: 0x804 8000
//exe with dyanmic lib: elf_entry: ld.so addr
start_thread(regs,elf_entry,bprm->p);
...


}

整體流程圖

謝謝ma89481508的精心製作,訪問原帖請點選圖片

圖中有輕重的說明了execve–> do——execve –> search_binary_handle –> load_binary流程

堆疊變化圖

謝謝morphad的精心製作,訪問原帖請點選圖片

圖中對於引數塊和環境塊如何被傳到新程序是很好的說明

六. 實踐 兩種動態庫呼叫方式

驗證兩種動態庫實現

兩種動態庫實現檔案比較起來就沒有差別!
差異關鍵在呼叫端:main函式

比較庫的實現


沒有功能差異,右側僅僅多個額外的巨集定義

比較庫標頭檔案


同上

驗證

  • 編譯庫
gcc -shared shlibexample.c -o libshlibexample.so -m32
gcc -shared dllibexample.c -o libdllibexample.so -m32

和常規的可執行檔案比較,這裡注意的就是要加上 -shared標誌

  • 編譯Main
gcc main.c -o main -L/media/sda_m/SharedLibDynamicLink -lshlibexample -ldl -m32

額外需要注意的就是這裡library路徑需要指明當前的,原本偷懶來個 -L. ,這樣是不可以的
出現典型的找不到動態庫的錯誤

#錯誤情形
/usr/bin/ld: cannot find -lshlibexample
collect2: ld returned 1 exit status
  • 設定動態庫查詢的位置
export LD_LIBRARY_PATH=$PWD

如果不設定,將會報找不到動態庫

#錯誤情形
./main: error while loading shared libraries: libshlibexample.so: cannot open shared object file: No such file or directory
  • 最後一擊
    可以看到我們成功的呼叫 動態庫 和 執行時動態庫的 函式
noya@noya-VirtualBox:/media/sda_m/SharedLibDynamicLink$ ./main
This is a Main program!
Calling SharedLibApi() function of libshlibexample.so!
This is a shared libary!
Calling DynamicalLoadingLibApi() function of libdllibexample.so!
This is a Dynamical Loading libary!


成功的截圖,沒有出現一種瑕疵啊,o(^▽^)o

七.實戰 驗證sys_execv流程

準備

準備工作如下:

#command work flow
rm menu -f
git clone
cd menu
ls
mv test_exec.c test.c 
vim test.c #you will see a new function:Exec
#execlp will be called

大小s來相會,開啟除錯模式

主要的斷點

#breakpoint : info b
b sys_execve #fs/exec.c
b load_elf_binary #fs/binfmt_elf.c
b start_thread

跟蹤過程

load_elf_binary 第一個被擊中,ignore it
進入系統,輸入exec,sys_execve被擊中

debug work flow:
sys_execve -> load_elf_binary ->start_thread

當跟蹤到start_thread時,檢視new_ip執行位置,new_ip是返回使用者空間執行的函式地址,鍵入以下命令:

gdb中:     po new_ip #檢視內容
主機命令列: read_elf -h hello

對比程式入口地址和 new_ip 指向地址一致!
new_ip對於靜態連結程式來說,就是真實的地址。

這裡的內容可以看到ip/sp被賦值,可以詳細跟蹤

動手實踐之

斷點資訊

#more:使用者空間啟動位置
b sys_execve
b load_elf_binary
b start_thread
#more:返回使用者空間的位置
  • load_elf_binary triggered by init
  • now execute the command “exec”
  • now trigger the sys_execve
  • next ,we will start_thread,we will find find new_ip
    “`
    千言萬語,我們來看我只做的gdb除錯exec視訊吧,
    這次主要是驗證exec的流程

”’

八.總結

1)熟悉exec,熟悉兩種裝載動態庫的方式
2)gdb跟蹤實戰驗證了exec的流程
3)可執行程式的起始位置,簡言之就是new_ip指向的位置
4)execve返回後,“新的程序上下文已經安裝好”,新的可執行程式在老程序“一覺睡醒”後開始執行
5)靜態連結的可執行程式,execve中修訂的ip地址是新程序對映到程序空間的地址。
;動態連結的可執行程式,execve中修訂的ip地址是動態聯結器的程式起點

針對話題,Linux核心裝載和啟動一個可執行程式
淺看來,是系統呼叫的sys_exec的實現
深究下來,涵蓋了elf格式解析,解析新程序啟動地址等等。
核心很大,這僅僅是一個階段總結。

九. 參考 本週要求

  • Linux核心如何裝載和啟動一個可執行程式
    理解編譯連結的過程和ELF可執行檔案格式,詳細內容參考本週第一節;

  • 程式設計使用exec*庫函式載入一個可執行檔案,動態連結分為可執行程式裝載時動態連結和執行時動態連結,程式設計練習動態連結庫的這兩種使用方式,詳細內容參考本週第二節;

  • 使用gdb跟蹤分析一個execve系統呼叫核心處理函式sys_execve ,驗證您對Linux系統載入可執行程式所需處理過程的理解,詳細內容參考本週第三節;

  • 特別關注新的可執行程式是從哪裡開始執行的?
    為什麼execve系統呼叫返回後新的可執行程式能順利執行?
    對於靜態連結的可執行程式和動態連結的可執行程式execve系統呼叫返回時會有什麼不同?

    (重點) where start ,where end-
  • 根據本週所學知識分析

    exec*函式對應的系統呼叫處理過程

    ,撰寫一篇署名部落格,並在部落格文章中註明“真實姓名(與最後申請證書的姓名務必一致) + 原創作品轉載請註明出處 + 《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”,部落格內容的具體要求如下:

    1. 題目自擬,內容圍繞對Linux核心如何裝載和啟動一個可執行程式;
    2. 可以結合實驗截圖、ELF可執行檔案格式、使用者態的相關程式碼等;
    3. 部落格內容中需要仔細分析新可執行程式的執行起點及對應的堆疊狀態等。
    4. 總結部分需要闡明自己對“Linux核心裝載和啟動一個可執行程式”的理解

參考

廣告時間