linux下強大的文件分析工具 -- nm


轉:https://www.cnblogs.com/downey-blog/p/10477835.html

什么是nm

nm命令是linux下自帶的特定文件分析工具,一般用來檢查分析二進制文件、庫文件、可執行文件中的符號表,返回二進制文件中各段的信息。

目標文件、庫文件、可執行文件

首先,提到這三種文件,我們不得不提的就是gcc的編譯流程:預編譯,編譯,匯編,鏈接。

  • 目標文件 :常說的目標文件是我們的程序文件(.c/.cpp,.h)經過預編譯,編譯,匯編過程生成的二進制文件,不經過鏈接過程,編譯生成指令為:

    gcc(g++) -c file.c(file.cpp)
    將生成對應的file.o文件,file.o即為二進制文件

  • 庫文件: 分為靜態庫和動態庫,這里不做過多介紹,庫文件是由多個二進制文件打包而成,生成的.a文件,示例:

    ar -rsc liba.a test1.o test2.o test3.o
    將test1.o test2.o test3.o三個文件打包成liba.a庫文件

  • 可執行文件:可執行文件是由多個二進制文件或者庫文件(由上所得,庫文件其實是二進制文件的集合)經過鏈接過程生成的一個可執行文件,對應windows下的.exe文件,可執行文件中有且僅有一個main()函數(用戶程序入口,一般由bootloader指定,當然也可以改),一般情況下,二進制文件和庫文件中是不包含main()函數的,但是在linux下用戶有絕對的自由,做一個包含main函數的庫文件也是可以使用的,但這不屬於常規操作,不作討論。

上述三種文件的格式都是二進制文件。

為什么要用到nm

在上述提到的三種文件中,用編輯器是無法查看其內容的(亂碼),所以當我們有這個需求(例如debug,查看內存分布的時候)去查看一個二進制文件里包含了哪些內容時,這時候就將用到一些特殊工具,linux下的nm命令就可以完全勝任(同時還有objdump和readelf工具,這里暫不作討論)。

怎么使用nm

如果你對linux下的各種概念還算了解的話,就該知道一般linux下的命令都會自帶一些命令參數來滿足各種應用需求,了解這些參數的使用是使用命令的開始。

man

那么,如何去了解一個命令呢,最好的方法就是linux下的man命令,linux是一個寶庫,而man指令就相當於這個寶庫的說明書。

用法:

man nm

這里面介紹了nm的各種參數以及詳細用法,如果你有比較不錯的英文水平和理解能力,可以直接參考man page中的內容。

nm的常用命令參數

-A 或-o或 --print-file-name:打印出每個符號屬於的文件
-a或--debug-syms:打印出所有符號,包括debug符號
-B:BSD碼顯示
-C或--demangle[=style]:對低級符號名稱進行解碼,C++文件需要添加
--no-demangle:不對低級符號名稱進行解碼,默認參數
-D 或--dynamic:顯示動態符號而不顯示普通符號,一般用於動態庫
-f format或--format=format:顯示的形式,默認為bsd,可選為sysv和posix
-g或--extern-only:僅顯示外部符號
-h或--help:國際慣例,顯示命令的幫助信息
-n或-v或--numeric-sort:顯示的符號以地址排序,而不是名稱排序
-p或--no-sort:不對顯示內容進行排序
-P或--portability:使用POSIX.2標准
-V或--version:國際管理,查看版本
--defined-only:僅顯示定義的符號,這個從英文翻譯過來可能會有偏差,故貼上原文:

Display only defined symbols for each object file

好了,上述就是常用的命令參數,光說不練假把式,下面將給出一個示例來進一步理解nm用法:
示例代碼:

#include
#include

using namespace std;

const char *str="downey";
int g_uninit;
int g_val=10;


void func1()
{
    int *val=new int;
    static int val_static=1;
    cout<<"downey"<<endl;
}

void func1(char* str)
{
    cout<<str<<endl;
}
```

 

編譯指令:

g++ -c test.cpp
在當前目錄下生成test.o目標文件,然后使用nm命令解析:  
nm -n -C test.o
由於是C++源文件,故添加-C 選項,為了方便查看,添加-n選項

輸出信息:

```
                    U __cxa_atexit
                    U __dso_handle
                    U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
                    U std::ios_base::Init::Init()
                    U std::ios_base::Init::~Init()
                    U operator new(unsigned long)
                    U std::cout
                    U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
                    U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
