鏈接(Linking)就是通過符號將各個模塊組合成一個獨立的程序的過程。
鏈接的主要內容就是把各個模塊之間的相互引用部分處理好,使得各個模塊能夠正確地銜接。鏈接器所做的主要工作跟前面提到的“人工調整地址”本質上沒有什么兩樣,只不過現代的高級語言擁有諸多的特性,使得編譯器和鏈接器更為復雜,功能更為強大,但從原理上來講,無非是找到符號的地址,或者把指令中使用到的地址加以修正。這個過程稱為符號決議(Symbol Resolution)或者重定位(Relocation)。
對于簡單的C語言程序,鏈接過程如下圖所示。每個模塊的源文件(.c 和 .h)先被編譯成目標文件,再和系統庫一起鏈接成可執行文件。庫(Library)其實是一組目標文件的包,是將一些最常用的代碼編譯成目標文件后打包存放。

系統庫這個概念比較模糊,專業一點應該叫做運行時庫(Runtime Library)。“運行時”就是程序運行期間,“運行時庫”包含了程序運行期間所需要的基本函數,是程序運行不可或缺的,例如輸入輸出函數 printf()、scanf(),內存管理函數 malloc()、free() 等。
鏈接過程并沒有想象中的復雜,它還是一個比較容易理解的概念。
假設一個程序有兩個模塊 main.c 和 module.c,我們在 module.c 中定義了函數 func(),并在 main.c 中進行了多次調用,當所有模塊被編譯成一個可執行文件后,每一處對 func() 函數的調用都會被替換為一個絕對地址。但由于每個模塊都是單獨編譯的,編譯器在處理 main.c 時并不知道 func() 的地址,所以需要把這些調用 func() 的指令的目標地址擱置,等到最后鏈接的時候再由鏈接器將這些地址修正。
如果沒有鏈接器,我們必須手工修正 func() 的地址。當 module.c 被修改并重新編譯時,func() 的地址極有可能改變,那么在 main.c 中所有使用到 func() 函數的地方,都要全部重新調整地址。這些繁瑣的工作將成為程序員的噩夢。
有了鏈接器,我們可以直接調用其他模塊中的函數而無需知道它們的地址,因為在鏈接的時候,鏈接器會根據符號 func 自動去 module.c 模塊查找 func 的地址,然后將 main.c 模塊中所有使用到 func 的指令重新修正,讓它們的目標地址成為真正的 func() 函數的地址。
這種在程序運行之前確定符號地址的過程叫做靜態鏈接(Static Linking);如果需要等到程序運行期間再確定符號地址,就叫做動態鏈接(Dynamic Linking)。
Windows 下的 .dll 或者 Linux 下的 .so 必須要嵌入到可執行程序、作為可執行程序的一部分運行,它們所包含的符號的地址就是在程序運行期間確定的,所以稱為動態鏈接庫(Dynamic Linking Library)。
變量和函數一樣,都是符號,都需要確定它的地址。例如在 a.c 中有一個 int 類型的全局變量 var,現在需要在 b.c 中對它賦值 42,對應的C語言代碼是:
~~~
var = 100;
~~~
對應的匯編代碼為:
~~~
mov 0x2a, var
~~~
mov 用來將一份數據移動到一個存儲位置,這里表示將 0x2a 移動到 var 符號所代表的位置,也就是對 var 變量賦值。
當被編譯成目標文件后,得到如下的機器指令:
~~~
c705 ?00000000 ?0000002a
~~~
由于在編譯時不知道變量 var 的地址,編譯器將這條 mov 指令的目標地址設置為 0,等到將目標文件 a.o 和 b.o 鏈接起來的時候,再由鏈接器對其進行修正。
假設生成可執行文件后變量 var 的地址為 0x1100,那么上面的機器指令就變為:
~~~
c705 ?00001100 ?0000002a
~~~
這種地址修正的過程就是前面提到的重定位,每個需要被修正的地方叫做一個重定位入口(Relocation Entry)。重定位所做的工作就是給程序中每個這樣的絕對地址引用的位置“打補丁”,使它們指向正確的地址。
# 符號的概念
函數和變量在本質上是一樣的,都是地址的助記符,在鏈接過程中,它們被稱為符號(Symbol)。鏈接器的一個重要任務就是找到符號的地址,并對每個重定位入口進行修正。
我們可以將符號看做是鏈接中的粘合劑,整個鏈接過程正是基于符號才能正確完成。
目標文件被分成了多個部分,其中有一個叫做符號表(Symbol Value),它的段名是`.symtab`。符號表記錄了當前目標文件用到的所有符號,包括:
1. 全局符號,也就是函數和全局變量,它們可以被其他目標文件引用。
2. 外部符號(External Symbol),也就是在當前文件中使用到、卻沒有在當前文件中定義的全局符號。
3. 局部符號,也就是局部變量。它們只在函數內部可見,對鏈接過程沒有作用,所以鏈接器往往也忽略它們。
4. 段名,這種符號往往由編譯器產生,它的值就是該段的起始地址,比如`.text`、`.data`等。
對鏈接來說,最值得關注的是全局符號,也就是上面的第一類和第二類,其它符號都是次要的。
所有的符號都保存在符號表`.symtab`中,它一個結構體數組,每個數組元素都包含了一個符號的信息,包括符號名、符號在段中的偏移、符號大小(符號所占用的字節數)、符號類型等。
> 確切地說,真正的符號名字是保存在字符串表`.strtab`中的,符號表僅僅保存了當前符號在字符串表中的偏移。
# 符號決議(Symbol Resolution)
當要進行鏈接時,鏈接器首先掃描所有的目標文件,獲得各個段的長度、屬性、位置等信息,并將目標文件中的所有(符號表中的)符號收集起來,統一放到一個全局符號表。
在這一步中,鏈接器會將目標文件中的各個段合并到可執行文件,并計算出合并后的各個段的長度、位置、虛擬地址等。
在目標文件的符號表中,保存了各個符號在段內的偏移,生成可執行文件后,原來各個段(Section)起始位置的虛擬地址就確定了下來,這樣,使用起始地址加上偏移量就能夠得到符號的地址(在進程中的虛擬地址)。
這種計算符號地址的過程被稱為符號決議(Symbol Resolution)。
重定位表`.rel.text`和`.rel.data`中保存了需要重定位的全局符號以及重定位入口,完成了符號決議,鏈接器會根據重定位表調整代碼中的地址,使它指向正確的內存位置。
至此,可執行文件就生成了,鏈接器完成了它的使命
# 全局變量和局部變量
當程序被加載到內存后,全局變量要在數據區(全局數據區)分配內存,局部變量要在棧上分配內存
數據區在程序運行期間一直存在,全局變量的位置不會改變,地址也是固定的,所以在鏈接時就能夠計算出全局變量的地址。而棧區內存會隨著函數的調用不斷被分配和釋放,局部變量的地址不能預先計算,必須等到發生函數調用時才能確定,所以鏈接過程會忽略局部變量。
關于局部變量的定位,就是 ebp 加上偏移量,這在編譯階段就能給出計算公式(一條簡單的語句),程序運行后,只要執行這條語句,就能夠得到局部變量的地址。
總結起來,鏈接的一項重要任務就是確定函數和全局變量的地址,并對每一個重定位入口進行修正
- c語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介