linux編譯與連結的知識__linux

來源:互聯網
上載者:User

linux下編譯hello.c 程式,使用gcc hello.c,然後./a.out就可以運行;在這個簡單的命令後面隱藏了許多複雜的過程,這個過程包括了下面的步驟

宏定義展開,所有的#define 在這個階段都會被展開 先行編譯命令的處理,包括#if #ifdef 一類的命令 展開#include 的檔案,像上面hello world 中的stdio.h , 把stdio.h中的所有代碼合并到hello.c中 去掉注釋

gcc的先行編譯 採用的是先行編譯器cpp, 我們可以通過-E參數來看先行編譯的結果,如: gcc -E hello.c -o hello.i產生的 hello.i 就是經過了先行編譯的結果在先行編譯的過程中不會太多的檢查與先行編譯無關的文法(#ifdef 之類的還是需要檢查, #include檔案路徑需要檢查), 但是對於一些諸如 ; 漏掉的語法錯誤,在這個階段都是看不出來的。寫過makefile的人都知道, 我們需要加上-Ipath 一系列的參數來標示gcc對標頭檔的尋找路徑

小提示:

1.在一些程式中由於宏的原因導致編譯錯誤,可以通過-E把宏展開再檢查錯誤 , 這個在編寫 PHP擴充, python擴充這些大量需要使用宏的地方對於查錯誤很有協助。

2.如果在標頭檔中,#include 的時候帶上路徑在這個階段有時候是可以省不少事情, 比如 #include <public/connectpool/connectpool.h>, 這樣在gcc的-I參數只需要指定一個路徑,不會由於不小心導致,檔案名稱正好相同出現衝突的麻煩事情. 帶路徑的方式要多寫一些代碼,也是麻煩的事情, 路徑由外部指定相對也會靈活一些.

編譯

這個過程才是進行文法分析和詞法分析的地方, 他們將我們的C/C++代碼翻譯成為 彙編代碼, 這也是一個編譯器最複雜的地方

使用命令 gcc -S hello.i -o hello.s可以看到gcc編譯出來的彙編代碼, 現代gcc編譯器一般是把先行編譯和編譯合在一起,使用cc1 的程式來完成這個過程,編譯大檔案的時候可以用top命令看一個cc1的進程一直在佔用時間,這個時候就是程式在執行編譯過程. 後面提到的編譯過程都是指 cc1的處理包括了先行編譯與編譯.

彙編

現在C/C++代碼已經成為彙編代碼了,直接使用彙編代碼的編譯器把彙編變成機器碼(注意還不是可執行檔) . gcc -c hello.c -o hello.o這裡的hello.o就是最後的機器碼, 如果作為一個靜態庫到這裡可以所已經完成了,不需要後面的過程.

對於靜態庫, 比如ullib, COM提供的是libullib.a, 這裡的.a檔案其實是多個.o 通過ar命令打包起來的, 僅僅是為了方便使用,拋開.a 直接使用.o 也是一樣的

小提示:

1. gcc 採用as 進行彙編的處理過程,as 由於接收的是gcc產生的標準彙編, 在語法檢查上存在不少缺陷,如果是我們自己寫的彙編代碼給as去處理,經常會出現很多莫名奇妙的錯誤.

連結

連結的過程,本質上來說是一個把所有的機器碼檔案組合成一個可執行檔檔案上面彙編的結果得到一個.o檔案, 但是這個.o要產生二執行檔案只靠它自己是不行的, 它還需要一堆輔助的機器碼,幫它處理與系統底層打交道的事情. gcc -o hello hello.o

這樣就把一個.o檔案連結成為了一個二進位可執行檔.

這個地方也是本文討論的重點, 在後面會有更詳細的說明

小提示:

有些程式在編譯的時候會出現 "linker input file unused because linking not done" 的提示(雖然gcc不認為是錯誤,這個提示還是會出現的), 這裡就是把 編譯和連結 使用的參數搞混了,比如 g++ -c test.cpp -I../../ullib/include -L../../ullib/lib/ -lullib這樣的寫法就會導致上面的提示, 因為在編譯的過程中是不需要連結的, 它們兩個過程其實是獨立的

靜態連結

連結的過程這裡先介紹一下,連結器所做的工作

其實連結做的工作分兩塊: 符號解析和重定位

符號解析

符號包括了我們的程式中的被定義和引用的函數和變數資訊

在命令列上使用 nm ./test

test 是使用者的二進位程式,包括

可以把在二進位目標檔案中符號表輸出 00000000005009b8 A __bss_start 00000000004004cc t call_gmon_start 00000000005009b8 b completed.1 0000000000500788 d __CTOR_END__ 0000000000500780 d __CTOR_LIST__ 00000000005009a0 D __data_start 00000000005009a0 W data_start 0000000000400630 t __do_global_ctors_aux 00000000004004f0 t __do_global_dtors_aux 00000000005009a8 D __dso_handle 0000000000500798 d __DTOR_END__ 0000000000500790 d __DTOR_LIST__ 00000000005007a8 D _DYNAMIC 00000000005009b8 A _edata 00000000005009c0 A _end 0000000000400668 T _fini 0000000000500780 A __fini_array_end 0000000000500780 A __fini_array_start 0000000000400530 t frame_dummy 0000000000400778 r __FRAME_END__ 0000000000500970 D _GLOBAL_OFFSET_TABLE_ w __gmon_start__ U __gxx_personality_v0@@CXXABI_1.3 0000000000400448 T _init 0000000000500780 A __init_array_end ...當然上面由nm輸出的符號表可以通過編譯命令去除,讓人不能直接看到。

連結器解析符號引用的方式是將每一個引用的符號與其它的目標檔案(.o)的符號表中一個符號的定義聯絡起來, 對於那些和引用定義在相同模組的本地符號(注:static修飾的),編譯器在編譯期就可以發現問題,但是對於那些全域的符號引用就比較麻煩了.

下面來看一個最簡單程式: #include <stdio.h> int foo(); int main() { foo(); return 0; } 我們把檔案命名為test.cpp, 採用下面的方式進行編譯

g++ -c test.cpp g++ -o test test.o

第一步正常結束,並且產生了test.o檔案,到第二步的時候報了如下的錯誤

test.o(.text+0x5): In function `main': : undefined reference to `foo()' collect2: ld returned 1 exit status

由於foo 是全域符號, 在編譯的時候不會報錯,等到連結的時候,發現沒有找到對應的符號,就會報出上面的錯誤。但是如果我們把上面的寫法改成下面這樣

#include <stdio.h> //注意這裡的static static int foo(); int main() { foo(); return 0; }

在運行 g++ -c test.cpp, 馬上就報出下面的錯誤:

test.cpp:19: error: 'int foo()' used but never defined

在編譯器就發現foo 無法產生目標檔案的符號表,可以馬上報錯,對於一些本地使用的函數使用static一方面可以避免符號汙染,另一方面也可以讓編譯器儘快的發現錯誤.

在基礎庫中提供的都是一系列的.a檔案,這些.a檔案其實是一批的目標檔案(.o)的打包結果.這樣的目的是可以方便的使用已有代碼產生的結果,一般情況下是一個.c/.cpp檔案產生一個.o檔案,在編譯的時候如果帶上一堆的.o檔案顯的很不方便,像:

g++ -o main main.cpp a.o b.o c.o

這樣大量的使用.o也很容易出錯,在linux下使用 archive來講這些.o存檔和打包.

所以我們就可以把編譯參數寫成

g++ -o main main.cpp ./libullib.a

我們可以使用 ./libullib.a 直接使用 libullib.a這個庫,不過gcc提供了另外的方式來使用:

g++ -o main main.cpp -L./ -lullib

-L指定需要尋找的庫檔案的路徑, -l 選擇需要使用的庫名字,不過庫的名字需要用 lib+name的方式命名,才會被gcc認出來.不過上面的這種方式存在一個問題就是不區分動態庫和靜態庫, 這個問題在後面介紹動態庫的時候還會提到.

當存在多個.a ,並且在庫之間也存在依賴關係,這個時候情況就比較複雜.

如果要使用lib2-64/dict, dict又依賴ullib, 這個時候需要寫成類似下面的形式

g++ -o main main.cpp -L../lib2-64/dict/lib -L../lib2-64/ullib/lib -ldict -lullib

-lullib需要寫在-ldict的後面, 這是由於在預設情況對於符號表的解析和尋找工作是由後往前(內部實現是一個類似堆棧的尾遞迴).所以當所使用的庫本身存在依賴關係的時候,越是基礎的庫就越是需要放到後面.否則如果上面把 -ldict -lulib的位置換一下,可能就會出現 undefined reference to xxx 的錯誤.

當然gcc提供了另外的方式的來解決這個問題

g++ -o main main.cpp -L../lib2-64/dict/lib -L../lib2-64/ullib/lib -Xlinker "-(" -ldict -lullib -Xlinker "-)"

可以看到我們需要的庫被 -Xlinker "-(" 和 -Xlinker "-)"  包含起來,gcc在這裡處理的時候會迴圈自動尋找依賴關係,不過這樣的代價就是延長gcc的編譯時間,如果使用的庫非常的多時候,對編譯的耗時影響還是非常大.

-Xlinker有時候也簡寫成"-Wl, ",它的意思是 它後面的參數是給連結器使用的.-Xlinker 和 -Wl 的區別是一個後面跟的參數是用空格,另一個是用","

我們通過nm 命令查看目標檔案,可以看到類似下面的結果

1 0000000000009740 T _Z11ds_syn_loadPcS_
2 0000000000009c62 T _Z11ds_syn_seekP16Sdict_search_synPcS1_i
3 0000000000007928 T _Z11dsur_searchPcS_S_
4                 &nbs p; U _Z11ul_readfilePcS_Pvi
5                 &nbs p; U _Z11ul_writelogiPKcz
6 00000000000000a2 T _Z12creat_sign32Pc

其中用 U 標示的符號_Z11ul_readfilePcS_Pvi (其實是ullib中的 ul_readfile) ,表示在dict的目標檔案中沒有找到ul_readfile函數.

在連結的時候,連結器就會去其他的目標檔案中尋找_Z11ul_readfilePcS_Pvi的符號

小提示:

編譯的時候採用 -Lxxx -lyyy 的形式使用庫,-L和-l這個參數並沒有配對的關係,我們的一些Makefile 為了維護方便把他們寫成配對的形式,造成了誤解.其實完全可以寫成 -Lpath1, -Lpath2, -Lpath3, -llib1 這樣的形式.

在具體連結的時候,gcc是以.o檔案為單位, 編譯的時候如果寫 g++ -o main main.cpp libx.o 那麼無論main.cpp中是否使用到libx.o,libx.o中的所有符號都會被載入到mian函數中.但是如果是針對.a,寫成g++ -o main main.cpp -L./ -lx, 這個時候gcc在連結的時候只會連結有被用到.o, 如果出現libx.a中的某個.o檔案中沒有任何一個符號被main用到,那麼這個.o就不會被連結到main中

重定位

經過上面的符號解析後,所有的符號都可以找到它所對應的實際位置(U表示的連結找到具體的符號位置).

as 彙編產生一個目標模組的時候,它不知道資料和代碼在最後具體的位置,同時也不知道任何外部定義的符號的具體位置,所以as在產生目標代碼的時候,對於位置未知的符號,它會產生一個重定位表目,告訴連結器在將目標檔案合并成可執行檔時候如何修改地址成最終的位置

g++和gcc

採用gcc 和g++ 在編譯的時候產生的符號有所不同.

在C++中由於要支援函數重載,命名空間等特性,g++會把函數+參數(可能還有命名空間),把函數命變成一個特殊並且唯一的符號名.例如:

int foo( int a);

在gcc編譯後,在符號表中的名字就是函數名foo, 但是在g++編譯後名字可能就變成了_Z3fooi, 我們可以使用 c++filt命令把一個符號還原成它原本的樣子,比如

c++filt _Z3fooi

啟動並執行結果可以得到 foo(int)

由於在C++和純C環境中,符號表存在不相容問題,C程式不能直接調用C++編譯出來的庫,C++程式也不能直接調用C編譯出來的庫.為瞭解決這個問題C++中引入了 extern "C" 的方式.

extern "C" int foo(int a);

這樣在用g++編譯的時候, c++的編譯器會自動把上面的 int foo(int a)當做C的介面進行符號轉化.這樣在純C裡面就可以認出這些符號.

不過這裡存在一個問題,extern "C" 是C++支援的,gcc並不認識,所有在實際中一般採用下面的方式使用++

#ifdef __cplusplus extern " C" { #endif int foo( int a); #ifdef __cplusplus } #endif

這樣這個標頭檔中的介面即可以給gcc使用也可以給g++使用, 當然在extern "C" { } 中的介面是不支援重載,預設參數等特性

在我們的64位編譯環境中如果有gcc的程式使用上面方式g++編譯出來的庫,需要加上-lstdc++, 這是因為,對於我們64位環境下g++編譯出來的庫,需要使用到一個 __gxx_personality_v0 的符號,它所在的位置是/usr /lib64/libstdc++.so.6 (C++的標準庫iostream都在裡面,C++程式都需要的). 但是在32位2.96 g++編譯器中是不需要__gxx_personality_v0,所有編譯可以不加上 -lstdc++

小提示

在linux gcc 中, 只有在原始碼使用 .c做尾碼,並且使用gcc編譯才會被編譯成純C的結果,其他情況像 g++ 編譯.c檔案,或者gcc 編譯.cc, .cpp檔案都會被當作C++程式編譯成C++的目標檔案, gcc和g++唯一的不同在於gcc不會主動連結-lstdc++ 在 extern "C" { }中如果存在預設參數的介面,在g++編譯的時候不會出現問題,但是gcc使用的時候會報錯.因為對於函數重載,介面的符號表還是和不用預設參數的時候是一樣的.

符號表衝突

編譯器的時候時常會遇到類似於

multiple definition of `foo()'

的錯誤.

這些錯誤的產生都是由於所使用的.o檔案中存在了相同的符號造成的.

比如:

libx.cpp

int foo() { return 30; }

liby.cpp

int foo() { return 20; }

將libx.cpp, liby.cpp編譯成 libx.o和liby.o兩個檔案 g++ -o main main.cpp libx.o liby.o這個時候就會報出 multiple definition of `foo()' 的錯誤(一些參數可以把這個警報關掉)

但是如果把libx.o和liby.o分別打包成libx.a和liby.a用下面的方式編譯 g++ -o main main.cpp -L./ -lx -ly這個時候編譯不會報錯,它會選擇第一個出現的庫,上面的例子中會選擇libx中的foo

可以通過 g++ -o main main.cpp -L./ -lx -ly -Wl,--trace-symbol=_Z3foov的命令查看符號具體是連結到哪個庫中,

g++ -o main main.cpp -L./ -lx -ly -Wl, --cref 可以把所有的符號連結都輸出(無論是否最後被使用)

小提示:

對於一些定義在標頭檔中的全域常量,gcc和g++有不同的行為,g++中const也同時是static的,但gcc不是

例如: foo.h 中存在一個 const int INTVALUE = 2000;

的全域常量

有兩個庫 a和b, 他們在產生的時候有使用到了  INTVALUE,如果有一個程式main同時使用到了 a庫和b庫,在連結的時候gcc編譯的結果就會報錯,但如果a和b都是g++編譯的話結果卻一切正常.

這個原因主要是在g++中會把INTVALUE 這種const常量當做static的,這樣就是一個局部變數,不會導致衝突,但是如果是gcc編譯的話,這個地方INTVALUE會被認為是一個對外的全域常量是非static的,這個時候就會造成連結錯誤

動態連結

對於靜態庫的使用,有下面兩個問題

當我們需要對某一個庫進行更新的時候,我們必須把一個可執行檔再完整的進行一些重新編譯 在程式啟動並執行時候代碼是會被載入機器的記憶體中,如果採用靜態庫就會出現一個庫需要被copy到多個記憶體程式中,這個一方面佔用了一定的記憶體,另一方面對於CPU的cache不夠友好 連結的控制,從前面的介紹中可以看到靜態庫的串連行為我們不好控制,做不到在運行期替換使用的庫 編譯後的程式就是二進位代碼,有些代碼它們涉及到不同的機器和環境,假設在A 機器上編譯了一個程式X, 把它直接放到B機器上去運行,由於A和B環境存在差異,直接運行X程式可能存在問題,這個時候如果把和機器相關的這部分做成動態庫C,並且保證介面一致,編譯X程式的時候只調用C的對外介面.對於一般的使用者態的X程式而言,就可以簡單的從A環境放到B環境中.但如果是靜態編譯,就可能做不到這點,需要在B機器上重新編譯一次.

動態連結程式庫在linux被稱為共用庫(shared library,下文提到的共用庫和動態連結程式庫都是指代shared library),它主要是為瞭解決上面列出靜態庫的缺點而提出的.。

共用庫的使用

共用庫的使用主要有兩種方式,一種方式和.a的靜態庫類似由編譯器來控制,其實質和二進位程式一樣都是由系統中的載入器(ld-linux.so)載入,另一種是寫在代碼中,由我們自己的代碼來控制.

還是以前面的例子為例: g++ -shared -fPIC -o libx.so libx.cpp編譯的時候和靜態庫類似,只是加上了 -shared 和 -fPIC, 將輸出命名改為.so

然後和可執行檔連結.a一樣,都是 g++ -o main main.cpp -L./ -lx這樣main就是調用 libx.so, 在啟動並執行時候可能會出現找不到libx.so的錯誤, 這個原因是由於動態庫尋找路徑的問題, 動態庫預設的尋找路徑是由/etc /ld.so.conf檔案來指定,在運行可執行檔的時候,按照順會去這些目錄下尋找需要的共用庫。我們可以通過 環境變數 LD_LIBRARY_PATH來指定共用庫的尋找路徑(註:LD_LIBRARY_PATH的優先順序比ld.so.conf要高).

命令上運行 ldd ./main 我們可以看到這個二進位程式在啟動並執行時候需要使用的動態庫,例如:

libx.so => /home/bnh/tmp/test/libx.so (0x003cb000) libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00702000) libm.so.6 => /lib/tls/libm.so.6 (0x00bde000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x00c3e000) libc.so.6 => /lib/tls/libc.so.6 (0x00aab000)

