可執行程序的生成過程


  我們通常編寫的文本程序是由ASCII字符組成,但是一個可執行程序是由二進制數字組成,從ASCII——>二進制文件,經歷了

  1. 預處理:進行頭文件和宏定義的替換
  2. 編譯:由編譯器把高級語言代碼編譯為匯編代碼
  3. 匯編:由匯編器把匯編代碼翻譯成二進制代碼,也即是.o文件
  4. 連接:由連接器把多個.o文件連接成可執行文件;可分為編譯時鏈接,加載時鏈接(程序被加載到內存中執行時),運行時鏈接(由應用程序來執行時)。

靜態鏈接

  靜態鏈接器(gcc執行時)以一組可重定位目標文件和命令行參數作為輸入,生成一個完全鏈接的,可以加載運行的可執行目標文件作為輸出。

  靜態庫:把目標文件中的一個打包為單獨一個文件,稱為靜態庫。當構造一個輸出可執行程序時,他只復制靜態庫中被應用程序引用的目標木塊。在鏈接時,應用程序只復制被應用程序引用的目標模塊(一個鏈接庫中有多個函數,只需復制被引用的即可),減少了可執行文件在磁盤和內存中的大小。

靜態庫的缺點:

  1. 如果靜態庫有更新,必須了解到他的最新版本然后顯示的將程序與更新的庫連接。
  2. 他們的代碼會被復制到進程的文本段中,大量的進程在運行的話會造成內存的浪費

  連接器的任務:目標文件是字節塊的集合。它包含了代碼與數據,還有其他連接器加載器的數據結構,連接器是把這些塊連接起來,確定被連接塊運行時的位置,修改代碼和數據塊中的位置。總的來說:

  1. 符號解析:變量或函數。為了讓每個符號引用和一個符號定義關聯。
  2. 重定位:通過把符號定義與一個內存地址關聯起來,重定位這些節,修改這些符號的引用,使他們指向內存的位置。

符號和符號表

  每個可重定位模塊都有一個符號表(它由匯編器構造,使用編譯器輸出到匯編語言.s文件中的符號),包含

  1. 由該模塊定義並能被其他模塊一用的全局符號(非static函數和全局變量)
  2. 由其他模塊定義並被該模塊引用的全局符號(對應於其他模塊的非static函數和全局變量)
  3. 由該模塊定義並且只能被該模塊引用的局部符號(該模塊中static函數和全局變量)

  本地連接器符號和本地程序變量是不同的,.symtab中的符號不包含對應於本地非靜態程序變量的任何符號,這些符號運行時在棧中被管理。

  編譯器向匯編器輸出每個全局符號,匯編器把他們放到符號表條目中,函數和已經初始化的全局變量為強符號,未初始化的全局變量為弱符號。

  1. 不允許有多個同名強符號。
  2. 如果多個強符號和弱符號同名,選擇強符號(模塊A中:int x=10;模塊B中:int x;x=11; 如果在一個模塊里未被初始化,那么連接器將選擇另一個模塊中的定義,這不會產生警告,連接器感受不到多個x的定義)。
  3. 如果多個弱符號同名,選擇任意其中一個

符號解析

   當編譯器遇到不在該模塊定義的符號(變量或函數),假設該符號在其他模塊定義,生成一個連接器符號表條目,並把它交給連接器處理,如果連接器在如果連接器在其它模塊中找不到該符號的定義,會輸出一個錯誤並終止程序。

  連接器在符號解析階段從左到右按照他們在編譯器驅動程序命令行上的出現順序來掃描可重定位目標文件(E,這個集合中的文件會被形成可執行文件)和存檔文件( .c文件翻譯為.o文件)。連接器維護可重定位目標文件,未解析的符號集合(U),在前面輸入中已定義的符號集合(D)

  1. 對於命令行上的文件f,如果是目標文件,則添加到E中,修改D和U反應f中符號的定義和引用,繼續下一個文件
  2. 如果是個存檔文件,則匹配u中未解析的符號和存檔文件成員定義的符號,存檔文件的成員m定義了一個符號來解析U中的一個引用,則把m加到e中,修改u和d反應m中的符號定義和引用。對存檔文件中所有成員目標都依次進行這個過程,直到u和d不再變化。任何不包含在E中的成員目標文件都簡單的被丟棄。連接器繼續處理下一個輸入文件。
  3. 如果掃描完命令行的輸入后,u為非空,則連接器輸出一個錯誤並終止程序,否則合並重定位e中的目標文件。構建輸出的可執行文件。

