連接器規則會引入的巨坑

    首先來看一個簡單的程序。下面是是兩段程序,分別放在link.cbar.c中。函數

/* link.c */
#include<stdio.h>
void f(void);

int x = 13;

int main() {
	f();
	printf("x=%d\n", x);
	return 0;
}


/* bar.c */
int x;

void f() {
	x = 12;
}
複製代碼

    如今咱們使用命令gcc -o linkbar link.c bar.c將其編譯爲可執行文件,而後用./linkbar運行文件,運行結果爲:x=12微服務

    有趣的事情就發生了,在link.c中分明定義的是x = 13,並且打印語句使用的也是本身模塊的x變量,怎麼就變成了 12 了呢?ui

    那問題是否是就出在bar.c文件中呢?這個文件中也定義了x變量,而且在f()函數中將其賦值了,可是這個x是在bar.c中,link.c中也有本身的x變量,按理來講它們應該是相互不影響的,讓人疑惑!!!!spa

    實際上這都是連接器搞的鬼,上面場景在工做中遇到的可能性不小,這種錯誤引入程序後,並不會當即表現出來,而是可能在其它你想不到的地方報錯,試想一下,在一個擁有成百上千個模塊的大型系統中,發生了這樣的錯誤,而你也不知道錯誤的源頭,讓你定位出這個錯誤,其困難程度可想而知。code

    要理清這個問題,須要去了解連接器是怎麼工做的。咱們都知道,如今的系統愈來愈大,咱們將其分解成爲更小的、更好管理的模塊,能夠獨立地修改和編譯這些模塊(像不像微服務?),這樣協做讓咱們沒必要將整個應用程序組織成一個巨大的源文件。內存

    爲了構造可執行文件,連接器須要完成符號解析重定位兩個主要任務,這裏咱們主要看看符號解析。string

    每一個符號都對應一個函數、一個全局變量或一個靜態變量(C 中以static聲明的變量),符號解析就是要把每一個符號引用與符號定義關聯起來,注意不包括局部變量哦。it

    C 語言中 static 聲明的變量和 Java、C++ 中的 private 聲明同樣,不帶 static 的就是 public 類型的。io

    你確定據說過可重定位這個詞,源程序在通過預處理、編譯、彙編以後產生的就是可重定位目標文件,怎麼理解可重定位呢?簡單來講,就是說文件裏面的代碼段和數據的地址尚未最終肯定。編譯

    在每一個可重定位目標文件中都有一個符號表,這個符號表包含了可重定位目標文件本身定義和引用的符號信息。共有三種不一樣的符號:一、由本身定義並能被其它模塊引用的全局符號;二、由其它模塊定義並被本身引用的全局符號;三、只能被本身引用的局部符號。

    連接器解析符號引用是將每一個引用與它的輸入的可重定位目標文件的符號表中的一個肯定的符號定義關聯起來,可是不一樣的可重定位目標文件可能有多個同名的全局符號,即多重定義的全局符號。

    函數和已經初始化的全局變量是強符號,未初始化的全局變量是弱符號。根據強弱符號的定義,Linux 連接器使用三條規則來處理多重定義的符號。

  1. 不容許有多個同名的強符號;
  2. 若是有多個弱符號和一個強符號同名,那麼選擇強符號;
  3. 若是有多個弱符號同名,那麼選擇其中任意一個。

    看到這裏,就明白爲何會有開篇程序出現的那個錯誤了,由於它正好知足規則二,因此bar.c中的x變量實際上仍是link.c中的x變量。

    尤爲規則 2 和 3 的應用會帶來一些不易察覺的運行時錯誤,這是很是難理解的,尤爲是重複的符號中還有不一樣的類型時,好比下面這個例子。

/* link.c */
#include<stdio.h>
void f(void);

int y = 12;
int x = 13;

int main() {
    f();
    printf("x=0x%x y=0x%x \n", x, y);
    return 0;
}


/* bar.c */
double x;

void f() {
    x = -0.0;
}
複製代碼

    在一臺 x86-64/Linux 機器上,double類型是 8 個字節,而int類型是 4 個字節,假設系統中x的地址是0x601020y的地址是0x601024,而bar.c的賦值x = -0.0;將會用負零的雙精度浮點表示覆蓋內存中xy的位置。

相關文章
相關標籤/搜索