1. 程式人生 > >Linux------可執行程式的裝載

Linux------可執行程式的裝載

一、基礎知識
(1)可執行程式是怎麼來的?
一個.c檔案經過編譯器預處理(.cpp),
然後編譯成彙編程式碼(.S/.asm),
由彙編器生成目的碼(.o二進位制),
由連結器連結成可執行檔案,
最後由作業系統載入到記憶體中然後執行
用gcc執行的過程:
1.預處理:gcc -E -o hello.cpp hello.c -m32
2.編譯:gcc會檢查程式碼(是否有語法錯誤等),將程式碼翻譯成組合語言
gcc -x cpp-ouput -S -o hello.s hello.cpp -m32
3.彙編:將編譯階段生成的.S檔案轉變為目標檔案(.o)
gcc -x assembler -c hello.s -o hello.o -m32
4.連結:將編譯輸出.o檔案連結成最終的可執行檔案(hello也是一個二進位制檔案)
gcc -o hello-static hello.o -m32 -static
(2)可執行檔案的內部是怎樣的?


1.目標檔案的格式ELF
1)目標檔案格式分類
這裡寫圖片描述
2)ABI
ABI:應用程式二進位制介面,在目標檔案中二進位制相容模式適應到某一種cpu體系結構上的二進位制指令
3)ELF中三種目標檔案
1.可重定位檔案(.o檔案):用來和其他的object檔案一起建立一個可執行檔案或一個共享檔案
2.可執行檔案:用來儲存一個可執行的程式,該檔案指出了exec(BA_OS)如何來建立程式程序映像(作業系統如何把程式載入起來,並且從哪裡開始執行)
3.共享目標檔案(.so):儲存著程式碼和合適的資料,用來被下面的兩個連結器連結:第一種是連結編輯器,第二種是動態連結器
4)ELF頭(儲存了很多關鍵資訊)
這裡寫圖片描述

5)可執行的檔案載入的工作:當建立或者增加一個程序映像時,系統在理論上將拷貝一個檔案的段到虛擬的記憶體段
6)靜態連結的ELF可執行檔案與程序的地址空間
這裡寫圖片描述
1.一個程序載入了新的可執行檔案開始的入口點
2.一般靜態連結會將所有的程式碼放在一個程式碼段
3.動態連結的程序有多個程式碼段
(3)可執行程式、共享庫和動態程序
1、裝載可執行程式之前的工作
可執行程式的執行環境:shell命令列、main函式引數、execve引數
1)命令列引數和shell環境

  • 列出/usr/bin下的目錄資訊:
    $ ls -l /usr/bin
  • Shell本身不限制命令列引數的個數,命令列引數的個數受限於命令自身
    int main(int argc, char *argv[], char *envp[])
    //envp接受shell命令列的相關變數
  • Shell會呼叫execve將命令列引數和環境引數傳遞給可執行程式的main函式:
    int execve(const char * filename,char * const argv[ ],char * const envp[ ])
    Shell會先定義一個子程序,在子程序中呼叫execlp(“/bin/ls”,”ls”,NULL);
  • 庫函式exec*都是execve的封裝例程
    (4)命令列引數和環境變數是如何儲存和傳遞的?是如何進入新程式的堆疊的
    1)當fork時,子程序複製父程序的堆疊,呼叫execv時,在載入的可執行程式前將原來的程序用要載入的可執行程式覆蓋掉,覆蓋掉後用戶棧和堆疊會被清空。
    2)命令列引數和環境變數都存放在使用者堆疊中
  • shell程式 —> execve —> sys_execve
  • 初始化新程式堆疊時拷貝進去(execve在建立可執行程式堆疊時,幫我們拷貝進去)
    這裡寫圖片描述
    新的程式從main函式開始講對應的引數接收進來然後先函式呼叫引數傳遞,再系統呼叫引數傳遞
    3)裝載時動態連結和執行時動態連結應用
    動態連結分為可執行程式裝載時動態連結和執行時動態連結
    這裡寫圖片描述
    對於動態連結庫,可以作為在程序裝載的時候動態連結。也可以作為執行時裝載起來
    標頭檔案 # include < dlfcn.h > //動態載入

編譯main:-L :庫對應的介面標頭檔案所在的目錄
-l:苦命,如Linshlibexample.so,去掉lib和.so部分

gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32