重定位

  完成符號解析后,代碼中的每個符號引用和一個符號定義(輸入一個輸入目標文件模塊的符號條目表)關聯起來,此時連接器就知道目標模塊代碼和數據確切大小。然后可以開始重定位,為每個符號分配運行時地址。

  1. 重定位節和符號定義。將所有類型的節(比如所有目標模塊中.data)合並為一個新聚合節。然后連接器將運行時的地址賦給新的聚合節,賦給輸入模塊的每個節,賦給輸入模塊的每個符號,當這一步完成時,程序中每條指令和全局變量都有唯一運行時內存地址。
  2. 重定義節中的符號引用。連接器修改代碼節和數據節中的每個符號引用,使他們指向正確的運行時地址。依賴於重定義目標模塊中重定位條目的數據結構

重定位條目

  當匯編器生成一個目標模塊時,他不知道數據和代碼最終放在內存的什么位置,他也不知道模塊引用的外部定義或函數的全局變量的位置。所以無論何時匯編器遇到對最終位置未知的目標引用時,他就會生成一個重定義條目。告訴連接器生成執行文件時如何修改這個條目。重定位代碼條目放在.rel.text中,以初始化的數據放在.rel.data中。

目標文件

  有三種形式:

  1. 可重定位目標文件:包含二進制代碼和數據,可以在編譯時與其他可重定位文件合並共建一個可執行文件。(由編譯器和匯編器生成)
  2. 可執行目標文件:包含二進制代碼和數據,可直接加載到內存執行。(由連接器生成)
  3. 共享目標文件:特殊的可重定位文件,可以在加載時或運行時被動態加載到內存並連接。(由編譯器和匯編器生成)

  目標模塊:一個字節序列;目標文件:以文件形式存放在磁盤上的目標模塊。

可重定位目標文件

 

  ELF頭包含了16字節開始的序列,剩下的是EFL頭大小,目標文件類型,機器類型,節頭部表的文件偏移,節頭目表中的條目大小和數量。

可執行目標文件

  成語一開始是ASCII文本文件,此時被轉化為二進制文件,而且這個二進制文件包含加載程序到內存並運行它所需要的信息。

  ELF頭標書文件的整體格式還包含程序的入口點(程序需要運行時執行的第一條指令的地址)。可執行文件的連續片(chunk)被映射到連續的內存段。

加載可執行目標文件

  當在shell中輸入./programName時,shell解析到/判斷不是內置命令(如果是內置命令時會搜索/usr /usr/lib ...)而是一個可執行文件,調用常駐內存的加載器(通過execve調用加載器)的操作系統代碼來調用他。將可執行程序的代碼和數據從磁盤復制到內存,在程序頭部表的引導下加載器將可執行文件的片(chunk)復制到代碼段和數據段,跳轉到程序的第一條指令或入口點來運行。

  linux的每個程序都運行在一個進程的上下文中,有自己的虛擬地址空間。當一個shell運行時,父進程shell生成一個子進程,他是父進程的一個復制。子進程通過execve系統調用調用加載器,加載器刪除現有的虛擬內存段,創建新的代碼段數據段堆棧,新堆棧被初始化為0,通過將虛擬地址空間的頁映射到可執行文件的頁面大小chunk,新的代碼段和數據段被初始化為可執行文件的內容,最后跳轉到_start,最終調用程序的main函數,除了頭部的一些信息,加載過程沒有任何數據從磁盤復制到內存,知道CPU引用的第一個虛擬頁時才被復制。利用頁面調度算法將他從磁盤復制到內存。

共享目標文件

  共享庫是個目標模塊,在運行加載時,可以被加載到任意內存地址,並和一個在內存中的程序連接起來。這成為動態鏈接由動態鏈接器的程序來執行。

  -fpic指示編譯器生成與位置無關代碼,-shared指示連接器創建一個共享目標文件。

  當創建可執行文件時,靜態執行一些連接,然后在程序加載時,完成動態鏈接過程,此時動態庫中沒有任何代碼和數據被復制到可執行文件中,連接器復制了一些重定位和符號表信息,其中的.interp節包含動態鏈接器的路徑名,然后執行下列完成重定位:

  1. 重定位libc.so的文本段和數據段到某個內存段
  2. 重定位libvector.so的文本段和數據段到某個內存段
  3. 重定位執行程序中對動態庫的符號定義和引用

  最后將控制權給程序,此后共享庫的位置不再變。

注意:

  調用動態庫的時候有幾個問題會經常碰到,有時,明明已經將庫的頭文件所在目錄 通過 “-I” include進來了,庫所在文件通過 “-L”參數引導,並指定了“-l”的庫名,但通過ldd命令察看時,就是死活找不到你指定鏈接的so文件,這時你要作的就是通過修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件來指定動態庫的目錄。通常這樣做就可以解決庫無法鏈接的問題了。

  在linux下可以用export命令來設置這個值,在linux終端下輸入:

  LD_LIBRARY_PATH=newdirs:$LD_LIBRARY_PATH.(newdirs是新的路徑串)

  然后再輸入:export ,即會顯示是否設置正確,export方式在重啟后失效,所以也可以用 vim /etc/bashrc ,修改其中的LD_LIBRARY_PATH變量。 

