Linux 系统中有大量的工具可用于 ELF 文件的二进制调试,常用的工具在 GNU binutils 包中可以找到,注意你可能需要这些工具的 x86 版本和 arm 版本,以便在调试环境中能够调试 x86 ELF 文件和 arm ELF 文件——与交叉编译器 arm-linux-gcc 类似,我们需要所谓的“交叉调试工具”,你可以通过互联网下载别人已经编译好的 crosstool ,或者自己重新编译( configure 时指 ——target=arm-linux )。
GNU binutils 包在 GNU 的官方网站提供下载: http://www.gnu.org/software/binutils/ ,特别的,更多跟 arm 相关的信息和工具可以看看 gnu arm 网站: http://www.gnuarm.org/ .我们将常用的 ELF 调试工具归纳介绍如下。由于这些工具的 x86 版本和 arm 版本使用起来基本没有区别,这里也不作区分。读者在使用的时候请根据使用对象的类型(用 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 类型的错误可以用 NM 来辅助调试。比如用 NM 结合 GREP 来查看变量或函数是否被定义或引用:$ nm [xx.o, or yy.a, or zz.so] | grep [your symbol]对于 C++ 程序,可以使用选项 -C 来进行所谓的 demangle —— C++ 编译器一般会将变量名或函数名进行修饰 (mangle) ,加上类信息、参数信息等,变成比较难以辨认的符号,而 -C 选项的 demangle 则可将其恢复为比较正常的符号。比如下面很简单的 C++ 程序:#include <iostream>
int main()
{ std::cout<<"Hello World!"<<std::endl;}编译之后用 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 之后的 C++ 符号是比较难以辨认的,如果使用 nm –C 进行 demangle 就好多了:$ nm -C hello.o 00000094 t _GLOBAL__I_main 0000003e t __static_initialization_and_destruction_0(int, int)
U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
U std::ios_base::Init::Init[in-charge]()
U std::ios_base::Init::~Init [in-charge]()
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> >&)
00000000 b std::__ioinit 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*)
U __cxa_atexit U __dso_handle U __gxx_personality_v0 0000007c t __tcf_0 00000000 T main -C 选项在其他一些二进制调试工具中也有提供,使用 C++ 开发的读者可以多加注意,毕竟 demangle 之后的符号可读性要强很多。
? OBJDUMP objdump 是所有二进制工具之母,能够显示一个目标文件中所有的信息,通常我们用它来反汇编 .text 节中的二进制指令。
比如对上面的 hello.o 反汇编的结果如下:# objdump -d hello.o
hello.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>: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 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 83 ec 08 sub $0x8,%esp 13: 68 00 00 00 00 push $0x0 18: 83 ec 0c sub $0xc,%esp 1b: 68 00 00 00 00 push $0x0 20: 68 00 00 00 00 push $0x0 25: e8 fc ff ff ff call 26 <main+0x26> 2a: 83 c4 14 add $0x14,%esp 2d: 50 push %eax 2e: e8 fc ff ff ff call 2f <main+0x2f> 33: 83 c4 10 add $0x10,%esp 36: b8 00 00 00 00 mov $0x0,%eax 3b: c9 leave 3c: c3 ret 3d: 90 nop……
注意这里用的目标文件 hello.o 和工具 objdump 都是 x86 版本的,生成的反汇编代码是 Unix 系统上传统的 AT&T 汇编,而不是多数人更熟悉的 Intel 汇编。
如果你用 ARM 格式的 hello.o ,及针对 ARM 的交叉调试工具 arm-linux-objdump ,得到的则是 ARM 汇编:$ arm-linux-objdump -d hello.o
hello.o: file format elf32-littlearm
Disassembly of section .text:…
00000180 <main>: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 <.text+0x1a8> 190: e59f1014 ldr r1, [pc, #20] ; 1ac <.text+0x1ac> 194: ebfffffe bl 194 <main+0x14> 198: e59f1010 ldr r1, [pc, #16] ; 1b0 <.text+0x1b0> 19c: ebfffffe bl 19c <main+0x1c> 1a0: e3a00000 mov r0, #0 ; 0x0 1a4: e89da800 ldmia sp, {fp, sp, pc}……
在主机的模拟环境中进行调试时,你可以用 AT&T 汇编来作为参考,但涉及到与 CPU 体系结构有关的代码时,最好还是反汇编得到 arm 汇编格式的代码,这样更为准确一些。? READELF readelf 可用来显示 ELF 格式可执行文件的信息。比如用 readelf 查看 hello.o 中的各个 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 0 0 0 [ 1] .text PROGBITS 00000000 000034 0000ae 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000754 000060 08 13 1 4 [ 3] .data PROGBITS 00000000 0000e4 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 0000e4 000001 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 0000e4 00000d 00 A 0 0 1 [ 6] .ctors PROGBITS 00000000 0000f4 000004 00 WA 0 0 4 [ 7] .rel.ctors REL 00000000 0007b4 000008 08 13 6 4 [ 8] .eh_frame PROGBITS 00000000 0000f8 000090 00 A 0 0 4 [ 9] .rel.eh_frame REL 00000000 0007bc 000028 08 13 8 4 [10] .note.GNU-stack NOTE 00000000 000188 000000 00 0 0 1 [11] .comment PROGBITS 00000000 000188 000034 00 0 0 1 [12] .shstrtab STRTAB 00000000 0001bc 00006a 00 0 0 1 [13] .symtab SYMTAB 00000000 000480 000180 10 14 e 4 [14] .strtab STRTAB 00000000 000600 000153 00 0 0 1 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 可以用来简单快速的了解 ELF 文件各个段的情况,比如:$ size hello.o text data bss dec hex filename 331 4 1 336 150 hello.o
? OBJCOPY objcopy 用来把一种目标文件中的内容复制到另一种类型的目标文件中。一般用来将复制或替换目标文件中的某些段,或者去掉某些段。
? STRINGS strings 打印某个文件的可打印字符串,这些字符串最少 4 个字符长,也可以使用选项 -n 设置字符串的最小长度。默认情况下,它只打印目标文件初始化和可加载段中的可打印字符;对于其它类型的文件它打印整个文件的可打印字符,这个程序对于了解非文本文件的内容很有帮助。
? STRIP strip :丢弃目标文件中的全部或者特定符号,可以用来减小可执行文件和库的大小。具体示例请参见下一章存储优化部分的相关内容。
? ADDR2LINE addr2line :把程序地址转换为文件名和行号。在命令行中给它一个地址和一个可执行文件名,它就会使用这个可执行文件的调试信息指出在给出的地址上是哪个文件以及行号。具体示例请参见后面的 Core Dump 分析( 9.5 节)时,如何通过寄存器 pc 的值和 addr2line 工具找出出错的 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)
ldd 最常用的地方是解决运行时找不到库的错误,如程序运行时得到的类似“ error while loading shared libraries: libxxx.so ”的错误,这时可以运行 ldd 来具体查看是缺少哪些库文。
hexdump 可执行程序 //将可执行程序导成16进制
hexdump是Linux下的一个二进制文件查看工具,可以将二进制文件转换为ASCII、10进制、16进制或8进制进行查看。
常用选项:
-b 将每个字节显示为8进制
-c 将每个字节显示为ASCII字符
-C 每个字节显示为16进制和相应的ASCII字符
-d 每两个字节显示为10进制
-o 每两个字节显示为8进制
-x 每两个字节显示为16进制
例如:
显示分为三部分:
左边为地址,中间是16进制,右边是对于的ASCII字符。
readelf 可执行程序 //可以查看文件非常多的信息
elf格式的文件分为:
shared object *.so文件
reloadable *.o文件
executable 可执行程序
file 可执行程序 //查看文件的属性
objdump -S 可执行程序 //将可执行程序返汇编
strings 可执行程序 //读取可执行程序的文件中的字符
Ubuntu Linux 查看、编辑、比较二进制文件
想根据gdb的反汇编来修改一个内核模块的汇编,没发现上面方便的工具,就直接用二进制编辑器来改了一下,还好是个简单的整型参数,该起来还比较简单。下面是用到的命令。
1. 这两个命令都可以以十六进制打印输出 二进制文件内容。可以指定偏移和打印格式等
hexdump xxd
2。 Vim 可以用来查看和编辑二进制文件
vim -b ddddd.ko 加上-b参数,以二进制打开
然后输入命令 :%!xxd -g 1 切换到十六进制模式显示
0000000: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
0000010: 01 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
0000020: 2c a7 03 00 00 00 00 00 34 00 00 00 00 00 28 00 ,.......4.....(.
0000030: 10 00 0d 00 55 89 e5 51 51 8b 45 08 85 c0 74 11 ....U..QQ.E...t.
0000040: 52 52 50 a1 00 00 00 00 50 e8 fc ff ff ff 83 c4 RRP.....P.......
0000050: 10 89 ec 5d c3 8d 76 00 55 89 e5 50 50 b8 01 00 ...]..v.U..PP...
0000060: 00 00 8b 55 08 39 d0 73 09 8d 76 00 01 c0 39 d0 ...U.9.s..v...9.
0000070: 72 fa 8b 55 04 51 52 50 a1 00 00 00 00 50 e8 fc r..U.QRP.....P..
0000080: ff ff ff 89 ec 5d c3 90 55 89 e5 50 50 b8 01 00 .....]..U..PP...
0000090: 00 00 8b 55 08 39 d0 73 09 8d 76 00 01 c0 39 d0 ...U.9.s..v...9.
00000a0: 72 fa 8b 4d 04 8b 55 10 51 52 50 a1 00 00 00 00 r..M..U.QRP.....
00000b0: 50 e8 fc ff ff ff 89 ec 5d c3 89 f6 55 89 e5 57 P.......]...U..W
00000c0: 56 53 81 ec 8c 00 00 00 8b 5d 1c 8b 45 10 85 db VS.......]..E...
00000d0: 75 7e 85 c0 75 71 50 8b 45 04 50 a1 00 00 00 00 u~..uqP.E.P.....
00000e0: 68 8c 00 00 00 50 e8 fc ff ff ff 83 c4 10 89 c2 h....P.........
然后就可以像修改文本文件一样修改16进制的字符,可以 用 / 查找指定的偏移等等。修改右边的ascii字符应该无效。
修改完成后再执行 ;%!xxd -r 切换会二进制模式,然后再 :wq 保存退出就可以了。在这vim里面这样编辑还是很方便的,注意一定要;%!xxd -r切换回来之后在保存才行。
3. linux 上面也有十六进制编辑的GUI工具
jeex (http://www.hds619.net/jeex.php)
ghex
bless (Bless is a Hex Editor for Gtk# http://home.gna.org/bless/)
UltraEdit
试过bless要比ghex好用,用bless应该可以满足大部分要求了吧。在ubuntu的软件中心中都可以的到。
4. 比较两个二进制文件,可以使用vimdiff。
vimdiff应该还是比较好用的,类似windows 平台的windiff
vim -bd base.ko base2.ko
打开后就可以在两个窗口里面显示两个文件
ctrl + W +L 把输入焦点切换到右边的窗口,激活右边的窗口后输入的命令就是针对右窗口了
:%!xxd -g 1 切换成十六进制的一个字节的模式
ctrl + W +H 把输入焦点切换到左边的窗口
:%!xxd -g 1
] + c 查找上一个不同点
[ + c 查找下一个不同点
0012930: 89 df 68 77 01 00 00 e8 fc ff| 0012930: 89 df 68 78 01 00 00 e8 fc f
一、在Linux下查看二进制文件的软件:
xxd
hexdump
二、编辑:
1、biew
2、hexedit
3、vim
Vim 来编辑二进制文件。Vim 本非为此而设计的,因而有若干局限。但你能读
取一个文件,改动一个字符,然后把它存盘。结果是你的文件就只有那一个字符给改了,
其它的就跟原来那个一模一样。
要保证 Vim 别把它那些聪明的窍门用错地方,启动 Vim 时加上 "-b" 参数:
vim -b datafile
这个参数设定了 'binary' 选项。其作用是排除所有的意外副作用。例如,'textwidth'
设为零,免得文本行给擅自排版了。并且,文件一律以 Unix 文件格式读取。
二进制模式可以用来修改某程序的消息报文。小心别插入或删除任何字符,那会让程序运
行出问题。用 "R" 命令进入替换模式。
文件里的很多字符都是不可显示的。用 Hex 格式来显示它们的值:
:set display=uhex
另外,也可以用命令 "ga" 来显示光标下的字符值。当光标位于一个 字符上时,
该命令的输出看起来就像这样:
27, Hex 1b, Octal 033
文件中也许没那么多换行符。你可以关闭 'wrap' 选项来获得总览的效果:
:set nowrap
字 节 位 置
要发现你在文件中的当前字节位置,请用这个命令:
g CTRL-G
其输出十分冗长:
Col 9-16 of 9-16; Line 277 of 330; Word 1806 of 2058; Byte 10580 of 12206
最后两个数字就是文件中的当前字节位置和文件字节总数。这已经考虑了 'fileformat'
选项导致换行符字节不同的影响。
要移到文件中某个指定的字节,请用 "go" 命令。例如,要移到字节 2345:
2345go
使 用 XXD
一个真正的二进制编辑器用两种方式来显示文本: 二进制和十六进制格式。你可以在 Vim
里通过转换程序 "xxd" 来达到这效果。该程序是随 Vim 一起发布的。
首先以二进制方式编辑这个文件:
vim -b datafile
现在用 xxd 把这个文件转换成十六进制:
:%!xxd
文本看起来像这样:
0000000: 1f8b 0808 39d7 173b 0203 7474 002b 4e49 ....9..;..tt.+NI
0000010: 4b2c 8660 eb9c ecac c462 eb94 345e 2e30 K,.`.....b..4^.0
0000020: 373b 2731 0b22 0ca6 c1a2 d669 1035 39d9 7;'1.".....i.59.
现在你可以随心所欲地阅读和编辑这些文本了。 Vim 把这些信息当作普通文本来对待。
修改了十六进制部分并不导致可显示字符部分的改变,反之亦然。
最后,用下面的命令把它转换回来:
:%!xxd -r
只有十六进制部分的修改才会被采用。右边可显示文本部分的修改忽略不计。