這裡列出了mian所需要的動態庫, 如果有看類似 libx.so=>no found的錯誤,就意味著路徑不對,需要設定LD_LIBRARY_PATH來指定路徑

手動載入共用庫

除了採用類型於靜態庫的方式來使用動態庫,我們還可以通過由代碼來控制動態庫的使用。

這種方式允許應用程式在運行時載入和連結共用庫,主要有下面的四個介面

載入動態連結程式庫 void *dlopen( const char *filename, int flag); 擷取動態庫中的符號 void *dlsym( void *handle, char *symbol); 關閉動態連結程式庫 void dlclose( void *handle); 輸出錯誤資訊 const char *dlerror( void);

看下面的例子: typedef int foo_t(); foo_t * foo = (foo_t*) dlsym(handle, " foo");

通過上面的方式我們可以載入符號"foo"所對應的地址,然後通過強制類型轉換給一個函數指標,當然這裡函數指標的類型需要和符號的原型類型保持一致,這些一般是由共用庫所對應的標頭檔提供.

這裡要注意一個問題,在dlsym中載入的符號表示是和我們使用nm 庫檔案所看到符號表要保持一致,這裡就有一個前面提到的 gcc和g++符號表的不同,一個 int foo(), 如果是g++編譯,並且沒有extern "C"匯出介面,那麼用dlsym載入的時候需要用 dlsym(handle, "_Z3foov") 方式才可以載入函數 int foo(), 所以建議所以的共用庫對外介面都採用 extern "C"的方式匯出 純C介面對外使用,這樣在使用上也會比較方便

