1. 程式人生 > >C/C++ Lib庫檔案nm除錯之符號表

C/C++ Lib庫檔案nm除錯之符號表

本文主要介紹了一下在Linux下開發c/c++時候,不可避免的會開發或者生成.o .a .so這種中間庫狀態的檔案(可能是自己寫了一個lib讓別人呼叫,或者提供.c/.cpp檔案嵌入別人的Makefile工程)。如何檢視這些庫檔案的一些基本資訊。有時候大家編譯程式時候(確切的說是連結器連結的時候)很多錯誤例如"undefine reference",之類的常見錯誤,原因就是因為沒有找到.o .a .so的庫檔案,導致連結失敗。

        -------------------------------------------------------------------------------

        1、Linux庫檔案

        2、庫檔案的使用方式

        3、利用tar/nm檢視庫檔案的資訊

        -------------------------------------------------------------------------------

       1、庫檔案的定義之類的就不在此累贅了,有興趣Google之。說白了就是我們寫好一些對應的.h和.c(.cpp)檔案,然後通過編譯器的編譯,生成中間程式碼供他人使用,他人只需要將你的中間程式碼include進自己的程式即可。注意,編譯器編譯成最終可執行的檔案需要好幾步,基本可以分為:文字解析->語法解析->此法分析->預處理分析->編譯->連線。生成中間庫是沒有連結階段的,在Linux Gcc下通過-C引數指定只編譯不連結,所以如果寫了一個.c檔案用到了比如pthread_create之類的外部呼叫,在Gcc -C編譯的時候不用-lpthread因為這個時候是不需要連結的。

      2、(對庫檔案熟悉話直接跳過)在各個系統平臺上,庫檔案的格式和形式各不相同,Windows下就是如同xxx.dll或者xxx.lib,*inux下就是xxx.so或者xxx.a。兩種分別對應的是靜態庫和動態庫,靜態庫會連同編譯器編譯連結進入程式成為程式的一部分,好處是作為程式的一部分不用每次執行時都去load(弊端是可能很多程序都用到這個庫但是每個程序中都有一份,動態庫的話記憶體中只有一份,通過重定向來載入),而且不會導致因庫的缺失而執行失敗,壞處是會導致可執行檔案偏大。動態庫是程式執行時動態載入到程序裡去的,而且可以多程序共,並且方便軟體更新,直接替換老的庫即可。

      到底使用靜態還是動態庫取決於程式的使用上下文環境,一般第三方庫都提供了兩種版本,系統庫的話一般都是動態連結庫,因為同樣系統下的庫都是一樣的。用Linux的Gcc舉例:

      一、寫了一個.h宣告一個foo()函式,然後在.c或者.cpp中實現foo()函式

      二、gcc(g++) -c -o foo.o foo.c (注意此處不需要.h標頭檔案,標頭檔案只是對庫的對外介面描述)

      三、生成靜態庫: ar -r libfoo.a foo.o (靜態庫.a其實就是.o檔案的壓縮包,注意這裡不支援把.a打入.a)

             生成動態庫: gcc foo.c -fPIC -shared -o libfoo.so(-fPIC意思是生成位置無關程式碼,因為動態庫是執行時載入的,需要對程式碼進行重定向,不清楚可以Google一下)

      四、寫個含有main函式的檔案,並呼叫foo()函式 : gcc(g++) -o test test.c -lfoo,這裡-lfoo意思是去找以lib開頭的某.so或.a檔案,預設優先找.so動態庫

      需要注意一下的是,如果是cpp引用了c的庫或.c那麼標頭檔案裡要用externc "C"關鍵字來指定按c的方式讀取(根本上是因為c和c++的函式簽名不一致,因為c++支援過載,所以按c++的方式是找不到同名的c函式的)。在使用動態庫的情況下,程式回去一些預定義的地方找.so檔案。比如/usr/lib/下,如果需要自己指定,請修改/etc/ld.so.conf檔案。並用ldconfig來重新整理cache。

      3、如果我們需要檢視自己寫的庫的資訊時可以用nm來檢視,如檢視庫中有哪些函式,有哪些全域性變數,有哪些依賴別的庫的東西等等,下面我們寫一個例子來說明一下:

原始碼列印
  1. #include <stdio.h>
  2. int g1;   
  3. int g2 = 0;  
  4. staticint g3;   
  5. staticint g4=0;  
  6. constint g5=0;  
  7. staticconstint g6 = 0;  
  8. int main(int argc, char *argv[])  
  9. {  
  10.     staticint st = 0;  
  11.     int t1;   
  12.     int t2 = 0;  
  13.     constint t3 = 0;  
  14.     printf("printf-function");  
  15.     return 0;  
  16. }  
  17. void foo1(){}  
  18. staticvoid foo2(){}  
原始碼列印
  1. void overload(int i){}  
原始碼列印
  1. void overload(float i){}  


        linux的nm命令可以一個檔案中的符號列表,列出以上程式碼Gcc -c編譯出的a.o(a.a a.so)可以通過nm命令來檢視其中的符號資訊:

原始碼列印
  1. 0000000000000000 t   
  2. 0000000000000000 d   
  3. 0000000000000000 b   
  4. 0000000000000000 r   
  5. 0000000000000000 r   
  6. 0000000000000000 n   
  7. 0000000000000000 n   
  8. 0000000000000000 B g1  
  9. 0000000000000004 B g2  
  10. 0000000000000008 b g3  
  11. 000000000000000c b g4  
  12. 000000000000001c r g5  
  13. 0000000000000020 r g6  
  14.                  U __gxx_personality_v0  
  15. 0000000000000000 T main  
  16. 0000000000000000 a nm.cpp  
  17.                  U printf  
  18. 000000000000003e T _Z4foo1v  
  19. 0000000000000044 t _Z4foo2v  
  20. 0000000000000054 T _Z8overloadf  
  21. 000000000000004a T _Z8overloadi  
  22. 0000000000000010 b _ZZ4mainE2st  

        其中左邊第一列是符號的地址值,對應原始碼可以看出遞增的規律。第二列是該符號的型別,第三列是符號的名稱(比如函式名,變數名):

        符號型別:介紹幾個最常用的,其他的如果遇到了直接Google:

           B --- 全域性非初始化資料段(BBS段)的符號,其值表示該符號在bss段中的偏移,如g1

           b --- 全域性static的符號,如g3

           r --- const型只讀的變數(readonly)

           N --- debug用的符號

           T --- 位於程式碼區的符號,比如本檔案裡的函式main foo

           t  --- 位於程式碼區的符號,一般是static函式

           U --- 位於本檔案外的呼叫函式或變數符號,比如系統的printf()函式

       這裡要注意的是,本人使用g++編譯的,所以是按c++的支援過載的函式風格編譯的,可以看到所有函式均帶了字首和字尾,字首代表屬於類的名字,字尾代表引數列表的型別縮寫,因為過載必須是區分引數型別,這裡也可以看出,為什麼返回值不同的函式不是過載,因為符號表裡沒有返回值的記錄。

例如兩個overload函式的字尾分別是f和i代表一個是float型一個是int型(上面還有v ->void型)。

       nm命令對大家除錯多模組的程式很有用處,大部分情況下可以解決"undefined reference"的問題,如果大家發現nm的另類很好的用法,也可以留言哈!!!


原文連結:http://blog.csdn.net/gugemichael/article/details/8215738