從四個問題透析Linux下C++編譯&鏈接

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"摘要:"},{"type":"text","text":"編譯&鏈接對C&C++程序員既熟悉又陌生,熟悉在於每份代碼都要經歷編譯&鏈接過程,陌生在於大部分人並不會刻意關注編譯&鏈接的原理。本文通過開發過程中碰到的四個典型問題來探索64位linux下C++編譯&鏈接的那些事。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"編譯原理:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將如下最簡單的C++程序(main.cpp)編譯成可執行目標程序,實際上可以分爲四個步驟:預處理、編譯、彙編、鏈接,可以通過"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"g++ main.cpp –v看到詳細的過程,不過現在編譯器已經把預處理和編譯過程合併。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/dd/dde050a3990870307722cab1e8ee3502.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"預處理:"},{"type":"text","text":"g++ -E main.cpp -o main.ii,-E表示只進行預處理。預處理主要是處理各種宏展開;添加行號和文件標識符,爲編譯器產生調試信息提供便利;刪除註釋;保留編譯器用到的編譯器指令等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"編譯:"},{"type":"text","text":"g++ -S main.ii –o main.s,-S表示只編譯。編譯是在預處理文件基礎上經過一系列詞法分析、語法分析及優化後生成彙編代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"彙編:"},{"type":"text","text":"g++ -c main.s –o main.o。彙編是將彙編代碼轉化爲機器可以執行的指令。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"鏈接:"},{"type":"text","text":"g++ main.o。鏈接生成可執行程序,之所以需要鏈接是因爲我們代碼不可能像main.cpp這麼簡單,現代軟件動則成百上千萬行,如果寫在一個main.cpp既不利於分工合作,也無法維護,因此通常是由一堆cpp文件組成,編譯器分別編譯每個cpp,這些cpp裏會引用別的模塊中的函數或全局變量,在編譯單個cpp的時候是沒法知道它們的準確地址,因此在編譯結束後,需要鏈接器將各種還沒有準確地址的符號(函數、變量等)設置爲正確的值,這樣組裝在一起就可以形成一個完整的可執行程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題一:頭文件遮擋"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在編譯過程中最詭異的問題莫過於頭文件遮擋,如下代碼中main.cpp包含頭文件common.h,真正想用的頭文件是圖中最右邊那個包含name"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4e/4edb9df5a086569a77e01cf08cc56c00.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"成員的文件(所在目錄爲./include),但在編譯過程中中間的common.h(所在目錄爲./include1)搶先被發現,導致編譯器報錯:Test結構沒有name成員,對程序員來講,自己明明定義了name成員,居然說沒有name這個成員,如果第一次碰到這種情況可能會懷疑人生。應對這種詭異的問題,我們可以用-E參數看下編譯器預處理後的輸出,如下圖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/dc/dcdeebfa552bcd023e6cd152565be636.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"預處理文件格式如下:# linenum filename flag,表示之後的內容是從文件名爲filaname的文件中第linenum行展開的,flag的取值可以是1,2,3,4,可以是用空格分開的多值,1表示接下來要展開一個新文件;2表示一個文件展開完畢;3表示接下來內容來自一個系統頭文件;4表示接下來的內容應該看做是extern C形式引入的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從展開後的輸出我們可以清楚地看到Test結構確實沒有定義name這個成員,並且Test這個結構是在./include1中的common.h中定義的,到此真相大白,編譯器壓根就沒用我們定義的Test結構,而是被別的同名頭文件截胡了。我們可以通過調整-I或者在頭文件中帶上部分路徑更詳細制定頭文件位置來解決。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"目標文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯鏈接最終會生成各種目標文件,Linux下目標文件格式爲ELF(Executable Linkable Format),詳細定義見/usr/include/elf.h頭文件,常見的目標文件有:可重定位目標文件,也即.o結尾的目標文件,當然靜態庫也歸爲此類;可執行文件,比如默認編譯出的a.out文件;共享目標文件.so;核心轉儲文件,也就是core dump後產出的文件。Linux文件格式可以通過file命令查看。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個典型的ELF文件格式如下圖所示,文件有兩種視角:編譯視角,以section頭部表爲核心組織程序;運行視角,程序頭部表以segment爲核心組織程序。這麼做主要是爲了節約存儲,很多細碎的section在運行時由於對齊要求會導致很大的內存浪費,運行時通常會將權限類似的section組織成segment一起加載。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a8/a8a9416cdae2ee7090089f4a8bdd7aae.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過命令objdump和readelf可以查看ELF文件的內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對可重定位目標文件常見的section有:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/72/72f22a9c269d97fc5e8e44ccd2e3dac0.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"符號解析:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈接器會爲對外部符號的引用修改爲正確的被引用符號的地址,當無法爲引用的外部符號找到對應的定義時,鏈接器會報undefined reference to XXXX的錯誤。另外一種情況是,找到了多個符號的定義,這種情況鏈接器有一套規則。在描述規則前需要了解強符號和弱符號的概念,簡單講函數和已初始化的全局變量是強符號,未初始化的全局變量是弱符號。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對符號的多重定義鏈接器處理規則如下(作者在gcc 7.3.0上貌似規則2,3都按1處理):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 不允許多個強符號定義,鏈接器會報告重複定義貌似的錯誤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 如果一個強符號和多個弱符號同名,則選擇強符號"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 如果符號在所有目標文件中都爲弱符號,那麼選擇佔用空間最大的一個"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了這些基礎,我們先來看一下"},{"type":"text","marks":[{"type":"strong"}],"text":"靜態鏈接"},{"type":"text","text":"過程:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 鏈接器從左到右按照命令行出現順序掃描目標文件和靜態庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 鏈接器維護一個目標文件的集合E,一個未解析符號集合U,以及E中已定義的符號集合D,初始狀態E、U、D都爲空"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 對命令行上每個文件f,鏈接器會判斷f是否是一個目標文件還是靜態庫,如果是目標文件,則f加入到E,f中未定義的符號加入到U中,已定義符號加入到D中,繼續下一文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 如果是靜態庫,鏈接器嘗試到靜態庫目標文件中匹配U中未定義的符號,如果m中匹配U中的一個符號,那麼m就和上步中文件f一樣處理,對每個成員文件都依次處理,直到U、D無變化,不包含在E中的成員文件簡單丟棄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5. 所有輸入文件處理完後,如果U中還有符號,則出錯,否則鏈接正常,輸出可執行文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題二:靜態庫順序"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖所示,main.cpp依賴liba.a,liba.a又依賴libb.a,根據靜態鏈接算法,如果用g++ main.cpp liba.a libb.a的順序能正常鏈接,因爲解析liba.a時未定義符號FunB會加入到上述算法的U中,然後在libb.a中找到定義,如果用g++ main.cpp libb.a liba.a的順序編譯,則無法找到FunB的定義,因爲根據靜態鏈接算法,在解析libb.a的時候U爲空,所以不需要做任何解析,簡單拋棄libb.a,但在解析liba.a的時候又發現FunB沒有定義,導致U最終不爲空,鏈接錯誤,因此在做靜態鏈接時,需要特別注意庫的順序安排,引用別的庫的靜態庫需要放在前面,碰到鏈接很多庫的時候,可能需要做一些庫的調整,從而使依賴關係更清晰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/eb/eb2424db465620def3e0527b203a50a6.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"動態鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前大部分內容都是靜態鏈接相關,但靜態鏈接有很多不足:不利於更新,只要有一個庫有變動,都需要重新編譯;不利於共享,每個可執行程序都單獨保留一份,對內存和磁盤是極大的浪費。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要生成動態鏈接庫需要用到參數“-shared -fPIC”表示要生成位置無關PIC(Position Independent Code)的共享目標文件。對靜態鏈接,在生成可執行目標文件時整個鏈接過程就完成了,但要想實現動態鏈接的效果,就需要把程序按照模塊拆分成相對獨立的部分,在程序運行時將他們鏈接成一個完整的程序,同時爲了實現代碼在不同程序間共享要保證代碼是和位置無關的(因爲共享目標文件在每個程序中被加載的虛擬地址都不一樣,要保證它不管被加載在哪都能工作),而爲了實現位置無關又依賴一個前提:數據段和代碼段的距離總是保持不變。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於不管在內存中如何加載一個目標模塊,數據段和代碼段間的距離是不變的,編譯器在數據段前面引入了一個全局偏移表GOT(Global Offset Table),被引用的全局變量或者函數在GOT中都有一條記錄,同時編譯器爲GOT中每個條目生成一個重定位記錄,因爲數據段是可以修改的,動態鏈接器在加載時會重定位GOT中的每個條目,這樣就實現了PIC。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大體原理基本就這樣,但具體實現時,對函數的處理和全局變量有所不同。由於大型程序函數成千上萬,而程序很可能只會用到其中的一小部分,因此沒必要加載的時候把所有的函數都做重定位,只有在用到的時候纔對地址做修訂,爲此編譯器引入了過程鏈接表PLT(Procedure Linkage Table)來實現延時綁定。PLT在代碼段中,它指向了GOT中函數對應的地址,第一次調用時候,GOT存放的不是函數的實際地址,而是PLT跳轉到GOT代碼的後一條指令地址,這樣第一次通過PLT跳轉到GOT,然後通過GOT又調回到PLT的下一條指令,相當於什麼也沒做,緊接着PLT後面的代碼會將動態鏈接需要的參數入棧,然後調用動態鏈接器修正GOT中的地址,從這以後,PLT中代碼跳轉到GOT的地址就是函數真正的地址,從而實現了所謂的延時綁定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對共享目標文件而言,有幾個需要關注的section:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/85/85977f1637745e530e138ca5dfec686a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了以上基礎後,我們看一下"},{"type":"text","marks":[{"type":"strong"}],"text":"動態鏈接"},{"type":"text","text":"的過程:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 裝載過程中程序執行會跳轉到動態鏈接器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 動態鏈接器自舉通過GOT、.dynamic信息完成自身的重定位工作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 裝載共享目標文件:將可執行文件和鏈接器本身符號合併入全局符號表,依次廣度優先遍歷共享目標文件,它們的符號表會不斷合併到全局符號表中,如果多個共享對象有相同的符號,則優先載入的共享目標文件會屏蔽掉後面的符號"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 重定位和初始化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題三:全局符號介入"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"動態鏈接過程中最關鍵的第3步可以看到,當多個共享目標文件中包含一個相同的符號,那麼會導致先被加載的符號佔住全局符號表,後續共享目標文件中相同符號被忽略。當我們代碼中沒有很好的處理命名的話,會導致非常奇怪的錯誤,幸運的話立刻core dump,不幸的話直到程序運行很久以後才莫名其妙的core dump,甚至永遠不會core dump但是結果不正確。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖所示,main.cpp中會用到兩個動態庫libadd.so,libadd1.so的符號,我們把重點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cb/cb6602732a35d9604ba85dc9ce6323b4.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"放在Add函數的處理上,當我們以g++ main.cpp libadd.so libadd1.so編譯時,程序輸出“Add in add lib”說明Add是用的libadd.so中的符號(add.cpp),當我們以g++ main.cpp libadd1.so libadd.so編譯時,程序輸出“Add in add1 lib”說明Add是用的libadd1.so中的符號,這時候問題就大了,調用方main.cpp中認爲Add只有兩個參數,而add1.cpp中認爲Add有三個參數,程序中如果有這樣的代碼,可以預見很可能造成巨大的混亂。具體符號解析我們可以通過LD_DEBUG=all ./a.out來觀察Add的解析過程,如下圖所示:左邊是對應libadd.so在編譯時放在前面的情況,Add綁定在libadd.so中,右邊對應libadd1.so放前面的情況,Add綁定在libadd1.so中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/94/942821b3c3a4a5bfe83ab313aef8c841.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"運行時加載動態庫:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了動態鏈接和共享目標文件的加持,Linux提供了一種更加靈活的模塊加載方式:通過提供dlopen,dlsym,dlclose,dlerror幾個API,可以實現在運行的時候動態加載模塊,從而實現插件的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下代碼演示了動態加載Add函數的過程,add.cpp按照正常編譯“g++ -fPIC –shared –o libadd.so add.cpp”成libadd.so,main.cpp通過“g++ main.cpp -ldl”編譯爲a.out。main.cpp中首先通過dlopen接口取得一個句柄void *handle,然後通過dlsym從句柄中查找符號Add,找到後將其轉化爲Add函數,然後就可以按照正常的函數使用,最後dlclose關閉句柄,期間有任何錯誤可以通過dlerror來獲取。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/72/727646d89a579df8e699fa0c1cf12f9a.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題四:靜態全局變量與動態庫導致double free"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在全面瞭解了動態鏈接相關知識後,我們來看一個靜態全局變量和動態庫糾結在一起引發的問題,代碼如下,foo.cpp中有一個靜態全局對象foo_,foo.cpp會編譯成一個libfoo.a,bar.cpp依賴libfoo.a庫,它本身會編譯成libbar.so,main.cpp既依賴於libfoo.a又依賴libbar.so。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f60745ab349df3bc9064e29d7771ea7d.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯的makefile如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a4bb1a9abd94bc060db370bf32beeff2.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行a.out會導致double free的錯誤。這是由於在一個位置上調用了兩次析構函數造成的。之所以會這樣是因爲鏈接的時候先鏈接的靜態庫,將foo_的符號解析爲靜態庫中的全局變量,當動態鏈接libbar.so時,由於全局已經有符號foo_,因此根據全局符號介入,動態庫中對foo_的引用會指向靜態庫中版本,導致最後在同一個對象上析構了兩次。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/07/0705754457bee39153edc2d79fc739e0.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決辦法如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 不使用全局對象"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 編譯時候調換庫的順序,動態庫放在前面,這樣全局只會有一個foo_對象"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 全部使用動態庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 通過編譯器參數來控制符號的可見性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過四個編譯鏈接中碰到的問題,基本把編譯鏈接的這些事覆蓋了一遍,有了這些基礎,在日常工作中應對一般的編譯鏈接問題應該可以做到遊刃有餘。由於篇幅有限,文章省略了大量的細節,主要集中在大的框架原理性梳理,如果想進一步深挖相關的細節,可參與相關參考文獻,以及閱讀elf.h相關的頭文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考文獻:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 《鏈接器和加載器》"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 《深入理解計算機系統》"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 《程序員的自我修養》"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. http://www.gnu.org/software/binutils/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注1"},{"type":"text","text":":本文所涉及工具可從http://www.gnu.org/software/binutils/獲取詳細信息"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注2"},{"type":"text","text":":本文示例代碼圖片中,每個窗口下面的白色區域有這份代碼對應的文件名稱,注意匹配對應文中說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://bbs.huaweicloud.com/blogs?utm_source=jianshu&utm_medium=bbs-ex&utm_campaign=other&utm_content=content","title":""},"content":[{"type":"text","text":"點擊關注,第一時間瞭解華爲雲新鮮技術~"}],"marks":[{"type":"strong"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章