dlopen 的flag 標誌可以選擇 RTLD_GLOBAL , RTLD_NOW, RTLD_LAZY. RTLD_NOW, RTLD_LAZY只是表示載入的符號是一開始就被載入還等到使用的時候被載入,對於多數應用而言沒有什麼特別的影響.這兩個標誌都可以通過| 和RTLD_GLOBAL一起連用

這裡主要是說明RTLD_GLOBAL的功能,考慮這樣的一個情況:

我們有一個main.cpp ,調用了兩個動態 libA, 和 libB, 假設A中有一個對外介面叫做 testA, 在main.cpp可以通過dlsym擷取到testA的指標,進行使用.但是對於libB 中的介面,它是看到不libA的介面,使用testA 是不能調用到libA中的testA的,但是如果在dlopen 開啟libA.so的時候,設定了RTLD_GLOBAL這個選項,就可以把libA.so中的介面升級為全域可見, 這樣在libB中就可以直接調用libA中的testA,如果在多個共用庫都有相同的符號,並且有RTLD_GLOBAL選項,那麼會優先選擇第一個。

另外這裡注意到一個問題, RTLD_GLOBAL使的動態庫之間的對外介面是可見的,但是動態庫是不能調用主程式中的全域符號,為瞭解決這個問題, gcc引入了一個參數-rdynamic,在編譯載入共用庫的可執行程式的時候最後在連結的時候加上-rdynamic,會把可執行檔中所有的符號變成全域可見,對於這個可執行程式而言,它載入的動態庫在運行中可以直接調用主程式中的全域符號,而且如果共用庫(自己或者另外的共用庫 RTLD_GLOBAL) 加中有同名的符號,會選擇可執行檔中使用的符號,這在一些情況下可能會帶來一些莫名其妙的運行錯誤。