-ldl:動態載入
(5)可執行程式的裝載
1、execve系統呼叫的核心處理過程(execve也是一種特殊的系統呼叫)
1)新的可執行程式起點——一般是地址空間為0x8048000或0x8048300
2)execve和fork都是特殊的系統呼叫——一般的都是陷入到核心態再返回到使用者態

  • fork兩次返回,第一次返回到父程序繼續向下執行,第二次是子程序返回到ret_from_fork然後正常返回到使用者態。
  • execve執行的時候陷入到核心態,用execve中載入的程式把當前正在執行的程式覆蓋掉,當系統呼叫返回的時候也就返回到新的可執行程式起點(不是原來的位置了)
  • sys_execve內部會解析可執行檔案格式
    do_ execve —> do_ execve_common —> exec _binprm
      search_ binary _ handler符合尋找檔案格式對應的解析模組
      對於ELF格式的可執行檔案fmt->load_ binary(bprm);執行的應該是load_ elf _binary其內部是和ELF檔案格式解析的部分需要和ELF檔案格式標準結合起來閱讀
    2.search_binary _handle符合尋找檔案格式對應的解析模組,根據ELF檔案頭部資訊尋找對應的檔案格式處理模組
    尋找能解析ELF格式的模組
    對於ELF格式的可執行檔案fmt->load_ binary(bprm):執行的應該是load_elf _ library,其內部是和ELF檔案格式解析的部分(和ELF標準相聯絡)
    3.Linux核心是如何支援多種不同的可執行檔案合適的?
    elf_ format全域性變數:將load_ elf_ binary賦給了全域性變數的指標load_ library,在init elf binfmt時register _ binfmt( &elf _ format),將它註冊到核心連結串列(fmt連結串列),(elf format和init _elf binfat像是觀察者模式中的觀察者)
    當出現elf檔案時,elf format 自動執行 load elf _ binary 實際上執行了 retval _fmt ->load _binary (bprm),(多型機制)
    在load _ elf_ bimary 中呼叫了 start _thread(struct pt _regs *regs,unsigned long new _ip,unsigned long new _sp);
    修改了pt_ regs
    load_ elf_binary中,呼叫了start _thread()函式,通過修改核心堆疊中EIP的值作為新程式的起點
    將flags,ip,sp都壓棧,regs->ip = new_ip,regs.sp = new _ sp,
    其中new_ ip來自:在load elf _binary中,start thread(regs,elf _ entry,bprm->p),在新的可執行程式返回到使用者態之前,要修改int $0x80壓人核心堆疊的EIP,用新的可執行檔案來修改
    (6)sys_execve的內部處理過程

  • 系統呼叫的入口:do_execve
    return do_execve(getname(filename), argv, envp);

  • 轉到do _ execve _ common函式
    return do_ execve_ common(filename, argv, envp);
      file = do_ open_exec(filename); //開啟要載入的可執行檔案,載入它的檔案頭部
      bprm->file = file;
      bprm->filename = bprm->interp = filename->name; //建立了一個結構體bprm,把環境變數和命令列引數都copy到結構體中
  • exec_binprm(對可執行檔案的處理過程)
      ret = search_binary_handler(bprm);  //尋找此可執行檔案的處理函式 在其中關鍵的程式碼
      list_ for each entry(fmt, &formats, lh);
      retval = fmt->load_ binary(bprm);
       //在這個迴圈中尋找能夠解析當前可執行檔案的程式碼並加載出來,實際呼叫的是load_elf _binary函式

  • 檔案解析相關模組:核心的工作就是把檔案對映到程序的空間,對於ELF可執行檔案會被預設對映到0x8048000。

  • 需要動態連結的可執行檔案先載入連結器ld​(load _ elf _ interp 動態連結庫動態連結檔案),動態連結器的起點
  • 如果它是一個靜態連結,可直接將檔案地址入口進行賦值
    (7)結構體變數如何進入到核心的處理模組?
    在init _ elf binfmt中,函式register binfmt(&elf _ format)。
    需要動態連結庫的可執行檔案先載入動態連結器ld,
    if(elf_ interpreter)需要載入其他的動態庫
    執行elf_load _elf _interp<——載入動態連結器
    else
    如果是靜態連結檔案執行
    elf _ entry = loc->elf _ex.e _entry
    在start _thread中直接使用elf _entry
    1.如果elf _entry是動態連結檔案,elf指向連結器的起點
    2.如果elf _entry是靜態連結檔案,elf指向可執行檔案中規定的頭(main函式的位置)
    將cpu的控制權交給ld來載入依賴庫並完成動態連結
    對於靜態連結的檔案elf_ entry是新程式執行的起點
    (8)用莊生夢蝶的典故理解可執行程式的載入
    莊周(呼叫execve的可執行程式)入睡(呼叫execve陷入核心),醒來(系統呼叫execve返回使用者態)發現自己是蝴蝶(被execve載入的可執行程式)(醒來時發現自己不是原來的“自己”了)。
    (9)動態連結的可執行程式的裝載

  • 實際上動態連結庫的依賴關係會形成一個“依賴樹”

  • 動態連結庫的裝載過程一般是一個圖的廣度遍歷
    將所有依賴的動態連結庫裝載起來,裝載和連結之後ld將cpu的控制權交給可執行程式。
  • 動態連結是由動態連結器完成而不是核心

總之:靜態連結:直接執行可執行程式的入口
動態連結:裝載和連結之後ld將CPU的控制權交給可執行程式

二、實驗部分 ——Linux核心如何裝載和啟動一個可執行程式
(一)搭建環境
(檢視程式碼時,可以使用shift+G直接跳到檔案末尾)
這裡寫圖片描述
修改Makefile檔案
這裡寫圖片描述
(生成根檔案系統時,將init hello放入rootfs地址中,這樣在執行exec檔案時,就自動載入hello檔案)
這裡寫圖片描述
這裡寫圖片描述
(二)使用gdb跟蹤sys_execve核心函式的處理過程
1、載入符號表,並連線到埠1234
2、設定斷點
這裡寫圖片描述
3、執行
這裡寫圖片描述
這裡寫圖片描述
輸入c繼續執行,進入到sys_execve系統呼叫:
這裡寫圖片描述
輸入s進行跟蹤:
這裡寫圖片描述
new_ip是返回到使用者態的第一條指令的地址:
這裡寫圖片描述
用readelf -h hello 檢視資訊,入口點地址為0x8048doa

二、實驗總結
由靜態連結和動態連結對可執行檔案的載入過程進行了解,通過對execve系統呼叫的功能和執行的分析,瞭解了可執行檔案的載入過程,應記憶函式的功能和特點以及引數傳遞的方法,動態連結庫和靜態連結庫的區別等等。