在計算機編程的早期年代,你面臨一個揮之不去的的噩夢。。。
你找了一個剛剛運行成功的程序仔細看了看:
; main.asm - 主程序 start: ; 初始化 mov cx, 10 mov dx, 20 ; 調用math.asm中的add函數 call 0x1234 ; 這里的0x1234是add函數在內存中的絕對地址 ; 退出 mov ax, 0 int 21h
; math.asm - 數學函數模塊 add: ; 加法函數 mov ax, cx add ax, dx ret
你一眼就注意到main.asm
中的那些數字了,0x1234
和0x5678
。
這些是函數在最終內存中的絕對地址,也是所有程序員的噩夢,因為這些地址都是程序員手動計算出來的!
例如,如果math.asm
被加載到內存地址0x1000
,而add
函數在模塊內的偏移是0x234
,那么add
的絕對地址就是0x1234
。
這個過程不僅繁瑣,而且極易出錯,更糟糕的是維護問題。
牽一發而動全身
你清楚地知道,如果程序員在math.asm
的開頭添加了一個新函數,會發生什么!
; math.asm - 修改后 new_function: ; 新增的函數 ; 一些代碼 ret add: ; 位置改變了! mov ax, cx add ax, dx ret
這個看似無害的修改會導致add
函數的位置發生變化!它的偏移量增加了,絕對地址也隨之改變。現在,main.asm
中的call 0x1234
指令將跳轉到錯誤的位置!
程序員必須重新計算add
函數的新地址并修改所有調用add
的地方。
如果程序有數十個模塊,數百個函數調用,這個過程將變成一場噩夢,每次修改代碼,都可能引發一連串的地址更新工作。
于是你的開始思索,需要一種機制,能夠自動處理這些地址綁定,讓程序員們專注于代碼邏輯而非地址計算,為實現這種機制就決不能在程序中使用絕對內存地址!
窮則思變
不使用內存地址使用啥呢?
此時你想到當你找喊一個人的時候直呼其名而不是喊這個人的經緯度坐標,對了!這里也可以使用名字而不是地址來引用函數和變量,想到這里符號(Symbol)概念誕生了。
是啊,為啥要用內存地址硬編碼,程序員可以使用符號啊:
; main.asm - 使用符號名 start: ; 初始化 mov cx, 10 mov dx, 20 ; 使用符號名而非硬編碼地址 call add ; 使用符號名"add"而非0x1234 ; 退出 mov ax, 0 int 21h
這種方法的核心思想是:程序員只需關心名字(如add
、print
),而不必關心這些函數最終在內存中的確切位置。
這是一個巨大的抽象飛躍!
你設計的符號概念帶來了2個關鍵優勢:
減少錯誤:不再需要手動計算和更新地址,消除了一大類潛在錯誤。
簡化維護:當函數位置變化時,只需保持符號名不變,調用代碼無需修改。
最重要的是,符號為自動化解決依賴關系奠定了基礎。
遺留問題
符號概念是很優雅,但問題是:如何確定符號名最終的內存地址呢?
顯然這次需要有一個能夠自動確定符號最終內存地址的工具,讓程序員徹底擺脫地址計算的負擔,到底該怎么做到呢?
要達到這個目的就不能讓編譯器直接生成機器碼,而是把這個過程拆成兩步:
編譯器處理各個模塊,但不必關心跨模塊引用
根據各個模塊提供的信息來確定符號最終的內存地址并合并所有的模塊為一個最終可執行文件
就這樣在你的設想中你把整個編程過程拆成了兩步,第一步是編譯、第二步你將其稱之為鏈接,link。
第二步中各個模塊提供的信息還是比較模糊,這個信息是什么,該怎么提供?
目標文件的誕生
既然編譯器不直接生成最終的機器碼,那么就需要一種文件來承接這一階段編譯器的輸出,這個用來記錄編譯器第一階段輸出的文件就是所謂的目標文件,Object File。
這個文件包含機器碼,但不去確定引用的外部符號的內存地址:
call print
你把所有這樣的符號收集起來記錄來目標文件中,這就是所謂的重定位表(Relocation Table),標記代碼中需要在鏈接時填充正確地址的位置,這就是所謂的重新定位,重定位。
同時這個文件記錄模塊定義的所有符號(函數、變量)及其相對位置,這就是所謂符號表(Symbol Table):記錄模塊定義的所有符號(函數、變量)及其相對位置。
它們可能長這樣:
-- main.obj -- 代碼段: 偏移 0x03: mov dx, 20 偏移 0x06: call ??? (需要重定位,調用add) 偏移 0x0B: mov bx, ax 偏移 0x0D: call ??? (需要重定位,調用print) 符號表: start -> 偏移 0x00 (本模塊定義,"我能提供什么") 重定位表: 偏移 0x07: 需要add的地址 偏移 0x0E: 需要print的地址 未解析引用: add (外部符號,“我需要什么”) print (外部符號,“我需要什么”)
目標文件的意義目標文件的出現是一個關鍵突破,因為它:
分離了編譯和鏈接:編譯器只需關注單個模塊的翻譯,不必處理跨模塊引用。
明確記錄了依賴關系:每個模塊清楚地表達了"我提供什么"(符號表)和"我需要什么"(未解析引用)。
為自動化鏈接提供了數據結構:重定位表明確標記了需要修正的地址位置。
現在, 你的任務就變得明確了:讀取多個目標文件,解析它們的符號和依賴關系,然后將它們正確地"鏈接"在一起。
但如何實現這個鏈接過程?很明顯,你需要實現兩個核心算法:符號解析和重定位。
符號解析與重定位
符號解析解決一個基本問題:將每個模塊的"需求"與其他模塊的"供給"匹配起來。
具體來說,你需要:
收集所有符號:遍歷每個目標文件的符號表,建立一個全局符號字典,記錄每個符號的定義位置。
檢查未解析引用:對每個模塊的未解析引用,在全局符號字典中查找其定義。
處理沖突和錯誤:如果一個符號有多個定義(沖突)或沒有定義(未解析),生成適當的錯誤信息。
如果所有未解析引用都能在全局符號表中找到對應的定義,符號解析就成功了。否則,你的算法會生成一個錯誤,這就是后來的程序員熟悉的"undefined reference to..."。
符號解析解決了"符號供需匹配"問題,重定位的任務是:確定每個模塊和符號在最終內存中的確切位置。
重定位過程包括:
內存布局規劃:決定各個模塊在最終內存空間中的排列順序和基址。
地址計算:根據模塊基址和符號在模塊內的偏移,計算每個符號的最終絕對地址。
填充重定位條目:遍歷每個模塊的重定位表,將正確的地址填充到代碼中的相應位置。
符號解析和重定位這兩個步驟解決了模塊化編程中最核心的問題:如何讓分散在不同文件中的代碼片段正確地找到并調用彼此。
鏈接器的誕生
至此,這兩個核心算法的實現徹底解放了程序員,讓他們不再需要手動計算和修改地址。
來源:碼農的荒島求生
編輯:未
轉載內容僅代表作者觀點
不代表中科院物理所立場
如需轉載請聯系原公眾號
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.