Linux
系統中有大量的工具可用于
ELF
檔案的二進制調試,常用的工具在
GNU binutils
包中可以找到,注意你可能需要這些工具的
x86
版本和
arm
版本,以便在調試環境中能夠調試
x86 ELF
檔案和
arm ELF
檔案——與交叉編譯器
arm-linux-gcc
類似,我們需要所謂的“交叉調試工具”,你可以通過網際網路下載下傳别人已經編譯好的
crosstool
,或者自己重新編譯(
configure
時指
--target=arm-linux
)。
包在
GNU
的官方網站提供下載下傳:
<a href="http://www.gnu.org/software/binutils/" target="_blank">http://www.gnu.org/software/binutils/</a>
,特别的,更多跟
相關的資訊和工具可以看看
gnu arm
網站:
<a href="http://www.gnuarm.org/" target="_blank">http://www.gnuarm.org/</a>
。
我們将常用的
調試工具歸納介紹如下。由于這些工具的
版本使用起來基本沒有差別,這裡也不作區分。讀者在使用的時候請根據使用對象的類型(用
FILE
指令檢視)自行區分。
Ø
AR
用來建立、修改、提取靜态庫檔案。靜态庫檔案包含多個可重定位目标檔案,其結構保證了可以恢複原始目标檔案内容。比如:
$ gcc –c file1.c file2.c
$ ar rcs libxx.a file1.o file2.o
這裡我們先用
gcc
編譯得到
file1.o file2.o
兩個目标檔案,然後用
ar
指令生成靜态庫
libxx.a
當你希望檢視靜态庫中包含了哪些目标檔案時,可以用選項
-x
解開靜态庫檔案:
$ ar x libxx.a
NM
列出目标檔案的符号表中定義的符号。常見的連結或者運作時發生的
unresolved symbol
類型的錯誤可以用
來輔助調試。比如用
結合
GREP
來檢視變量或函數是否被定義或引用:
$ nm [xx.o, or yy.a, or zz.so] | grep [your symbol]
對于
C++
程式,可以使用選項
-C
來進行所謂的
demangle
——
編譯器一般會将變量名或函數名進行修飾
(mangle)
,加上類資訊、參數資訊等,變成比較難以辨認的符号,而
選項的
則可将其恢複為比較正常的符号。比如下面很簡單的
程式:
#include
int main()
{
std::cout
}
編譯之後用
nm
來檢視:
$ g++ -c hello.cpp
$ nm hello.o
00000094 t _GLOBAL__I_main
0000003e t _Z41__static_initialization_and_destruction_0ii
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
00000000 b _ZSt8__ioinit
U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
U __cxa_atexit
U __dso_handle
U __gxx_personality_v0
0000007c
t __tcf_0
00000000 T main
這時這些
mangle
之後的
符号是比較難以辨認的,如果使用
nm –C
進行
就好多了:
$ nm -C hello.o
0000003e t __static_initialization_and_destruction_0(int, int)
U
std::basic_ostream
>::operator >& (*)(std::basic_ostream >&))
U std::ios_base::Init::Init[in-charge]()
U std::ios_base::Init::~Init [in-charge]()
U std::cout
std::basic_ostream >&
std::endl
>(std::basic_ostream >&)
00000000 b std::__ioinit
std::operator
>(std::basic_ostream
>&, char const*)
選項在其他一些二進制調試工具中也有提供,使用
開發的讀者可以多加注意,畢竟
之後的符号可讀性要強很多。
OBJDUMP
objdump
是所有二進制工具之母,能夠顯示一個目标檔案中所有的資訊,通常我們用它來反彙編
.text
節中的二進制指令。
比如對上面的
hello.o
反彙編的結果如下:
# objdump -d hello.o
hello.o:
file format elf32-i386
Disassembly of section .text:
00000000 :
0:
55
push
%ebp
1:
89 e5
mov
%esp,%ebp
3:
83 ec 08
sub
$0x8,%esp
6:
83 e4 f0
and
$0xfffffff0,%esp
9:
b8 00 00 00 00
$0x0,%eax
e:
29 c4
%eax,%esp
10:
13:
68 00 00 00 00
$0x0
18:
83 ec 0c
$0xc,%esp
1b:
20:
25:
e8 fc ff ff ff
call
26
2a:
83 c4 14
add
$0x14,%esp
2d:
50
%eax
2e:
2f
33:
83 c4 10
$0x10,%esp
36:
3b:
c9
leave
3c:
c3
ret
3d:
90
nop
...
注意這裡用的目标檔案
和工具
都是
版本的,生成的反彙編代碼是
Unix
系統上傳統的
AT&T
彙編,而不是多數人更熟悉的
Intel
彙編。
如果你用
ARM
格式的
,及針對
的交叉調試工具
arm-linux-objdump
,得到的則是
彙編:
$ arm-linux-objdump -d hello.o
file format elf32-littlearm
…
00000180 :
180:
e1a0c00d
mov
ip, sp
184:
e92dd800
stmdb
sp!, {fp, ip, lr, pc}
188:
e24cb004
sub
fp, ip, #4
; 0x4
18c:
e59f0014
ldr
r0, [pc, #20]
; 1a8
190:
e59f1014
r1, [pc, #20]
; 1ac
194:
ebfffffe
bl
194
198:
e59f1010
r1, [pc, #16]
; 1b0
19c:
19c
1a0:
e3a00000
r0, #0
; 0x0
1a4:
e89da800
ldmia
sp, {fp, sp, pc}
在主機的模拟環境中進行調試時,你可以用
彙編來作為參考,但涉及到與
CPU
體系結構有關的代碼時,最好還是反彙編得到
彙編格式的代碼,這樣更為準确一些。
READELF
readelf
可用來顯示
格式可執行檔案的資訊。比如用
檢視
中的各個
Section
的結果如下:
$ readelf -S hello.o
There are 15 section headers, starting at offset 0x228:
Section Headers:
[Nr] Name
Type
Addr
Off
Size
ES Flg Lk Inf Al
[ 0]
NULL
00000000 000000 000000 00
[ 1] .text
PROGBITS
00000000 000034 0000ae 00
AX
4
[ 2] .rel.text
REL
00000000 000754 000060 08
13
1
[ 3] .data
00000000 0000e4 000000 00
WA
[ 4] .bss
NOBITS
00000000 0000e4 000001 00
[ 5] .rodata
00000000 0000e4 00000d 00
A
1
[ 6] .ctors
00000000 0000f4 000004 00
[ 7] .rel.ctors
00000000 0007b4 000008 08
6
[ 8] .eh_frame
00000000 0000f8 000090 00
[ 9] .rel.eh_frame
00000000 0007bc 000028 08
8
[10] .note.GNU-stack
NOTE
00000000 000188 000000 00
[11] .comment
00000000 000188 000034 00
[12] .shstrtab
STRTAB
00000000 0001bc 00006a 00
[13] .symtab
SYMTAB
00000000 000480 000180 10
14
e
[14] .strtab
STRTAB
00000000 000600 000153 00
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
SIZE
size
指令可以列出目标檔案每一段的大小以及總體的大小。預設情況下,對于每個目标檔案或者一個歸檔檔案中的每個子產品隻産生一行輸出。
可以用來簡單快速的了解
檔案各個段的情況,比如:
$ size hello.o
text
data
bss
dec
hex filename
331
4
1
336
150 hello.o
OBJCOPY
objcopy
用來把一種目标檔案中的内容複制到另一種類型的目标檔案中。一般用來将複制或替換目标檔案中的某些段,或者去掉某些段。
STRINGS
strings
列印某個檔案的可列印字元串,這些字元串最少
個字元長,也可以使用選項
-n
設定字元串的最小長度。預設情況下,它隻列印目标檔案初始化和可加載段中的可列印字元;對于其它類型的檔案它列印整個檔案的可列印字元,這個程式對于了解非文本檔案的内容很有幫助。
STRIP
strip
:丢棄目标檔案中的全部或者特定符号,可以用來減小可執行檔案和庫的大小。具體示例請參見下一章存儲優化部分的相關内容。
ADDR2LINE
addr2line
:把程式位址轉換為檔案名和行号。在指令行中給它一個位址和一個可執行檔案名,它就會使用這個可執行檔案的調試資訊指出在給出的位址上是哪個檔案以及行号。具體示例請參見後面的
Core Dump
分析(
9.5
節)時,如何通過寄存器
pc
的值和
工具找出出錯的
C/C++
源代碼。
LDD
ldd
可用來顯示執行檔案需要哪些共享庫
,
共享庫裝載管理器在哪裡找到了需要的共享庫。比如:
# ldd hello
libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40026000)
libm.so.6 => /lib/tls/libm.so.6 (0x400d9000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x400fb000)
libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
最常用的地方是解決運作時找不到庫的錯誤,如程式運作時得到的類似“
error while loading shared libraries: libxxx.so
”的錯誤,這時可以運作
來具體檢視是缺少哪些庫文