【计算机系统基础】符号表、符号解析(详解)
一、符號、符號表
符號:通俗地說,就是前面跟著類型(如int/void等)的函數名,或變量名。
????????? 注意:這里的變量必須是除了非靜態局部變量之外的其它變量。
所有符號載入史冊——符號表。
分類:
①Global symbols(模塊內部定義的全局符號,又稱全局符號) – 由模塊m定義并能被其他模塊引用的全局符號。
例如,非static函數和非static的全局變量(指不帶static的全局變量)
如,main.c 中的全局變量名buf
②External symbols(外部定義的全局符號,又稱外部符號) – 由其他模塊定義并被模塊m引用的全局符號(標志是extern,extern用來修飾全局變量,即聲明在“最外層”)(要體現引用,否則不會進入符號表)
如,main.c 中的函數名swap是一個外部符號。
i.它是一個聲明,但是也僅僅是告訴鏈接器,這個swap實際上來自外部。
ii.還必須在模塊m中引用,該符號才能出現在符號表里。對swap的引用,則體現在main函數中的swap();中。
當然,考試不會給出只定義/聲明,但后面沒有引用的例子。故了解即可。
【實際上,main里不寫extern都可以,它是個弱符號,后續鏈接會自動把在main里寫到的void swap的真正定義認為在swap.o中。只不過做題時,我們把extern int a這樣的a稱作外部符號(在外面有定義),int a這樣的a稱作全局符號(其在.bss)】
?Local symbols(本模塊的局部符號,又稱本地符號) – 僅由模塊m定義并能被本模塊引用的本地符號。例如,在模塊m中定義的帶static的函數和變量(無論定義變量的位置,是全局還是在函數中,只要變量前面有static,都算是(本地)符號!)
如,swap.c 中的static變量名bufp1
【注】不關心局部變量(當然,也不叫符號)。
如果函數的聲明中帶有關鍵字extern,則暗示這個函數可能在別的模塊里定義,在此處只是一個聲明而非定義(如下區分)。extern...可簡單理解為是引用其他模塊定義過的東西,其變量(函數)名屬于外部符號。它在當前文件的符號表中,在真正定義模塊的.data或.bss節中。
int a; // 定義一個變量, 不初始化int b = 10; // 定義一個變量, 同時進行初始化extern int c; // 聲明一個外部extern的int型變量avoid swap(); //函數的聲明【而非定義】void swap(){ //函數的定義......}【做題總結】找符號,就找前面帶類型的&&(聲明在最外層的 || 前面帶extern的 || 前面帶static的)
如圖:紅為內部(全局)符號,藍為外部(全局)符號,綠為本地符號。
【符號表】
如圖:符號的偏移量或虛擬地址一目了然!
st_size可能是函數(代碼、指令占的字節數)的字節數,也可能是變量所占的字節數,視情況而定。
st_info比較重要,包含的信息很多!
【例如】
main.o
?本圖中,swap是外部函數,它的信息許多未知
在swap.o中
同樣,buf的相關信息,在swap.o中也是未知信息。而且,bufp1是未初始化變量,放在bss中(符號表中描述bufp1滿足4字節對齊,占4個字節,可以理解為.o文件鏈接后映射到存儲器(實則為主存)中,bufp1是4字節對齊,占4個字節)。
二、符號解析【重點!!!】
1、符號定義的實質:為函數名時,指代碼所在區;為變量名時,指所占的靜態數據區。
2、全局符號的解析比較復雜,內部符號解析較簡單。所有定義符號的值,是其目標所在的首地址。
3、強符號/弱符號
接下來所說的全局變量/函數名包括:外部符號(即外部定義的全局符號)和全局符號(即模塊內部定義的全局符號)
已初始化的全局變量名、全局函數名是強符號
未初始化的全局變量名和全局函數名【即光聲明不寫具體指令的函數(如前面的extern void swap();中的swap)】,是弱符號
【易錯】本地(static)符號不參與強弱討論。函數的“引用”不算強/弱符號。非靜態局部變量也談不上強/弱符號。
【練習】
p1中的var是強符號,p2中的var是弱符號。
【練習】指出強符號和弱符號
紅框里面是強符號,藍框里面是弱符號。
【做題總結】找強/弱符號,就找帶類型的&&(聲明在最外層的||前面帶extern的)[注意:不包括前面帶static的];然后看是否初始化 值 或 代碼!
解析有如下規則:
Rule 1: 強符號不能多次定義
– 強符號只能被定義一次,否則鏈接錯誤
Rule 2: 若一個符號被定義為一次強符號和多次弱符號,則按強定義為準
– 對弱符號的引用被解析為其強定義符號
Rule 3: 若有多個弱符號定義,則任選其中一個(一般選第一個)
– 使用命令 gcc –fno-common鏈接時,會告訴鏈接器在遇 到多個弱定義的全局符號時輸出一條警告信息。
【做題總結】找鏈接后符號的真正定義處,若是本地符號,則定義就在本地;若是全局符號(內部/外部),則根據上述規則找真正定義處。
如本題:紅色是強符號,藍色是弱符號。
y一次強定義,一次弱定義
z兩次弱定義
p1一次強定義,一次弱定義
main一次強定義
沒有兩次強定義,因此:鏈接不會報錯
但是,y和z的輸出結果到底是誰?
p1.c中的p1函數,會將左端的y符號賦值為200(雖然y是用的main.c的強定義)
同理,z最后也會因為經過p1()函數,而變成2000.
【典例】
d最終輸出結果如何?
單純編譯p1.c的時候,會認為把1.0給double d(匯編也是浮點指令)。但最終鏈接后,1.0作為浮點數,要放到int d中,因為main.c中d是強符號。具體如下:
d(1.0)的double浮點數為3FF0 0000 0000 0000H,它會把原來int d空間里的數據全部沖掉,換成0000 0000,把原來int x里的數據全部沖掉,變成3FF0 0000,為一個很大的數。如果在main.c中還定義了一個如short f的在.bss的變量,則無需考慮它。.data和.bss雖然說是連續的兩個空間,且.data在低地址,.bss在高地址。但由于.bss的對齊原因,兩者之間有很大的空隙,不會沖刷掉.bss空間的內容。
因此,鏈接可以通過是真的,但是程序結果出錯了。
【規律】待用以賦值的數到底是什么類型的機器數,由本.c來決定(如double),因為這是在匯編及之前就完成的。而真正賦值給的對象,是強符號(同一名稱的符號若都是弱符號,則任選其一)。
注意區分:
整個存儲器鏡像(右圖)從低地址到高地址,是從下往上的(和棧幀一樣)。因此,我們在畫d和x的內容的時候,從下往上也應該是從低地址到高地址。對于.data節而言,先聲明的變量占據低地址空間。
【經驗教訓】別用全局變量,用的時候也要賦初值!多用本地變量(static)。
三、庫
1、避免所有函數在一個源文件中,也不要一個源文件只包括一個函數。要雨露均沾。
2、靜態庫 (.a archive files)
– 將所有相關的目標模塊(.o)打包為一個單獨的庫文件(.a)【包含了許多.o文件】,稱為靜態庫文件 ,也稱存檔文件(archive)
– 在構建可執行文件時只需指定庫文件名,鏈接器會自動到庫中尋找那些應用程序用到的目標模塊,并只把用 到的模塊從庫中拷貝出來
– 在gcc命令行中無需明顯指定C標準庫libc.a(默認庫)
如果要修改一個.c文件,只需要將對應新生成的.o文件覆蓋到.a中,非常方便。
例子:
ar是一個歸檔程序。
$ ar rcs libc.a atoi.o printf.o … random.o
這樣,就可以生成名為libc.a的靜態庫
然后,如$ gcc –static –o myproc main.o ./mylib.a,則就可以使用(假設自定義的靜態庫是)mylib.a了!
【注意】在鏈接的時候,無需特別地指出libc.a的標準庫,鏈接器可以自動查詢。
3、若干集合
E 將被合并到一起以組成可執行文件的所有目標文件集合(可執行文件+靜態庫文件)
U 未解析符號(未不對應定義符號關聯的的引用符號)的集合 (符號)
比如說:在一個.o文件用了max(a,b),但是卻沒有這個max符號,這個max現在就稱作未解析符號。
D 當前已被加入到E的所有目標文件中定義符號的集合(定義符號)
開始E、U、D為空,符號解析過程如下:(掃描文件的順序:按gcc指令的順序掃描)
①命令行中文件按照順序出現,假如現在是f文件,鏈接器看它是什么文件:
是可重定位目標文件,就將f放到E中,并將f中未解析符號放到U,定義符號放到D。
是靜態庫文件:鏈接器嘗試把U中所有未解析符號與f中各個目標模塊中的定義的符號匹配。如果f中的某個m模塊定義了U中的未解析符號x,就將:m放入E,x從U移到D,直到U和D不再變化。又假設f中的模塊n,里面的定義符號都沒在鏈接中用到,那么n就被扔了!(隨掃隨丟)庫文件里也有可能存在那種未解析符號,那么就把靜態庫里的未解析符號也放到U里去。
如圖例,處理main.o的時候,先把這個模塊放到E中。發現d是定義符號,那么d就加入D,而main.c第8行的d已經被定義了,所以d不會被放到U里面。
②若往D中加入了一個已經存在的符號(雙重定義),或掃描完所有文件時U非空,則連接器報錯并停止。否則,鏈接器將生成.o文件,最終U中一定為空,D中符號唯一。
③最終還會自動檢索默認庫,也是匹配符號的過程。它不需要在 gcc -static 后明顯指出。
【注意】鏈接器對外部引用的解析
①順序掃描.o和.a,一般把靜態庫放到后面。如果靜態庫之間用相互引用關系,則必須按照引用關系在命令行中排列靜態庫文件,使得:對每個靜態庫目標模塊中的外部引用(定義)的符號,包含其定義的靜態庫文件排在其后面。【每個靜態庫中,可能有外引符號,因此,為方便理解:可以把靜態庫中用到的模塊,當成.o文件,理解成庫內模塊中的未定義符號也放到U。】
我們的目標是:每一個找不到家的孩子找到家,每一個未關聯符號U都找到它的定義!
②最終U中一定是空集,D每個符號都是唯一的!
假設調用關系如下:
?func.o → libx.a 和 liby.a 中的函數
?libx.a → libz.a 中的函數
?libx.a 和 liby.a 之間、liby.a 和 libz.a 相互獨立
?則以下幾個命令行都是可行的:(共三種)
func.o → libx.a 和 liby.a 中的函數
libx.a → liby.a 同時 liby.a → libx.a
則下列都可以:(共兩種)
– gcc -static –o myfunc func.o libx.a liby.a libx.a – gcc -static –o myfunc func.o liby.a libx.a liby.a【例題】
(1)顯然是$ gcc –static –o myproc p.o libx.a liby.a
(2)y和x庫相互解析,故為$gcc -static -o myproc p.o libx.a liby.a (注意:y里有的符號需要x解析,故繼續)? libx.a
(3)先順著寫:
$gcc -static -o myproc p.o libx.a liby.a libz.a
發現y也是調用x,x再調用z
那么直接寫成:$gcc -static -o myproc p.o libx.a liby.a libx.a libz.a
【解讀】總之就是被外引的.a一定要在外引的.a的后面!
總結
以上是生活随笔為你收集整理的【计算机系统基础】符号表、符号解析(详解)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 记录一次网件r6300v2 wan口失效
- 下一篇: Java+spring 基于ssm的健康