從引用程序中加載和鏈接共享庫

#include<dlfcn.h>
/*下面是打開一個動態連接庫句柄指針*/
void *dlopen(const char * filename,int flag);
 
/*下面是返回動態連接庫中的函數地址*/
void dlsym(void *handle,char *symbol);
 
/*下面是關閉打開的動態連接庫*/
int dlclose(void *handle);
  
/*下面是用來檢測是否連接成功*/
const char *dlerror(void);

與位置無關代碼

  把代碼加載到內存的任何位置而無需連接器修改。加載而無需重定位的代碼稱為與位置無關代碼,這樣無數進程可以共享代碼段的單一副本。

PIC數據引用

  無論在內存何處加載一個目標模塊(包含共享目標模塊),數據段和代碼段的距離總保持不變,因此代碼段中的任何指令和數據段中的任何變量運行時都是一個常量。

  在數據段開始的地方創建一個GOT(全局偏移量表),在此表中,每個被這個模塊引用的全局數據目標都有一個8字節條目,每個條目還有個重定位記錄。連接器會重定位GOT中的每個條目,使它包含正確的目標地址。

PIC函數調用

  延遲綁定是避免一個庫中成百上千個函數,在連接時需要大量重定位不需要的函數,把地址的綁定延遲到第一次調用該過程時。

庫打樁機制

  給一個需要打樁的目標函數創建個包裹函數,它的原型與目標函數一樣,使用某種打樁機制欺騙系統調用某種包裹函數而不是目標函數。

編譯時打樁

  正是有-I.所以會進行打樁,告訴預處理器在搜索系統目錄之前先在當前目錄中malloc.h查找。

鏈接時打樁

  鏈接時打樁通過在鏈接時傳遞標志 -wl, --wrap f 給鏈接器,告訴鏈接器把符號 f 和 __real_f解析為 __wrap_f,實現替換。同樣,實現替換的函數

#ifdef LINKTIME
#include<stdio.h>
#include<malloc.h>
//std malloc
//試了直接調用malloc,編譯鏈接ok,但是運行時core
void *__real_malloc(size_t size);
void __real_free(void *ptr);
void *__wrap_malloc(size_t size)
{
    void *ptr = __real_malloc(size);
    printf("[debug] malloc size %d\n", (int)size);
    return ptr;
}
void __wrap_free(void *ptr)
{
    __real_free(ptr);
    printf("[debug] free %p\n", ptr);
}
#endif

  編譯代碼,-Wl,option標志把option傳遞給鏈接器,其中的每個都好替換為空格。

gcc -DLINKTIME -c mymalloc.c
gcc -Wl,--wrap,malloc -Wl,--wrap,free -o out main.c mymalloc.o

運行時打樁

  以上兩種需要有源文件的情況下實現,而對於運行時打樁,只需要可以訪問執行文件,利用動態鏈接器的LD_PRELOAD環境變量實現。

  當加載程序時,解析未定義的引用時,動態鏈接器會先搜索LD_PRELOAD指定的庫,然后才搜索其他,因此,通過把自己實現的動態庫設置到這個環境變量,動態鏈接器加載時搜索的該庫內有對應實現的函數,就會直接使用該函數而不會再搜索其他系統庫。

  實現自己的動態庫,包含需要替代的函數

#ifdef RUNTIME
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>

void *malloc(size_t size)
{
    void *(*mallocp)(size_t size);
    char *error;
    // 查找標准庫的實現
    mallocp = dlsym(RTLD_NEXT, "malloc");
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    void *ptr = mallocp(size);
    printf("[debug] malloc size %d\n", (int)size);
    return ptr;
}

void free(void *ptr)
{
    void (*freep)(void *ptr);
    char *error;
    freep = dlsym(RTLD_NEXT, "free");
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    freep(ptr);
    printf("[debug] free %p\n", ptr);
}
#endif

  編譯代碼

all:out
out: main.c mymalloc.o
    gcc -o out main.c

## 編譯共享庫
mymalloc.o: mymalloc.c
    gcc -DRUNTIME --share -fpic -o mymalloc.so mymalloc.c -ldl

.PHONY : clean run
run:
    # 指定運行時加載的庫
    #setenv LD_PRELOAD "./mymalloc.SO"; ./out; unsetenv LD_PRELOAD
    ## 設定環境
    export LD_PRELOAD="./mymalloc.so"; ./out; unset LD_PRELOAD
    ## 其他任何的可執行程序都可以打樁
    export LD_PRELOAD="./mymalloc.so"; uptime; unset LD_PRELOAD

clean:
    @rm -rf out *.so

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM