符號解析
連結器解析符號引用的方法是將每個引用與它輸入的可重新導向目標檔案的符號表中的一個確定的符號定義聯絡起來。對於那些和引用定義在相同模組中的本地符號的引用,符號解析是非常簡單明了的。編譯器只允許每個模組中每個本地符號只有一個定義。編譯器還確保靜態本地變數,它們也會有本地連結器符號,擁有唯一的名字。
不過對全域符號的解析就棘手多了。當編譯器遇到一個不是在當前模組中定義的符號(變數或符號)時,它會假設符號是在其他某個模組中定義的,生產一個連結器符號表條目,並把它交給連結器處理。如果連結器在它的任何輸入模組中都找不到這個引用的符號,它就會輸出一條(通常很難閱讀的)錯誤資訊並終止。比如,如果我們試著在一台Linux機器上編譯和連結下面的源檔案:
voidfoo(void);
intmain(){
foo();
return0;
}
那麼編譯器會沒有障礙的運行,但是連結器無法解析對foo的引用時,它會終止:
unix>gcc -Wall -o2 -o linkerrorlinkerror.c
/tmp/ccSz5uti.o: In function 'main':
/tmp/ccSz5uti.o(.text+0x7):undefinedreference to 'foo'
collect2: ldreturned 1 exit stauts
對全域符號的符號解析很棘手,還因為多個目標檔案可能會定義相同的符合.在這種情況中,連結器必須要麼標誌一個錯誤,要麼以某種方法選出一個定義並拋棄其他定義。Unix系統採納的方法涉及編譯器、彙編器和連結器之間的協作,這樣也可能給不警覺的程式員帶來麻煩。 連結器解析多重定義的全域符號
在編譯時間,編譯器向彙編器輸出的每個全域符號,或者是強或者是弱,而彙編器把這個資訊隱含地編碼在可重定位目標檔案的符號表裡。函數和已初始化的全域變數是強符號,未初始化的全域變數是弱符號。
根據強弱符號的定義,Unix連結器使用下面的規則來處理多重定義的符號:
1)不允許有多個強符號
2)如果有一個強符號和多個弱符號,那麼選擇強符號
3)如果有多個弱符號,那麼從這些弱符號中選擇任意一個
例如,假設我們試圖編譯和連結下面兩個c模組:
int main(){
return0:
} foo1.c
int main(){
return0;
} bar1.c
在這種情況下,連結器將產生一條錯誤資訊,因為強符號main被定義了兩次(規則1):
unix>gcc foo1.c bar1.c
****** :In function 'main':
****** : mutipledefinition of 'main'
****** : firstdefined here
相似的,連結器對於下面的模組也會產生一條錯誤資訊,因為強符號x被定義了兩次(規則2):
int x = 123;
int main(){
return0;
}foo2.c
int x = 123;
void f(){}bar2.c
然而,如果在和一個模組裡x未被初始化,那麼連結器將安靜地選擇定義在另一個模組中的強符號(規則2):
#include <stdio.h>
void f();
int x = 123;
int main(){
f();
printf("x= %d\n",x);
return0;
}foo3.c
int x;
void f(){
x= 124;
}bar3.c
運行時函數f將x的值由123改為124,這會給main函數的作者帶來不受歡迎的意外。注意,連結器通常不會表明它檢測到多個x的定義:
unix> gcc -o foobar3 foo3.c bar3.c
unin>./foobar3
x = 124
如果x有兩個弱定義,也會發生相同的情況(規則3):
#include <stdio.h>
void f();
int x ;
int main(){
x=123;
f();
printf("x= %d\n",x);
return0;
}foo4.c
int x;
void f(){
x = 124;
}bar4.c
規則2和規則3的應用會造成一些不易察覺的執行階段錯誤,對於不警惕的程式員來說,這是很難解釋的,尤其當如果重複的符號定義還有不同的類型時,考慮下面的這個例子,其中x在一個模組中定義為int,在另一個模組中定義為double:
#include <stdio.h>
void f();
int x=15213;
int y = 15213;
int main(){
f();
printf("x= 0x%x y = 0x%x \n",x,y);
return0;
}foo5.c
double x;
void f(){x = -0.0;}bar5.c
在一台IA32/Linux機器上,double類型是8個位元組,而int類型是4個位元組。因此,bar5.c的第6行中的賦值x=-0.0將用負零的雙精確度浮點表示覆蓋儲存空間中的x和y的位置。
linux>gcc -o foobar5 foo5.c bar5.c
linux>./foobar5
x = 0x0 y = 0x8000000
這是一個非常細微而令人討厭的錯誤,尤其是因為它是默默發生的,編譯系統不會給出警告,而且因為通常要在程式執行很久以後才表現出來,且遠離錯誤的發生地。當你懷疑由此錯誤時,用像gcc -fno-common這樣的選項調用連結器,在遇到多重定義的全域符號時,輸出一條警告。