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變量),不會出現太多的問題,下面以出現的問題作例子來說明使用的注意事項。
|