Linux ldd and nm
ldd和nm是Linux下兩個非常實用的程式分析工具。其中,ldd是用來分析程式執行時需要依賴的動態連結庫的工具,nm是用來檢視指定程式中的符號表資訊的工具。
1 ldd
格式:ldd [options] file
功能:列出file執行所需的共享庫
引數:
-d 執行重定位並報告所有丟失的函式
-r 執行對函式和物件的重定位並報告丟失的任何函式或物件
tanghuaming@Thm:~/Documents/sys_programming$ whereis ldd ldd: /usr/bin/ldd /usr/share/man/man1/ldd.1.gz tanghuaming@Thm:~/Documents/sys_programming$ ll /usr/bin/ldd-rwxr-xr-x 1 root root 5420 3月 26 14:59 /usr/bin/ldd*
ldd能夠顯示可執行模組的dependency,其原理是通過設定一系列的環境變數,如下:LD_TRACE_LOADED_OBJECTS、LD_WARN、LD_BIND_NOW、LD_LIBRARY_VERSION、LD_VERBOSE等。當LD_TRACE_LOADED_OBJECTS環境變數不為空時,任何可執行程式在執行時,它都會只顯示模組的dependency,而程式並不真正執行。要不你可以在shell終端測試一下,如下:
(1) export LD_TRACE_LOADED_OBJECTS=1
(2) 再執行任何的程式,如ls等,看看程式的執行結果。
ldd顯示可執行模組的dependency的工作原理,其實質是通過ld-linux.so(elf動態庫的裝載器)來實現的。我們知道,ld-linux.so模組會先於executable模組程式工作,並獲得控制權,因此當上述的那些環境變數被設定時,ld-linux.so選擇了顯示可執行模組的dependency。
實際上可以直接執行ld-linux.so模組,如:/lib/ld-linux.so.2 --list program(這相當於ldd program)ldd命令使用方法(摘自ldd --help)
我們選擇一段待測試的應用程式,程式碼如下:
//@file tooltest.c //@brief resource sharing between parent-process and sub-process #include <stdio.h> #include <stdlib.h> #include <unistd.h> int global = 1; /*global variable, stored at data section*/ int main(void) { pid_t pid;//to store pid value int stack = 1;//local variable, stored at stack int *heap;//pointer to a heap variable heap = (int *)malloc(sizeof(int)); *heap = 2;//set the heap value to 2 pid = fork();//create a new process if (pid < 0) { //error perror("fail to fork"); exit(-1); } else if (pid == 0) { //sub-process, change values global++; stack++; (*heap)++; //print all values printf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); exit(0); } else { //parent process sleep(2);//sleep 2 secends to make sure the sub-process runs first printf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); } return 0; }
然後,我們編譯並執行ldd命令:
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ldd tooltest linux-gate.so.1 => (0xb775b000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7595000) /lib/ld-linux.so.2 (0xb775c000)
我們可以將ldd的輸出結果分為3列來看:
■ 第一列:程式需要依賴什麼庫
■ 第二列:系統提供的與程式需要的庫對應的庫名稱
■ 第三列:依賴庫載入的開始地址
通過上面的這些資訊,我們可以總結出下面的用途:
(1) 通過對比第一列和第二列,我們可以知道程式需要的動態連結庫和系統實際提供的是否相比配。
(2) 通過第三列,我們可以知道當前動態連結庫中的符號在程序地址空間中的起始位置。
2 nm
格式:nm [options] file
功能:列出file中的所有符號
引數:
-C 將符號轉化為使用者級的名字
-s 當用於.a檔案即靜態庫時,輸出把符號名對映到定義該符號的模組或成員名的索引
-u 顯示在file外定義的符號或沒有定義的符號
-l 顯示每個符號的行號,或為定義符號的重定義項
下面是執行nm命令的輸出結果:
xiaomanon@xiaomanon-machine:~/Documents/c_code$ nm tooltest 0804a038 B __bss_start 0804a038 b completed.6590 0804a02c D __data_start 0804a02c W data_start 08048450 t deregister_tm_clones 080484c0 t __do_global_dtors_aux 08049f0c t __do_global_dtors_aux_fini_array_entry 0804a030 D __dso_handle 08049f14 d _DYNAMIC 0804a038 D _edata 0804a03c B _end U exit@@GLIBC_2.0 08048674 T _fini U fork@@GLIBC_2.0 08048688 R _fp_hw 080484e0 t frame_dummy 08049f08 t __frame_dummy_init_array_entry 080487e0 r __FRAME_END__ 0804a034 D global 0804a000 d _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 08048354 T _init 08049f0c t __init_array_end 08049f08 t __init_array_start 0804868c R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable 08049f10 d __JCR_END__ 08049f10 d __JCR_LIST__ w _Jv_RegisterClasses 08048670 T __libc_csu_fini 08048600 T __libc_csu_init U __libc_start_main@@GLIBC_2.0 0804850d T main U malloc@@GLIBC_2.0 U perror@@GLIBC_2.0 U printf@@GLIBC_2.0 08048480 t register_tm_clones U sleep@@GLIBC_2.0 08048410 T _start 0804a038 D __TMC_END__ 08048440 T __x86.get_pc_thunk.bx
上面便是tooltest這個程式中所有的符號,首先介紹一下上面輸出內容的格式:
■ 第一列:當前符號的地址。
■ 第二列:當前符號的型別(關於型別的說明,可以檢視手冊頁man nm詳細閱讀)。
■ 第三列:當前符號的名稱。
使用nm主要有一下幾個方面的幫助:
(1) 判斷指定的程式中有沒有指定的符號,比較常用的方式為:nm –C program | grep symbol
(2) 解決程式編譯時undefined reference的錯誤,以及multiple definition的錯誤。
(3) 檢視某個符號的地址,以及在程序空間的大概位置(.bss, .data, .text段,具體可以通過第二列的型別來判斷)。
部分符號型別說明
A: 該符號的值是絕對的,在以後的連結過程中,不允許進行改變。這樣的符號值,常常出現在中斷向量表中,例如用符號來表示各個中斷向量函式在中斷向量表中的位置。
B: 該符號的值出現在非初始化資料段(.bss)中。例如,在一個檔案中定義全域性static int test。則該符號test的型別為b,位於bss section中。其值表示該符號在bss段中的偏移。一般而言,bss段分配於RAM中 。
C: 該符號為common。common symbol是未初始話資料段。該符號沒有包含於一個普通section中。只有在連結過程中才進行分配。符號的值表示該符號需要的位元組數。例如在一個c檔案中,定義int test,並且該符號在別的地方會被引用,則該符號型別即為C。否則其型別為B。
D: 該符號位於初始化資料段中。一般來說,分配到.data section中。例如定義全域性int baud_table[5] = {9600, 19200, 38400, 57600, 115200},則會分配於初始化資料段中。
G: 該符號也位於初始化資料段中。主要用於small object提高訪問small data object的一種方式。
I: 該符號是對另一個符號的間接引用。
N: 該符號是一個debugging符號。
R: 該符號位於只讀資料段。例如定義全域性const int test[] = {123, 123};則test就是一個只讀資料區的符號。注意在cygwin下如果使用gcc直接編譯成MZ格式時,原始檔中的test對應_test,並且其符號型別為D,即初始化資料段中。但是如果使用m6812-elf-gcc這樣的交叉編譯工具,原始檔中的test對應目標檔案的test,即沒有新增下劃線,並且其符號型別為R。一般而言,位於rodata section。值得注意的是,如果在一個函式中定義const char *test = “abc”, const char test_int = 3。使用nm都不會得到符號資訊,但是字串“abc”分配於只讀儲存器中,test在rodata section中,大小為4。
S: 符號位於非初始化資料段,用於small object。
T: 該符號位於程式碼段text section。
U: 該符號在當前檔案中是未定義的,即該符號的定義在別的檔案中。例如,當前檔案呼叫另一個檔案中定義的函式,在這個被呼叫的函式在當前就是未定義的;但是在定義它的檔案中型別是T。但是對於全域性變數來說,在定義它的檔案中,其符號型別為C,在使用它的檔案中,其型別為U。
V: 該符號是一個weak object。
W: The symbol is a weak symbol that has not been specifically tagged as a weak object symbol.
-: 該符號是a.out格式檔案中的stabs symbol。
?: 該符號型別沒有定義。