0000000000000000    B g_uninit
0000000000000000    D str
0000000000000000    T func1()
0000000000000004    b std::__ioinit
0000000000000008    D g_val
000000000000000c    d func1()::val_static
0000000000000035    T func1(char*)
0000000000000062    t __static_initialization_and_destruction_0(int, int)
00000000000000a0    t _GLOBAL__sub_I_str
```

下面我們再來解析輸出信息中各部分所代表的意思吧

  • 首先,前面那一串數字,指的就是地址

  • 然后,我們發現,每一個條目前面還有一個字母,類似'U','B','D等等,其實這些符號代表的就是當前條目所對應的內存所在部分

  • 最右邊的就是對應的符號內容了

首要的需要講解的就是第二點中字符所對應的含義:

同樣在還是在linux命令行下man nm指令可以得到:

A     :符號的值是絕對值,不會被更改
B或b  :未被初始化的全局數據,放在.bss段
D或d  :已經初始化的全局數據
G或g  :指被初始化的數據,特指small objects
I     :另一個符號的間接參考
N     :debugging 符號
p     :位於堆棧展開部分
R或r  :屬於只讀存儲區
S或s  :指為初始化的全局數據,特指small objects
T或t  :代碼段的數據,.test段
U     :符號未定義
W或w  :符號為弱符號,當系統有定義符號時,使用定義符號,當系統未定義符號且定義了弱符號時,使用弱符號。
?    :unknown符號

根據以上的規則,我們就可以來分析上述的nm顯示結果:

  • 首先,輸出的上半部分對應的符號全是U,跟我們常有思路不一致的是,這里的符號未定義並不代表這個符號是無法解析的,而是用來告訴鏈接器,這個符號對應的內容在我這個文件只有聲明,沒有具體實現,如std::cout,std::string類,在鏈接的過程中,鏈接器需要到其他的文件中去找到它的實現,如果找不到實現,鏈接器就會報常見的錯誤:undefined reference。

  • 在接下來的三行中

    0000000000000000 B g_uninit
    0000000000000000 D str
    0000000000000000 T func1()
    令人疑惑的是,為什么他們的地址都是0,難道說mcu的0地址同時可以存三種數據?其實不是這樣的,按照上面的符號表規則,g_uninit屬於.bss段,str屬於全局數據區,而func1()屬於代碼段,這個地址其實是相對於不同數據區的起始地址,即g_uninit在.bss段中的地址是0,以此類推,而.bss段具體被映射到哪一段地址,這屬於平台相關,並不能完全確定。
    在目標文件中指定的地址都是邏輯地址,符號真正的地址需要到鏈接階段時進行相應的重定位以確定最終的地址。

  • 在接下來的四行中

    0000000000000004 b std::__ioinit
    0000000000000008 D g_val
    000000000000000c d func1()::val_static
    0000000000000035 T func1(char*)
    b在全局數據段中的4地址,因為上述g_uninit占用了四字節,所以std::__ioinit的地址為0+4=4.

    而g_val存在於全局數據段(D)中,起始地址為8,在程序定義中,因為在0地址處存放的是str指針,而我的電腦系統為64位,所以指針長度為8,則g_val的地址為0+8=8

    而靜態變量val_static則是放在全局數據段8+sizeof(g_val)=12處

    函數func1(char*)則放在代碼段func1()后面

講到這里,有些細心的朋友就會疑惑了,在全局數據區(D)中存放了str指針,那str指針指向的字符串放到哪里去了?其實這些字符串內容放在常量區,常量區屬於代碼區(t)(X86平台,不同平台可能有不同策略),對應nm顯示文件的這一部分:

 00000000000000a0    t _GLOBAL__sub_I_str  

如果你對此有一些疑惑,你可以嘗試將str字符串放大,甚至是改成上千個字節的字符串,就會看到代碼段(t)的變化。

好了,關於linux下nm命令的解析就到此為止啦,如果朋友們對於這個有什么疑問或者發現有文章中有什么錯誤,歡迎留言

原創博客,轉載請注明出處!

祝各位早日實現項目叢中過,bug不沾身.
(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM