objdump、readelf
都是 linux
系统下的一款二进制目标文件分析工具,它可以用来查看反汇编后的文件,文件中各个段的信息,调试信息以及符号表等内容。
我们就以下面的程序来详细说明 objdump
、readelf
的使用,分别有 foo.c/foo.h
和 main.c
这几个源代码文件。
// foo.h
#ifndef _FOO_HPP_
#define _FOO_HPP_
static int global_int_var = 1024;
static char* global_string_var = "hello world!";
int global_func(int a, int b);
#endif
// foo.cpp
#include "foo.hpp"
int global_func(int a, int b) {
return a + b;
}
// main.cpp
#include <cstdio>
#include "foo.hpp"
int main() {
int c = global_func(12, global_int_var);
printf("%s\n", global_string_var);
printf("global_int_var=%d; c=%d\n", global_int_var, c);
return 0;
}
在上面源码所在目录下再写一个 makefile
脚本文件方便编译。
all: main
main: main.cpp foo.so
gcc -Wl,-rpath=. -o $@ -g $^ -L.
foo.so: foo.o
gcc -o $@ $^ -shared -fPIC
foo.o: foo.cpp
gcc -o $@ -g -c $<
clean:
rm -rf main *.o *.so
执行 make
命令后就生成了可执行文件 main
,执行 ./main
命令可以看到程序正常输出,下面我们就以 objdump
、readelf
这两个工具来窥探一下 main
这个可执行文件的大致结构。
首先我们使用一个二进制查看器打开 main
这个可执行文件看看里面长得怎么样,大概如下:
这个文件其实非常长,我们这里只截取了开头的一部分,可以看到最左边和最上面类似坐标系那样标志了中间每个字节的位置,最右面部分显示中间每个字节对应的文本。一般这种通用格式的第一行都会是某种标志数字,也就是如上图中的 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
。
从这里也可以预见,不管是什么类型的文件,可执行文件也好,视频文件也好,音乐文件也好,图片文件也好,你用二进制编辑器查看就可以发现,这一切的一切都只是一行行的二进制序列而已。
查看ELF文件头
我们使用 readelf -h main
命令来查看 main
可执行文件的文件头显示如下:
zmant@PC-201807220249:~/test$ readelf -h main
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: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x5b0
Start of program headers: 64 (bytes into file)
Start of section headers: 11096 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 34
Section header string table index: 33
可以看到如上图所示的第一行正是表示 ELF 文件头中的 Magic Number,也就是魔数。另外从上面的输出还可以看到 ELF 文件头的结构组成,包括 ELF 魔数,ELF 类型,数据存储方式,版本,系统 ABI,ABI 版本,文件类型,机器硬件平台,硬件平台版本,程序入口地址,程序头入口和长度,段表的位置和长度,以及段的数量等。
归纳起来我们可以通过上面命令(readelf -h
)的输出知道该文件是个共享目标文件,属于 ELF64,数据是小端存储的,运行在 x86-64 平台的 Unix 系统中。
对程序进行反汇编
我们从上面知道了程序的入口地址是 0x5b0
,并且在编程入门的第一节课上你就知道了 main()
函数是程序的入口,所以聪明的你可能会笃定 main()
函数的地址就是 0x5b0
,到底是不是这样呢?我们可以通过 objdump -d
反汇编一下就知晓了 。
zmant@PC-201807220249:~/test$ objdump -d main > main.s
zmant@PC-201807220249:~/test$ cat main.s
main: file format elf64-x86-64
......
Disassembly of section .text:
00000000000005b0 <_start>:
5b0: 31 ed xor %ebp,%ebp
5b2: 49 89 d1 mov %rdx,%r9
5b5: 5e pop %rsi
5b6: 48 89 e2 mov %rsp,%rdx
5b9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
5bd: 50 push %rax
5be: 54 push %rsp
5bf: 4c 8d 05 ca 01 00 00 lea 0x1ca(%rip),%r8 # 790 <__libc_csu_fini>
5c6: 48 8d 0d 53 01 00 00 lea 0x153(%rip),%rcx # 720 <__libc_csu_init>
5cd: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 6ba <main>
5d4: ff 15 06 0a 20 00 callq *0x200a06(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
5da: f4 hlt
5db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
......
00000000000006ba <main>:
6ba: 55 push %rbp
6bb: 48 89 e5 mov %rsp,%rbp
6be: 48 83 ec 10 sub $0x10,%rsp
6c2: 8b 05 48 09 20 00 mov 0x200948(%rip),%eax # 201010 <_ZL14global_int_var>
6c8: 89 c6 mov %eax,%esi
6ca: bf 0c 00 00 00 mov $0xc,%edi
6cf: e8 35 00 00 00 callq 709 <_Z11global_funcii>
6d4: 89 45 fc mov %eax,-0x4(%rbp)
6d7: 48 8b 05 3a 09 20 00 mov 0x20093a(%rip),%rax # 201018 <_ZL17global_string_var>
6de: 48 89 c7 mov %rax,%rdi
6e1: e8 9a fe ff ff callq 580 <puts@plt>
6e6: 8b 05 24 09 20 00 mov 0x200924(%rip),%eax # 201010 <_ZL14global_int_var>
6ec: 8b 55 fc mov -0x4(%rbp),%edx
6ef: 89 c6 mov %eax,%esi
6f1: 48 8d 3d b8 00 00 00 lea 0xb8(%rip),%rdi # 7b0 <_IO_stdin_used+0x10>
6f8: b8 00 00 00 00 mov $0x0,%eax
6fd: e8 8e fe ff ff callq 590 <printf@plt>
702: b8 00 00 00 00 mov $0x0,%eax
707: c9 leaveq
708: c3 retq
0000000000000709 <_Z11global_funcii>:
709: 55 push %rbp
70a: 48 89 e5 mov %rsp,%rbp
70d: 89 7d fc mov %edi,-0x4(%rbp)
710: 89 75 f8 mov %esi,-0x8(%rbp)
713: 8b 55 fc mov -0x4(%rbp),%edx
716: 8b 45 f8 mov -0x8(%rbp),%eax
719: 01 d0 add %edx,%eax
71b: 5d pop %rbp
71c: c3 retq
71d: 0f 1f 00 nopl (%rax)
......
事情好像有点出乎意料了,我们找到了 main()
函数,但是我们发现它的地址是 0x6ba
,并不是我们认为的 0x5b0
,这是怎么回事?难道是 readelf -h
打印了错误的入口地址?还是 objdump -d
错误的反汇编了 main()
函数的地址?
其实都不是,我们继续在 main.s 中搜索 5b0
这个字符串,可以发现它对应的是 _start()
函数的地址,仔细看 _start()
函数那段汇编可以发现它里面有调用 main()
函数。
00000000000005b0 <_start>:
......
5bf: 4c 8d 05 ca 01 00 00 lea 0x1ca(%rip),%r8 # 790 <__libc_csu_fini>
5c6: 48 8d 0d 53 01 00 00 lea 0x153(%rip),%rcx # 720 <__libc_csu_init>
5cd: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 6ba <main>
5d4: ff 15 06 0a 20 00 callq *0x200a06(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
......
可以看到,在调用 main()
函数之前 _start()
其实还调用了两个初始化函数,初始化完成之后才开始调用 main()
函数,所以到这里我们通过反汇编的方式了解到程序真正的入口函数是 _start()
函数,而不是我们一直以为的 main()
函数。objdump
工具当记一功啊。
窥探文件中的符号表
在手写 Makefile
的时候可能有时候会经常遇到未定义的符号或重复定义的符号等各种错误。顾名思义,未定义的符号就是在目标文件的符号表中找不到对应的符号,但在目标文件的代码段中又有对该符号的引用;而重复定义的符号就是在该目标文件中出现了两个或两个以上相同的符号。这些问题其实都和目标文件中符号表中保存的符号有关,所以现在我们就来了解一下如何查看目标文件中符号表的内容。
我们预想中符号表的内容可能就只是 "global_int_var", "global_string_var", "global_func", "printf", "main" ...
这些出现在我们程序中的符号,实际又相差多少呢?我们运行 readelf -s main
来看看:
zmant@PC-201807220249:~/test$ readelf -s main
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 75 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
2: 0000000000000254 0 SECTION LOCAL DEFAULT 2
3: 0000000000000274 0 SECTION LOCAL DEFAULT 3
4: 0000000000000298 0 SECTION LOCAL DEFAULT 4
5: 00000000000002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000000378 0 SECTION LOCAL DEFAULT 6
7: 0000000000000402 0 SECTION LOCAL DEFAULT 7
8: 0000000000000418 0 SECTION LOCAL DEFAULT 8
9: 0000000000000438 0 SECTION LOCAL DEFAULT 9
10: 0000000000000528 0 SECTION LOCAL DEFAULT 10
11: 0000000000000558 0 SECTION LOCAL DEFAULT 11
12: 0000000000000570 0 SECTION LOCAL DEFAULT 12
13: 00000000000005a0 0 SECTION LOCAL DEFAULT 13
14: 00000000000005b0 0 SECTION LOCAL DEFAULT 14
15: 0000000000000794 0 SECTION LOCAL DEFAULT 15
16: 00000000000007a0 0 SECTION LOCAL DEFAULT 16
17: 00000000000007d8 0 SECTION LOCAL DEFAULT 17
18: 0000000000000820 0 SECTION LOCAL DEFAULT 18
19: 0000000000200db0 0 SECTION LOCAL DEFAULT 19
20: 0000000000200db8 0 SECTION LOCAL DEFAULT 20
21: 0000000000200dc0 0 SECTION LOCAL DEFAULT 21
22: 0000000000200fb0 0 SECTION LOCAL DEFAULT 22
23: 0000000000201000 0 SECTION LOCAL DEFAULT 23
24: 0000000000201030 0 SECTION LOCAL DEFAULT 24
25: 0000000000000000 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
28: 0000000000000000 0 SECTION LOCAL DEFAULT 28
29: 0000000000000000 0 SECTION LOCAL DEFAULT 29
30: 0000000000000000 0 SECTION LOCAL DEFAULT 30
31: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
32: 00000000000005e0 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
33: 0000000000000620 0 FUNC LOCAL DEFAULT 14 register_tm_clones
34: 0000000000000670 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
35: 0000000000201030 1 OBJECT LOCAL DEFAULT 24 completed.7698
36: 0000000000200db8 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
37: 00000000000006b0 0 FUNC LOCAL DEFAULT 14 frame_dummy
38: 0000000000200db0 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
39: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
40: 0000000000201010 4 OBJECT LOCAL DEFAULT 23 _ZL14global_int_var
41: 0000000000201018 8 OBJECT LOCAL DEFAULT 23 _ZL17global_string_var
42: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.cpp
43: 0000000000201020 4 OBJECT LOCAL DEFAULT 23 _ZL14global_int_var
44: 0000000000201028 8 OBJECT LOCAL DEFAULT 23 _ZL17global_string_var
45: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
46: 0000000000000944 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
47: 0000000000000000 0 FILE LOCAL DEFAULT ABS
48: 0000000000200db8 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
49: 0000000000200dc0 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
50: 0000000000200db0 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
51: 00000000000007d8 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
52: 0000000000200fb0 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
53: 0000000000000790 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
54: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
55: 0000000000201000 0 NOTYPE WEAK DEFAULT 23 data_start
56: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
57: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 23 _edata
58: 0000000000000794 0 FUNC GLOBAL DEFAULT 15 _fini
59: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
60: 0000000000000709 20 FUNC GLOBAL DEFAULT 14 _Z11global_funcii
61: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
62: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
63: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
64: 0000000000201008 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
65: 00000000000007a0 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
66: 0000000000000720 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
67: 0000000000201038 0 NOTYPE GLOBAL DEFAULT 24 _end
68: 00000000000005b0 43 FUNC GLOBAL DEFAULT 14 _start
69: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
70: 00000000000006ba 79 FUNC GLOBAL DEFAULT 14 main
71: 0000000000201030 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
72: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
73: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
74: 0000000000000558 0 FUNC GLOBAL DEFAULT 11 _init
毫无悬念,除了我们预料的符号外,还有很多我们不知道的符号也在符号表里面,这也很容易理解,因为通过上面反汇编的过程我们知道了程序在进入 main()
函数之前先调用了入口函数 _start()
并做了一些初始化的工作,这些动作都是编译器帮我们做好了,这些肯定是会产生一些符号的。再者,我们最终的可执行文件 main
也需要链接系统的一些库来完成输出的操作,这些库里面肯定也是存在符号的。所以有这么多我们不认识的符号也就不奇怪了。
除了 readelf -s
外,我们通常还会对照使用 objdump -t
命令来查看符号表。
zmant@PC-201807220249:~/test$ objdump -t main
main: file format elf64-x86-64
SYMBOL TABLE:
0000000000000238 l d .interp 0000000000000000 .interp
0000000000000254 l d .note.ABI-tag 0000000000000000 .note.ABI-tag
0000000000000274 l d .note.gnu.build-id 0000000000000000 .note.gnu.build-id
0000000000000298 l d .gnu.hash 0000000000000000 .gnu.hash
00000000000002b8 l d .dynsym 0000000000000000 .dynsym
0000000000000378 l d .dynstr 0000000000000000 .dynstr
0000000000000402 l d .gnu.version 0000000000000000 .gnu.version
0000000000000418 l d .gnu.version_r 0000000000000000 .gnu.version_r
0000000000000438 l d .rela.dyn 0000000000000000 .rela.dyn
0000000000000528 l d .rela.plt 0000000000000000 .rela.plt
0000000000000558 l d .init 0000000000000000 .init
0000000000000570 l d .plt 0000000000000000 .plt
00000000000005a0 l d .plt.got 0000000000000000 .plt.got
00000000000005b0 l d .text 0000000000000000 .text
0000000000000794 l d .fini 0000000000000000 .fini
00000000000007a0 l d .rodata 0000000000000000 .rodata
00000000000007d8 l d .eh_frame_hdr 0000000000000000 .eh_frame_hdr
0000000000000820 l d .eh_frame 0000000000000000 .eh_frame
0000000000200db0 l d .init_array 0000000000000000 .init_array
0000000000200db8 l d .fini_array 0000000000000000 .fini_array
0000000000200dc0 l d .dynamic 0000000000000000 .dynamic
0000000000200fb0 l d .got 0000000000000000 .got
0000000000201000 l d .data 0000000000000000 .data
0000000000201030 l d .bss 0000000000000000 .bss
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 l d .debug_aranges 0000000000000000 .debug_aranges
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
00000000000005e0 l F .text 0000000000000000 deregister_tm_clones
0000000000000620 l F .text 0000000000000000 register_tm_clones
0000000000000670 l F .text 0000000000000000 __do_global_dtors_aux
0000000000201030 l O .bss 0000000000000001 completed.7698
0000000000200db8 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry
00000000000006b0 l F .text 0000000000000000 frame_dummy
0000000000200db0 l O .init_array 0000000000000000 __frame_dummy_init_array_entry
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000201010 l O .data 0000000000000004 _ZL14global_int_var
0000000000201018 l O .data 0000000000000008 _ZL17global_string_var
0000000000000000 l df *ABS* 0000000000000000 foo.cpp
0000000000201020 l O .data 0000000000000004 _ZL14global_int_var
0000000000201028 l O .data 0000000000000008 _ZL17global_string_var
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
0000000000000944 l O .eh_frame 0000000000000000 __FRAME_END__
0000000000000000 l df *ABS* 0000000000000000
0000000000200db8 l .init_array 0000000000000000 __init_array_end
0000000000200dc0 l O .dynamic 0000000000000000 _DYNAMIC
0000000000200db0 l .init_array 0000000000000000 __init_array_start
00000000000007d8 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR
0000000000200fb0 l O .got 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000790 g F .text 0000000000000002 __libc_csu_fini
0000000000000000 w *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000201000 w .data 0000000000000000 data_start
0000000000000000 F *UND* 0000000000000000 puts@@GLIBC_2.2.5
0000000000201030 g .data 0000000000000000 _edata
0000000000000794 g F .fini 0000000000000000 _fini
0000000000000000 F *UND* 0000000000000000 printf@@GLIBC_2.2.5
0000000000000709 g F .text 0000000000000014 _Z11global_funcii
0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5
0000000000201000 g .data 0000000000000000 __data_start
0000000000000000 w *UND* 0000000000000000 __gmon_start__
0000000000201008 g O .data 0000000000000000 .hidden __dso_handle
00000000000007a0 g O .rodata 0000000000000004 _IO_stdin_used
0000000000000720 g F .text 0000000000000065 __libc_csu_init
0000000000201038 g .bss 0000000000000000 _end
00000000000005b0 g F .text 000000000000002b _start
0000000000201030 g .bss 0000000000000000 __bss_start
00000000000006ba g F .text 000000000000004f main
0000000000201030 g O .data 0000000000000000 .hidden __TMC_END__
0000000000000000 w *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w F *UND* 0000000000000000 __cxa_finalize@@GLIBC_2.2.5
0000000000000558 g F .init 0000000000000000 _init
可以看到,两种方式的输出大同小异,readelf
输出了符号的大小,但是没有输出符号所属的段,而 objdump
输出了符号所属的段,但是没有输出符号的大小。
总之通过这些命令,我们可以详细看到每个符号所表示的类型,比如它是表示函数,变量或者文件等,以及每个符号的地址,大小和它们所属的段等信息。
除此之外还有一个 nm
命令也可以查看符号表,但是它的输出就更简洁了。
zmant@PC-201807220249:~/test$ nm main
0000000000200dc0 d _DYNAMIC
0000000000200fb0 d _GLOBAL_OFFSET_TABLE_
00000000000007a0 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000000709 T _Z11global_funcii
0000000000201010 d _ZL14global_int_var
0000000000201020 d _ZL14global_int_var
0000000000201018 d _ZL17global_string_var
0000000000201028 d _ZL17global_string_var
0000000000000944 r __FRAME_END__
00000000000007d8 r __GNU_EH_FRAME_HDR
0000000000201030 D __TMC_END__
0000000000201030 B __bss_start
w __cxa_finalize@@GLIBC_2.2.5
0000000000201000 D __data_start
0000000000000670 t __do_global_dtors_aux
0000000000200db8 t __do_global_dtors_aux_fini_array_entry
0000000000201008 D __dso_handle
0000000000200db0 t __frame_dummy_init_array_entry
w __gmon_start__
0000000000200db8 t __init_array_end
0000000000200db0 t __init_array_start
0000000000000790 T __libc_csu_fini
0000000000000720 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
0000000000201030 D _edata
0000000000201038 B _end
0000000000000794 T _fini
0000000000000558 T _init
00000000000005b0 T _start
0000000000201030 b completed.7698
0000000000201000 W data_start
00000000000005e0 t deregister_tm_clones
00000000000006b0 t frame_dummy
00000000000006ba T main
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
0000000000000620 t register_tm_clones
或许对 C++ 不太熟悉的同学看到这里会有点疑惑,因为看起来并没有我们定义的 global_int_var, global_string_var, global_func
等这些符号。其实是因为 C++ 有一个符号修饰或符号改编(Name Mangling)的机制把原本我们定义的符号重新做了一个修饰导致的。
我们可以使用 c++filt
这个命令来还原修饰之前的符号。
zmant@PC-201807220249:~/test$ c++filt _Z11global_funcii
global_func(int, int)
zmant@PC-201807220249:~/test$ c++filt _ZL14global_int_var
global_int_var
zmant@PC-201807220249:~/test$ c++filt _ZL14global_int_var
global_int_var
当代码中存在命名空间,类的时候修饰后的符号可能就更加长了,但细看还是可以分辨的出原本的符号名称,若是不确定还可以像上面那样使用 c++filt
这个工具来对符号进行还原。
具体是如何对符号进行修饰的,各个编译器的方式都不尽相同,C++ 标准也没有对此做出规定,这里不再赘述。只是简单说明一下为何 C++ 需要这个符号改编(Name Mangling)的机制而 C 确不需要呢?原因就在于强大又复杂的 C++ 拥有像命名空间,函数重载等这些特性,它们使得符号管理更为复杂了。
举例最简单的一种函数重载的情况:两个相同名字的函数 func(int)
和 func(float)
, 他们名字相同但参数列表不同,那么编译器和链接器在链接过程中如何区分这两个函数才不会有符号重复定义的错误呢?最简单的当然就是把参数列表也添加到函数符号里面了,事实上大多数编译器都是这样做的。另外我们知道不同命名空间下是可以定义相同的变量而不会冲突,这也是类似的做法,这就是 C++符号改编的机制。
查看文件所依赖的库
在 被隐藏了的编译过程 这篇文章里我们了解到源代码到可执行文件的转换有一个很重要的步骤的就是链接,而链接需要的目标文件就是链接后形成的可执行文件的依赖库。比如有时我们在 Window
上运行某些软件的时候可能会提示你无法启动程序,因为计算机丢失 xxx.dll
的弹窗。这其实就是程序运行起来后发现找不到它的依赖库导致的。
同样的,我们可以通过 objdump -x
和 readelf -d
这两个命令来查看可执行文件的依赖库。
zmant@PC-201807220249:~/test$ readelf -d main
Dynamic section at offset 0xd98 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [foo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [.]
....
zmant@PC-201807220249:~/test$
zmant@PC-201807220249:~/test$ readelf -d main
Dynamic section at offset 0xd98 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [foo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [.]
....
可以看到除了我们自己写的 foo.so
这个动态库外,程序还需要一个 libc.so
这个动态库,缺少任意一个,程序都无法成功运行起来。
另外我们还可以从上面的输出中看到库的运行时路径是当前目录,当然这个路径是我们在编译的时候通过 gcc 的参数 -Wl,-rpath=.
指定的,除了这个路径外,Linux 动态库的默认搜索路径还有 /lib
和 /usr/lib
这两个路径。
比如我们把 foo.so
这个动态库移动到 ./subdir
这个目录后,再运行程序就会提示你找不到对应的共享库了。
zmant@PC-201807220249:~/test$ mv foo.so ./subdir/
zmant@PC-201807220249:~/test$ ./main
./main: error while loading shared libraries: foo.so: cannot open shared object file: No such file or directory
再移动到当前目录下程序又可以正常运行了。
zmant@PC-201807220249:~/test$ mv ./subdir/foo.so .
zmant@PC-201807220249:~/test$ ./main
hello world
global_int_var=1024; c=1036
另外,我们还可以通过 ldd
这个命令来查看程序的依赖库以及库的运行时路径。
zmant@PC-201807220249:~/test$ ldd main
linux-vdso.so.1 (0x00007fffc6483000)
foo.so => ./foo.so (0x00007f0d54e20000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d54a10000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0d55400000)
总结
其实 readelf
和 objdump
都还有很多非常有用参数可以查看更多你需要的信息,我这里也只是列举几个相对更常见或更重要的参数聊作说明以期让你对这两个工具有一个大概的认识。下面就以引用Linux 命令手册中的内容做个简单的总结。
objdump常用参数说明
—archive-headers -a 显示档案库的成员信息,类似ls -l将lib*.a的信息列出。
-b bfdname —target=bfdname 指定目标码格式。这不是必须的,objdump能自动识别许多格式,比如:
objdump -b oasys -m vax -h fu.o
显示fu.o的头部摘要信息,明确指出该文件是Vax系统下用Oasys编译器生成的目标文件。objdump -i将给出这里可以指定的目标码格式列表。
-C
—demangle
将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外,还使得C++函数名以可理解的方式显示出来。
—debugging
-g
显示调试信息。企图解析保存在文件中的调试信息并以C语言的语法显示出来。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。
-e
—debugging-tags
类似-g选项,但是生成的信息是和ctags工具相兼容的格式。
—disassemble
-d
从objfile中反汇编那些特定指令机器码的section。
-D
—disassemble-all
与 -d 类似,但反汇编所有section.
—prefix-addresses
反汇编的时候,显示每一行的完整地址。这是一种比较老的反汇编格式。
-EB
-EL
—endian={big|little}
指定目标文件的小端。这个项将影响反汇编出来的指令。在反汇编的文件没描述小端信息的时候用。例如S-records.
-f
—file-headers
显示objfile中每个文件的整体头部摘要信息。
-h
—section-headers
—headers
显示目标文件各个section的头部摘要信息。
-H
—help
简短的帮助信息。
-i
—info
显示对于 -b 或者 -m 选项可用的架构和目标格式列表。
-j name
—section=name
仅仅显示指定名称为name的section的信息
-l
—line-numbers
用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。
-m machine
—architecture=machine
指定反汇编目标文件时使用的架构,当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项很有用。可以用-i选项列出这里能够指定的架构.
—reloc
-r
显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来。
—dynamic-reloc
-R
显示文件的动态重定位入口,仅仅对于动态目标文件意义,比如某些共享库。
-s
—full-contents
显示指定section的完整内容。默认所有的非空section都会被显示。
-S
—source
尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
—show-raw-insn
反汇编的时候,显示每条汇编指令对应的机器码,如不指定—prefix-addresses,这将是缺省选项。
—no-show-raw-insn
反汇编时,不显示汇编指令的机器码,如不指定—prefix-addresses,这将是缺省选项。
—start-address=address
从指定地址开始显示数据,该选项影响-d、-r和-s选项的输出。
—stop-address=address
显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出。
-t
—syms
显示文件的符号表入口。类似于nm -s提供的信息
-T
—dynamic-syms
显示文件的动态符号表入口,仅仅对动态目标文件意义,比如某些共享库。它显示的信息类似于 nm -D|—dynamic 显示的信息。
-V
—version
版本信息
—all-headers
-x
显示所可用的头信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。
-z
—disassemble-zeroes
一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编。
@file 可以将选项集中到一个文件中,然后使用这个@file选项载入。
readelf常用参数说明
-a
—all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I.
-h
—file-header 显示elf文件开始的文件头信息.
-l
—program-headers
—segments 显示程序头(段头)信息(如果有的话)。
-S
—section-headers
—sections 显示节头信息(如果有的话)。
-g
—section-groups 显示节组信息(如果有的话)。
-t
—section-details 显示节的详细信息(-S的)。
-s
—syms
—symbols 显示符号表段中的项(如果有的话)。
-e
—headers 显示全部头信息,等价于: -h -l -S
-n
—notes 显示note段(内核注释)的信息。
-r
—relocs 显示可重定位段的信息。
-u
—unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。
-d
—dynamic 显示动态段的信息。
-V
—version-info 显示版本段的信息。
-A
—arch-specific 显示CPU构架信息。
-D
—use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。
-x
—hex-dump=
以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。 -w[liaprmfFsoR] or
—debug-dump[=line,=info,=abbrev,=pubnames,=aranges,=macro,=frames,=frames-interp,=str,=loc,=Ranges] 显示调试段中指定的内容。
-I
—histogram 显示符号的时候,显示bucket list长度的柱状图。
-v
—version 显示readelf的版本信息。
-H
—help 显示readelf所支持的命令行选项。
-W
—wide 宽行输出。
@file 可以将选项集中到一个文件中,然后使用这个@file选项载入。
输出不易~都看到这里,不打算鼓励一下作者么~~