新聞中心
GNU Binary Utilities或binutils是一整套的編程語言工具程序,用來處理許多格式的目標(biāo)文件。當(dāng)前的版本原本由在Cygnus Solutions的程序員以Binary File Descriptor library(libbfd)所撰寫。這個(gè)工具程序通常搭配GCC、make、和GDB這些程序來使用。

探索 binutils 工具
上面這個(gè)練習(xí)為使用 binutils 軟件包中的工具提供了良好的背景。我的系統(tǒng)帶有 binutils 版本 2.27-34;你的 Linux 發(fā)行版上的版本可能有所不同。
[~]# rpm -qa | grep binutils
binutils-2.27-34.base.el7.x86_64
binutils 軟件包中提供了以下工具:
[~]# rpm -ql binutils-2.27-34.base.el7.x86_64 | grep bin/
/usr/bin/addr2line
/usr/bin/ar
/usr/bin/as
/usr/bin/c++filt
/usr/bin/dwp
/usr/bin/elfedit
/usr/bin/gprof
/usr/bin/ld
/usr/bin/ld.bfd
/usr/bin/ld.gold
/usr/bin/nm
/usr/bin/objcopy
/usr/bin/objdump
/usr/bin/ranlib
/usr/bin/readelf
/usr/bin/size
/usr/bin/strings
/usr/bin/strip
上面的編譯練習(xí)已經(jīng)探索了其中的兩個(gè)工具:用作匯編器的 as 命令,用作鏈接器的 ld 命令。繼續(xù)閱讀以了解上述 GNU binutils 軟件包工具中的其他七個(gè)。
readelf:顯示 ELF 文件信息
上面的練習(xí)提到了術(shù)語“目標(biāo)文件”和“可執(zhí)行文件”。使用該練習(xí)中的文件,通過帶有 -h(標(biāo)題)選項(xiàng)的 readelf 命令,以將文件的 ELF 標(biāo)題轉(zhuǎn)儲到屏幕上。請注意,以 .o 擴(kuò)展名結(jié)尾的目標(biāo)文件顯示為 Type: REL (Relocatable file)(可重定位文件):
[testdir]# readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 [...]
[...]
Type: REL (Relocatable file)
[...]
如果嘗試執(zhí)行此目標(biāo)文件,會收到一條錯(cuò)誤消息,指出無法執(zhí)行。這僅表示它尚不具備在 CPU 上執(zhí)行所需的信息。
請記住,你首先需要使用 chmod 命令在對象文件上添加 x(可執(zhí)行位),否則你將得到“權(quán)限被拒絕”的錯(cuò)誤。
[testdir]# ./hello.o
bash: ./hello.o: Permission denied
[testdir]# chmod +x ./hello.o
[testdir]#
[testdir]# ./hello.o
bash: ./hello.o: cannot execute binary file
如果對 a.out 文件嘗試相同的命令,則會看到其類型為 EXEC (Executable file)(可執(zhí)行文件)。
[testdir]# readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
[...] Type: EXEC (Executable file)
如上所示,該文件可以直接由 CPU 執(zhí)行:
[testdir]# ./a.out Hello World
readelf 命令可提供有關(guān)二進(jìn)制文件的大量信息。在這里,它會告訴你它是 ELF 64 位格式,這意味著它只能在 64 位 CPU 上執(zhí)行,而不能在 32 位 CPU 上運(yùn)行。它還告訴你它應(yīng)在 X86-64(Intel/AMD)架構(gòu)上執(zhí)行。該二進(jìn)制文件的入口點(diǎn)是地址 0x400430,它就是 C 源程序中 main 函數(shù)的地址。
在你知道的其他系統(tǒng)二進(jìn)制文件上嘗試一下 readelf 命令,例如 ls。請注意,在 RHEL 8 或 Fedora 30 及更高版本的系統(tǒng)上,由于安全原因改用了位置無關(guān)可執(zhí)行文件position independent executable(PIE),因此你的輸出(尤其是 Type:)可能會有所不同。
[testdir]# readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file)
使用 ldd 命令了解 ls 命令所依賴的系統(tǒng)庫,如下所示:
[testdir]# ldd /bin/ls
linux-vdso.so.1 => (0x00007ffd7d746000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f060daca000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f060d8c5000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007f060d6bc000)
libc.so.6 => /lib64/libc.so.6 (0x00007f060d2ef000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f060d08d000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f060ce89000)
/lib64/ld-linux-x86-64.so.2 (0x00007f060dcf1000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f060cc84000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f060ca68000)
對 libc 庫文件運(yùn)行 readelf 以查看它是哪種文件。正如它指出的那樣,它是一個(gè) DYN (Shared object file)(共享對象文件),這意味著它不能直接執(zhí)行;必須由內(nèi)部使用了該庫提供的任何函數(shù)的可執(zhí)行文件使用它。
[testdir]# readelf -h /lib64/libc.so.6
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - GNU ABI Version: 0 Type: DYN (Shared object file)
size:列出節(jié)的大小和全部大小
size 命令僅適用于目標(biāo)文件和可執(zhí)行文件,因此,如果嘗試在簡單的 ASCII 文件上運(yùn)行它,則會拋出錯(cuò)誤,提示“文件格式無法識別”
[testdir]# echo "test" > file1
[testdir]# cat file1
test
[testdir]# file file1
file1: ASCII text
[testdir]# size file1
size: file1: File format not recognized
現(xiàn)在,在上面的練習(xí)中,對目標(biāo)文件和可執(zhí)行文件運(yùn)行 size 命令。請注意,根據(jù) size 命令的輸出可以看出,可執(zhí)行文件(a.out)的信息要比目標(biāo)文件(hello.o)多得多:
[testdir]# size hello.o
text data bss dec hex filename
89 0 0 89 59 hello.o
[testdir]# size a.out
text data bss dec hex filename
1194 540 4 1738 6ca a.out
但是這里的 text、data 和 bss 節(jié)是什么意思?
text 節(jié)是指二進(jìn)制文件的代碼部分,其中包含所有可執(zhí)行指令。data 節(jié)是所有初始化數(shù)據(jù)所在的位置,bss 節(jié)是所有未初始化數(shù)據(jù)的存儲位置。(LCTT 譯注:一般來說,在靜態(tài)的映像文件中,各個(gè)部分稱之為節(jié)section,而在運(yùn)行時(shí)的各個(gè)部分稱之為段segment,有時(shí)統(tǒng)稱為段。)
比較其他一些可用的系統(tǒng)二進(jìn)制文件的 size 結(jié)果。
對于 ls 命令:
[testdir]# size /bin/ls
text data bss dec hex filename
103119 4768 3360 111247 1b28f /bin/ls
只需查看 size 命令的輸出,你就可以看到 gcc 和 gdb 是比 ls 大得多的程序:
[testdir]# size /bin/gcc
text data bss dec hex filename
755549 8464 81856 845869 ce82d /bin/gcc
[testdir]# size /bin/gdb
text data bss dec hex filename
6650433 90842 152280 6893555 692ff3 /bin/gdb
strings:打印文件中的可打印字符串
在 strings 命令中添加 -d 標(biāo)志以僅顯示 data 節(jié)中的可打印字符通常很有用。
hello.o 是一個(gè)目標(biāo)文件,其中包含打印出 Hello World 文本的指令。因此,strings 命令的唯一輸出是 Hello World。
[testdir]# strings -d hello.o
Hello World
另一方面,在 a.out(可執(zhí)行文件)上運(yùn)行 strings 會顯示在鏈接階段該二進(jìn)制文件中包含的其他信息:
[testdir]# strings -d a.out
/lib64/ld-linux-x86-64.so.2
!^BU
libc.so.6
puts
__libc_start_main
__gmon_start__
GLIBC_2.2.5
UH-0
UH-0
=(
[]A\A]A^A_
Hello World
;*3$"
objdump:顯示目標(biāo)文件信息
另一個(gè)可以從二進(jìn)制文件中轉(zhuǎn)儲機(jī)器語言指令的 binutils 工具稱為 objdump。使用 -d 選項(xiàng),可從二進(jìn)制文件中反匯編出所有匯編指令。
回想一下,編譯是將源代碼指令轉(zhuǎn)換為機(jī)器代碼的過程。機(jī)器代碼僅由 1 和 0 組成,人類難以閱讀。因此,它有助于將機(jī)器代碼表示為匯編語言指令。匯編語言是什么樣的?請記住,匯編語言是特定于體系結(jié)構(gòu)的;由于我使用的是 Intel(x86-64)架構(gòu),因此如果你使用 ARM 架構(gòu)編譯相同的程序,指令將有所不同。
[testdir]# objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000
:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 retq
該輸出乍一看似乎令人生畏,但請花一點(diǎn)時(shí)間來理解它,然后再繼續(xù)?;叵胍幌?,.text 節(jié)包含所有的機(jī)器代碼指令。匯編指令可以在第四列中看到(即 push、mov、callq、pop、retq 等)。這些指令作用于寄存器,寄存器是 CPU 內(nèi)置的存儲器位置。本示例中的寄存器是 rbp、rsp、edi、eax 等,并且每個(gè)寄存器都有特殊的含義。
現(xiàn)在對可執(zhí)行文件(a.out)運(yùn)行 objdump 并查看得到的內(nèi)容。可執(zhí)行文件的 objdump 的輸出可能很大,因此我使用 grep 命令將其縮小到 main 函數(shù):
[testdir]# objdump -d a.out | grep -A 9 main\>
000000000040051d
:
40051d: 55 push %rbp
40051e: 48 89 e5 mov %rsp,%rbp
400521: bf d0 05 40 00 mov $0x4005d0,%edi
400526: e8 d5 fe ff ff callq 400400
40052b: b8 00 00 00 00 mov $0x0,%eax
400530: 5d pop %rbp
400531: c3 retq
請注意,這些指令與目標(biāo)文件 hello.o 相似,但是其中包含一些其他信息:
目標(biāo)文件 hello.o 具有以下指令:callq e 可執(zhí)行文件 a.out 由以下指令組成,該指令帶有一個(gè)地址和函數(shù):callq 400400 上面的匯編指令正在調(diào)用 puts 函數(shù)。請記住,你在源代碼中使用了一個(gè) printf 函數(shù)。編譯器插入了對 puts 庫函數(shù)的調(diào)用,以將 Hello World 輸出到屏幕。 查看 put 上方一行的說明:
目標(biāo)文件 hello.o 有個(gè)指令 mov:mov 0x4005d0)而不是 :0x4005d0,%edi 該指令將二進(jìn)制文件中地址 $0x4005d0 處存在的內(nèi)容移動(dòng)到名為 edi 的寄存器中。
這個(gè)存儲位置的內(nèi)容中還能是別的什么嗎?是的,你猜對了:它就是文本 Hello, World。你是如何確定的?
readelf 命令使你可以將二進(jìn)制文件(a.out)的任何節(jié)轉(zhuǎn)儲到屏幕上。以下要求它將 .rodata(這是只讀數(shù)據(jù))轉(zhuǎn)儲到屏幕上:
[testdir]# readelf -x .rodata a.out
Hex dump of section '.rodata':
0x004005c0 01000200 00000000 00000000 00000000 ....
0x004005d0 48656c6c 6f20576f 726c6400 Hello World.
你可以在右側(cè)看到文本 Hello World,在左側(cè)可以看到其二進(jìn)制格式的地址。它是否與你在上面的 mov 指令中看到的地址匹配?是的,確實(shí)匹配。
strip:從目標(biāo)文件中剝離符號
該命令通常用于在將二進(jìn)制文件交付給客戶之前減小二進(jìn)制文件的大小。
請記住,由于重要信息已從二進(jìn)制文件中刪除,因此它會妨礙調(diào)試。但是,這個(gè)二進(jìn)制文件可以完美地執(zhí)行。
對 a.out 可執(zhí)行文件運(yùn)行該命令,并注意會發(fā)生什么。首先,通過運(yùn)行以下命令確保二進(jìn)制文件沒有被剝離(not stripped):
[testdir]# file a.out
a.out: ELF 64-bit LSB executable, x86-64, [......] not stripped
另外,在運(yùn)行 strip 命令之前,請記下二進(jìn)制文件中最初的字節(jié)數(shù):
[testdir]# du -b a.out
8440 a.out
現(xiàn)在對該可執(zhí)行文件運(yùn)行 strip 命令,并使用 file 命令以確保正常完成:
[testdir]# strip a.out
[testdir]# file a.out a.out: ELF 64-bit LSB executable, x86-64, [......] stripped
剝離該二進(jìn)制文件后,此小程序的大小從之前的 8440 字節(jié)減小為 6296 字節(jié)。對于這樣小的一個(gè)程序都能有這么大的空間節(jié)省,難怪大型程序經(jīng)常被剝離。
[testdir]# du -b a.out
6296 a.out
addr2line:轉(zhuǎn)換地址到文件名和行號
addr2line 工具只是在二進(jìn)制文件中查找地址,并將其與 C 源代碼程序中的行進(jìn)行匹配。很酷,不是嗎?
為此編寫另一個(gè)測試程序;只是這一次確保使用 gcc 的 -g 標(biāo)志進(jìn)行編譯,這將為二進(jìn)制文件添加其它調(diào)試信息,并包含有助于調(diào)試的行號(由源代碼中提供):
[testdir]# cat -n atest.c
1 #include
2
3 int globalvar = 100;
4
5 int function1(void)
6 {
7 printf("Within function1\n");
8 return 0;
9 }
10
11 int function2(void)
12 {
13 printf("Within function2\n");
14 return 0;
15 }
16
17 int main(void)
18 {
19 function1();
20 function2();
21 printf("Within main\n");
22 return 0;
23 }
用 -g 標(biāo)志編譯并執(zhí)行它。正如預(yù)期:
[testdir]# gcc -g atest.c
[testdir]# ./a.out
Within function1
Within function2
Within main
現(xiàn)在使用 objdump 來標(biāo)識函數(shù)開始的內(nèi)存地址。你可以使用 grep 命令來過濾出所需的特定行。函數(shù)的地址在下面突出顯示(55 push %rbp 前的地址):
[testdir]# objdump -d a.out | grep -A 2 -E 'main>:|function1>:|function2>:'
000000000040051d :
40051d: 55 push %rbp
40051e: 48 89 e5 mov %rsp,%rbp
--
0000000000400532 :
400532: 55 push %rbp
400533: 48 89 e5 mov %rsp,%rbp
--
0000000000400547
:
400547: 55 push %rbp
400548: 48 89 e5 mov %rsp,%rbp
現(xiàn)在,使用 addr2line 工具從二進(jìn)制文件中的這些地址映射到 C 源代碼匹配的地址:
[testdir]# addr2line -e a.out 40051d
/tmp/testdir/atest.c:6
[testdir]#
[testdir]# addr2line -e a.out 400532
/tmp/testdir/atest.c:12
[testdir]#
[testdir]# addr2line -e a.out 400547
/tmp/testdir/atest.c:18
它說 40051d 從源文件 atest.c 中的第 6 行開始,這是 function1 的起始大括號({)開始的行。function2 和 main 的輸出也匹配。
nm:列出目標(biāo)文件的符號
使用上面的 C 程序測試 nm 工具。使用 gcc 快速編譯并執(zhí)行它。
[testdir]# gcc atest.c
[testdir]# ./a.out
Within function1
Within function2
Within main
現(xiàn)在運(yùn)行 nm 和 grep 獲取有關(guān)函數(shù)和變量的信息:
[testdir]# nm a.out | grep -Ei 'function|main|globalvar'
000000000040051d T function1
0000000000400532 T function2
000000000060102c D globalvar
U __libc_start_main@@GLIBC_2.2.5
0000000000400547 T main
你可以看到函數(shù)被標(biāo)記為 T,它表示 text 節(jié)中的符號,而變量標(biāo)記為 D,表示初始化的 data 節(jié)中的符號。
想象一下在沒有源代碼的二進(jìn)制文件上運(yùn)行此命令有多大用處?這使你可以窺視內(nèi)部并了解使用了哪些函數(shù)和變量。當(dāng)然,除非二進(jìn)制文件已被剝離,這種情況下它們將不包含任何符號,因此 nm 就命令不會很有用,如你在此處看到的:
[testdir]# strip a.out
[testdir]# nm a.out | grep -Ei 'function|main|globalvar'
nm: a.out: no symbols
結(jié)論
GNU binutils 工具為有興趣分析二進(jìn)制文件的人提供了許多選項(xiàng),這只是它們可以為你做的事情的冰山一角。請閱讀每種工具的手冊頁,以了解有關(guān)它們以及如何使用它們的更多信息。
當(dāng)前題目:9個(gè)常用的GNUbinutils工具
標(biāo)題路徑:http://www.fisionsoft.com.cn/article/dhijpje.html


咨詢
建站咨詢
