[上一篇 linux g++ 鏈接器] (/content/10953638.html)
由編譯源代碼生成了包含機器指令的文件,我們稱之為目標文件。
源代碼中的變量或者函數,我們稱之為符號。
通常情況下,我們不會把所有工作都寫在一個源代碼文件上,而是分成多個文件。
既然分成多個文件,就會這樣的情況:一個文件裡要使用的某個符號實際上是在另一個文件裡定義的。這種關系,我們稱之為引用。
那麼鏈接器要做的事情,就是處理好這些引用關系,使得當一個目標文件想要引用另一個目標文件中的符號時,它能順利地找到並使用。
當所有符號都能正確引用時,程序就可以正確地執行了。
因此,關於鏈接器的概念,記住三個關鍵字:
目標文件、
符號、
引用。
舉個例子:
文件一:main.cpp
[code]#include <iostream>
using namespace std;
void myfun();
int main()
{
myfun();
return 0;
}
文件二:a.cpp
[code]void myfun()
{
cout<<"myfun in a.cpp"<<endl;
}
在這個例子中,main.cpp和a.cpp分別編譯成目標文件main.o和a.o。
myfun
和
main
都是符號。
main.cpp中調用了myfun(),這裡的myfun()與a.cpp中myfun()的關系就是引用關系。
目標文件
上文的例子上,main.o和a.o都是目標文件,但目標文件不只有這一種。
可執行文件其實也算是一種目標文件。
但鏈接器所接受的目標文件有三種:可重定位目標文件(
.o,
.a),共享目標文件(
.so)。其中.a文件可以看作是若干個.o文件的打包。
| .o | .a | .so | 可重定位目標文件多個可重定位目標文件共享目標文件一本書一疊書一本厚書普通目標文件靜態庫動態庫不管是可重定位目標文件、共享目標文件還是可執行文件,所有的目標文件都有著相似的格式 — ELF。
表中列出了主要內容。但目前,我們只需要知道ELF主要分為兩部分,分別是指令部分(紅色)和數據部分(綠色)。
符號
符號是指源代碼中的變量和函數。
符號也是分多種的。鏈接器並不是對所有符號都關心。它只關心其中幾種。讓我們一一分析:
[code]extern int buf[];
static int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
這段代碼中有五個符號:buf, bufp0, bufp1, swap, temp
根據符號的作用域,可以把符號分為這四類:
局部符號
例如temp,是在函數內定義的非static符號。這類符號是在棧裡管理的,因此鏈接器對局部符號一點都不關心。
靜態符號
例如bufp0,是有static關鍵籽修飾的符號。它在本文件內定義,也只在本文件內使用。
鏈接器對靜態符號的處理比較簡單,只要保證模塊中一個符號只有一個定義就好了。
外部符號
例如buf,它是由其它模塊定義,但被本模塊引用了的符號。有extern修飾
其它模塊的可引出符號,如果被本模塊使用了,就是本模塊的外部符號
編譯器發現模塊中的符號沒有定義時,就會從別的模塊中找到它的定義,並產生正確的引用。
可引出符號/全局符號
例如bufp1和swap,是由本模塊定義,且能被本模塊和其它模塊引用的符號。
非Static全局函數,非static全局變量都是可引出符號。
鏈接器對可引出符號的處理比較復雜。它需要統一管理這些符號:(1)處理重定義問題(2)當某個模塊需要使用這個符號時能夠搜索到(3)向使用者提供符號的地址,以產生正確的引用。
總結:
| 符號類型 | 符號舉例 | owner | 作用域 | 鏈接器的處理方法 | 局部符號temp函數內定義函數內使用不關心靜態符號bufp0文件內定義文件內使用處理文件內重定義外部符號buf其它目標文件內定義本目標文件內使用(1)找到符號在其它文件的定義
(2)確定符號地址
(3)產生正確引用可引出符號/全局符號bufp1和swap本目標文件內定義所有參與鏈接的目標文件都可以使用(1)解決重定義問題
(2)當某個模塊需要使用這個符號時能夠搜索到
向使用者提供符號的地址
引用
鏈接器主要需要處理的是本文件內的外部符號到其它文件的全局符號之間的引用。
不管輸入的目標文件都是.o,.a還是.so,互相之間的引用都能正常進行,才能產生可執行文件。
本文接下來的篇幅都用來介紹不同目標文件之間是如何產生正確的引用的。