1. 程式人生 > >【軟體開發底層知識修煉】九 連結器-可重定位檔案與可執行檔案

【軟體開發底層知識修煉】九 連結器-可重定位檔案與可執行檔案

上幾篇文章學習了Binutils輔助工具裡面的幾個實用的工具,那些工具對於以後的學習都是非常有幫助的,尤其是C語、C++語言的學習以及除錯是非常有幫助的。點選連結檢視上一篇文章:點選檢視

本篇文章開始一個新的知識的學習,連結器的學習。學習完連結器的系列文章,我們將全面瞭解連結器的工作原理。

注意:本文講解的並不是很詳細,有些關鍵詞例如符號、重定位、段等都沒有具體說。這些比較原始的知識最好先去閱讀相關書籍,進行補充。本文只是通過實驗來理解連結器的作用!!!

1、可重定位檔案與可執行檔案

我們都知道,源程式在經過gcc編譯器編譯後,實際上是經過四個步驟—預處理,編譯,彙編,連結。最終得到一個可執行程式。這個可執行程式最終將會被作業系統的載入器載入帶記憶體中去執行。

在經過彙編之後,生成的檔案是可重定位檔案,然後可重定位檔案經過連結器的連結,最終生成可執行檔案。 今天我們就是來學習這個連結器的。

那麼可重定位檔案是一個什麼樣的檔案?為什麼它不能執行?

經過彙編後的檔案是可重定位檔案。它的檔案格式與可執行檔案很像(對於Linux,都是elf檔案格式)。對於可重定位檔案,它裡面的程式碼與資料,都是各個檔案獨立的程式碼與資料,在一個工程中,會存在多個C檔案,每個C檔案都會被首先編譯生成一個課重定位檔案,然後經過連結器將這些課重定位檔案進行連結,從而生成最終的可執行檔案。

對於可重定位檔案:

  • 各個段沒有具體的起始地址,只要段大小資訊
  • 各個識別符號沒有實際地址,只有在段中的偏移地址(相對地址)
  • 段和識別符號的實際地址都需要連結器具體制定,這也是連結器的主要作用

對於可執行檔案:

  • 各個段有自己的起始地址,這些地址就是將來要被載入到記憶體中的地址(虛擬記憶體),有了起始地址,才能說載入到記憶體,不然都不知道載入到哪裡,何來的執行呢?這就是可執行檔案與可重定位檔案一個區別
  • 可執行檔案中的各個符號,都有了正確的地址,以及符號被引用的地方也正確填上了符號的地址

以上內容,說的很簡單,如果不懂,參考《程式設計師的自我修養》與《深入理解計算機系統》第7章

2、通過程式碼分析,具體瞭解連結器的作用

連結器的作用簡單的說就是:

  • 符號解析
  • 重定位

下面我們以具體的程式例子來說明:

test.c

#include <stdio.h>

int g_global = 0;
int g_test = 1;

extern int* g_pointer;
extern void func();

int main(int argc, char *argv[])
{
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&g_pointer = %p\n", &g_pointer);
    printf("g_pointer = %p\n", g_pointer);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
    
    func();
	
    return 0;
}

func.c

#include <stdio.h>

int* g_pointer;

void func()
{
    g_pointer = (int*)"D.T.Software";

    return;
}

對上述兩個源程式進行編譯生成兩個可重定位檔案:

  • gcc -c func.c -o func.o
  • gcc -c test.c -o test.o

生成了可可重定位檔案func.o與test.o

  1. 我們使用上幾篇文章的學習的Binutils輔助工具來檢視這兩個可重定位檔案的符號資訊:
  • nm func.o
    在這裡插入圖片描述

  • nm test.o
    在這裡插入圖片描述

可以看到,在test.o與func.o中,各個符號的地址都是0,而且有的符號還是未定義的。地址為0是因為,在沒有連結之前,各個可重定位檔案是獨立的,他們無法載入到記憶體中去執行,各個符號還沒有進行重定位。而又的符號未定義是因為該檔案中引用了外部檔案的程式碼或者資料。比如上述程式碼test.c程式中引用了func.c程式中的g_pointer變數與func()函式,那麼在test.c程式中他們就是未定義的,需要將test.o與func.o連結,才能使整個程式是完整的。

  1. 還可以檢視他們的段資訊
  • objdump -h test.o
    在這裡插入圖片描述

  • objdump -h func.o
    在這裡插入圖片描述

可以看到,各個段的地址都是0(VMA與LMA),所以這種可重定位檔案是不可執行的,它連載入地址都沒有怎麼執行???

最後我們將上述兩個可重定位檔案進行連結生成可執行檔案,看看可執行檔案裡面是什麼樣子的?

  • gcc func.o test.o -o lyy

生成了可執行檔案lyy

執行可執行檔案:

  • ./lyy
    在這裡插入圖片描述
  1. 現在檢視可執行檔案lyy的符號資訊
  • nm lyy
    在這裡插入圖片描述

看到畫紅框的地方,是我們程式中有的,左邊的地址都是各個符號的地址,此時不為0了,每個符號都有自己的載入地址。其他多餘不認識的符號,我們再後面的文章會進行講解。

  1. 檢視可執行檔案lyy的段資訊,看看它與可重定位檔案有什麼區別
  • objdump -h lyy

在這裡插入圖片描述

以上圖片顯示的不全!

可以看出,可執行檔案的各個段,也都有了載入地址。那麼他就可以載入到記憶體中進行執行了。

上面我們沒有分析符號的引用。由於分析符號的引用需要檢視反彙編程式碼,這裡反彙編程式碼太長了,就不貼了。直接說原理。

實際上經過連結後,可執行檔案中,對於符號的引用,已經可以將正確的符號地址填寫到符號引用處(因為符號經過重定位已經有了執行時的地址,將這個地址填寫到它被引用的地方即可)。當符號引用處是正確的符號地址,在執行時,引用才能夠正確得到結果。

3、連結器的意義

從上面的實驗,大致可以理解連結器的意義:

連結器的主要作用是將各個可重定位目標模組之間的引用部分處理好,使得各個模組之間可以正確銜接。類似於下圖:
在這裡插入圖片描述

連結器的工作內容:

  • 將目標檔案(可重定位檔案)與庫檔案整合成最終的可執行檔案

    • 合併各個目標檔案的段(.text .data .bss等)以及使得符號與付哈引用之間進行一個關聯—符號解析
    • 確定各個段以及各個段中符號的最終地址—重定位

4、總結

上述內容,不夠系統,也不夠細緻,只是從實驗的角度來具體看可執行檔案與可重定位檔案,然後理解連結器到底做了什麼。前提是你已經理解了一些基礎的知識。如果不理解,還需要回頭去看看編譯連結過程中的基礎知識。

本文參考狄泰軟體學院相關課程
想學習的可以加狄泰軟體學院群,
群聊號碼:199546072

學習探討加個人(可以免費幫忙下載CSDN資源):
qq:1126137994
微信:liu1126137994