小提示:

/usr/sbin/lsof -p pid 可以查看到由pid在運行期所載入的所有共用庫 共用庫無論是通過dlopen方式載入還是載入器載入,實質都是通過 mmap的方式把共用庫映射到記憶體空間中去。mmap的參數MAP_DENYWRITE可以在修改已經被載入某個進程檔案的時候阻止對於記憶體資料的修改,由於現在核心中已經禁用這個參數,直接導致的結果就是如果對mmap的檔案進行修改,這個時候的修改會被直接反映到已經被mmap映射的空間上。由於核心的不支援,使得共用庫不能在運行期進行熱切換,共用庫在更新的時候需要由載入的程式通過一些外部的方式來判斷,主動使用dlclose,並且dlopen 重新載入共用庫,如果是載入器載入那麼需要重啟程式。另外這裡的熱切換指的是直接copy覆蓋原有的共用庫,如果是採用mv或者軟串連的方式那麼還是安全的,共用庫被mv後不會影響原來的已經載入它的程式。 g++ 加上 -rdynamic 參數實質上相當於ld連結的時候加上-E或者--export-dynamic參數,效果與g++ -Wl,-E或者g++ -Wl,--export-dynamic的效果是一樣的。

靜態庫和動態庫的混合編譯

靜態庫與動態庫的混合使用,經常會出現一些奇怪的錯誤,使用的時候需要有所關注

對於一般情況下,只要靜態庫與共用庫之間沒有依賴關係,沒有使用全域變數(包括static變數),不會出現太多的問題,下面以出現的問題作例子來說明使用的注意事項。

轉載地址:http://hi.baidu.com/herejing/blog/item/ca31c8d6d0d9f12206088b0b.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.