objdump、readelf 都是 linux 系统下的一款二进制目标文件分析工具,它可以用来查看反汇编后的文件,文件中各个段的信息,调试信息以及符号表等内容。

我们就以下面的程序来详细说明 objdumpreadelf 的使用,分别有 foo.c/foo.hmain.c 这几个源代码文件。

  1. // foo.h
  2. #ifndef _FOO_HPP_
  3. #define _FOO_HPP_
  4. static int global_int_var = 1024;
  5. static char* global_string_var = "hello world!";
  6. int global_func(int a, int b);
  7. #endif
  1. // foo.cpp
  2. #include "foo.hpp"
  3. int global_func(int a, int b) {
  4. return a + b;
  5. }
  1. // main.cpp
  2. #include <cstdio>
  3. #include "foo.hpp"
  4. int main() {
  5. int c = global_func(12, global_int_var);
  6. printf("%s\n", global_string_var);
  7. printf("global_int_var=%d; c=%d\n", global_int_var, c);
  8. return 0;
  9. }

在上面源码所在目录下再写一个 makefile 脚本文件方便编译。

  1. all: main
  2. main: main.cpp foo.so
  3. gcc -Wl,-rpath=. -o $@ -g $^ -L.
  4. foo.so: foo.o
  5. gcc -o $@ $^ -shared -fPIC
  6. foo.o: foo.cpp
  7. gcc -o $@ -g -c $<
  8. clean:
  9. rm -rf main *.o *.so

执行 make 命令后就生成了可执行文件 main,执行 ./main 命令可以看到程序正常输出,下面我们就以 objdumpreadelf 这两个工具来窥探一下 main 这个可执行文件的大致结构。

首先我们使用一个二进制查看器打开 main 这个可执行文件看看里面长得怎么样,大概如下:
image.png
这个文件其实非常长,我们这里只截取了开头的一部分,可以看到最左边和最上面类似坐标系那样标志了中间每个字节的位置,最右面部分显示中间每个字节对应的文本。一般这种通用格式的第一行都会是某种标志数字,也就是如上图中的 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

从这里也可以预见,不管是什么类型的文件,可执行文件也好,视频文件也好,音乐文件也好,图片文件也好,你用二进制编辑器查看就可以发现,这一切的一切都只是一行行的二进制序列而已。

查看ELF文件头

我们使用 readelf -h main 命令来查看 main 可执行文件的文件头显示如下:

  1. zmant@PC-201807220249:~/test$ readelf -h main
  2. ELF Header:
  3. Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  4. Class: ELF64
  5. Data: 2's complement, little endian
  6. Version: 1 (current)
  7. OS/ABI: UNIX - System V
  8. ABI Version: 0
  9. Type: DYN (Shared object file)
  10. Machine: Advanced Micro Devices X86-64
  11. Version: 0x1
  12. Entry point address: 0x5b0
  13. Start of program headers: 64 (bytes into file)
  14. Start of section headers: 11096 (bytes into file)
  15. Flags: 0x0
  16. Size of this header: 64 (bytes)
  17. Size of program headers: 56 (bytes)
  18. Number of program headers: 9
  19. Size of section headers: 64 (bytes)
  20. Number of section headers: 34
  21. 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 反汇编一下就知晓了 。

  1. zmant@PC-201807220249:~/test$ objdump -d main > main.s
  2. zmant@PC-201807220249:~/test$ cat main.s
  3. main: file format elf64-x86-64
  4. ......
  5. Disassembly of section .text:
  6. 00000000000005b0 <_start>:
  7. 5b0: 31 ed xor %ebp,%ebp
  8. 5b2: 49 89 d1 mov %rdx,%r9
  9. 5b5: 5e pop %rsi
  10. 5b6: 48 89 e2 mov %rsp,%rdx
  11. 5b9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
  12. 5bd: 50 push %rax
  13. 5be: 54 push %rsp
  14. 5bf: 4c 8d 05 ca 01 00 00 lea 0x1ca(%rip),%r8 # 790 <__libc_csu_fini>
  15. 5c6: 48 8d 0d 53 01 00 00 lea 0x153(%rip),%rcx # 720 <__libc_csu_init>
  16. 5cd: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 6ba <main>
  17. 5d4: ff 15 06 0a 20 00 callq *0x200a06(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
  18. 5da: f4 hlt
  19. 5db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
  20. ......
  21. 00000000000006ba <main>:
  22. 6ba: 55 push %rbp
  23. 6bb: 48 89 e5 mov %rsp,%rbp
  24. 6be: 48 83 ec 10 sub $0x10,%rsp
  25. 6c2: 8b 05 48 09 20 00 mov 0x200948(%rip),%eax # 201010 <_ZL14global_int_var>
  26. 6c8: 89 c6 mov %eax,%esi
  27. 6ca: bf 0c 00 00 00 mov $0xc,%edi
  28. 6cf: e8 35 00 00 00 callq 709 <_Z11global_funcii>
  29. 6d4: 89 45 fc mov %eax,-0x4(%rbp)
  30. 6d7: 48 8b 05 3a 09 20 00 mov 0x20093a(%rip),%rax # 201018 <_ZL17global_string_var>
  31. 6de: 48 89 c7 mov %rax,%rdi
  32. 6e1: e8 9a fe ff ff callq 580 <puts@plt>
  33. 6e6: 8b 05 24 09 20 00 mov 0x200924(%rip),%eax # 201010 <_ZL14global_int_var>
  34. 6ec: 8b 55 fc mov -0x4(%rbp),%edx
  35. 6ef: 89 c6 mov %eax,%esi
  36. 6f1: 48 8d 3d b8 00 00 00 lea 0xb8(%rip),%rdi # 7b0 <_IO_stdin_used+0x10>
  37. 6f8: b8 00 00 00 00 mov $0x0,%eax
  38. 6fd: e8 8e fe ff ff callq 590 <printf@plt>
  39. 702: b8 00 00 00 00 mov $0x0,%eax
  40. 707: c9 leaveq
  41. 708: c3 retq
  42. 0000000000000709 <_Z11global_funcii>:
  43. 709: 55 push %rbp
  44. 70a: 48 89 e5 mov %rsp,%rbp
  45. 70d: 89 7d fc mov %edi,-0x4(%rbp)
  46. 710: 89 75 f8 mov %esi,-0x8(%rbp)
  47. 713: 8b 55 fc mov -0x4(%rbp),%edx
  48. 716: 8b 45 f8 mov -0x8(%rbp),%eax
  49. 719: 01 d0 add %edx,%eax
  50. 71b: 5d pop %rbp
  51. 71c: c3 retq
  52. 71d: 0f 1f 00 nopl (%rax)
  53. ......

事情好像有点出乎意料了,我们找到了 main() 函数,但是我们发现它的地址是 0x6ba,并不是我们认为的 0x5b0,这是怎么回事?难道是 readelf -h 打印了错误的入口地址?还是 objdump -d 错误的反汇编了 main() 函数的地址?

其实都不是,我们继续在 main.s 中搜索 5b0 这个字符串,可以发现它对应的是 _start() 函数的地址,仔细看 _start() 函数那段汇编可以发现它里面有调用 main() 函数。

  1. 00000000000005b0 <_start>:
  2. ......
  3. 5bf: 4c 8d 05 ca 01 00 00 lea 0x1ca(%rip),%r8 # 790 <__libc_csu_fini>
  4. 5c6: 48 8d 0d 53 01 00 00 lea 0x153(%rip),%rcx # 720 <__libc_csu_init>
  5. 5cd: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 6ba <main>
  6. 5d4: ff 15 06 0a 20 00 callq *0x200a06(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
  7. ......

可以看到,在调用 main() 函数之前 _start() 其实还调用了两个初始化函数,初始化完成之后才开始调用 main() 函数,所以到这里我们通过反汇编的方式了解到程序真正的入口函数是 _start() 函数,而不是我们一直以为的 main() 函数。objdump 工具当记一功啊。

窥探文件中的符号表

在手写 Makefile 的时候可能有时候会经常遇到未定义的符号重复定义的符号等各种错误。顾名思义,未定义的符号就是在目标文件的符号表中找不到对应的符号,但在目标文件的代码段中又有对该符号的引用;而重复定义的符号就是在该目标文件中出现了两个或两个以上相同的符号。这些问题其实都和目标文件中符号表中保存的符号有关,所以现在我们就来了解一下如何查看目标文件中符号表的内容。

我们预想中符号表的内容可能就只是 "global_int_var", "global_string_var", "global_func", "printf", "main" ... 这些出现在我们程序中的符号,实际又相差多少呢?我们运行 readelf -s main 来看看:

  1. zmant@PC-201807220249:~/test$ readelf -s main
  2. Symbol table '.dynsym' contains 8 entries:
  3. Num: Value Size Type Bind Vis Ndx Name
  4. 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
  5. 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
  6. 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
  7. 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
  8. 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
  9. 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
  10. 6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
  11. 7: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
  12. Symbol table '.symtab' contains 75 entries:
  13. Num: Value Size Type Bind Vis Ndx Name
  14. 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
  15. 1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
  16. 2: 0000000000000254 0 SECTION LOCAL DEFAULT 2
  17. 3: 0000000000000274 0 SECTION LOCAL DEFAULT 3
  18. 4: 0000000000000298 0 SECTION LOCAL DEFAULT 4
  19. 5: 00000000000002b8 0 SECTION LOCAL DEFAULT 5
  20. 6: 0000000000000378 0 SECTION LOCAL DEFAULT 6
  21. 7: 0000000000000402 0 SECTION LOCAL DEFAULT 7
  22. 8: 0000000000000418 0 SECTION LOCAL DEFAULT 8
  23. 9: 0000000000000438 0 SECTION LOCAL DEFAULT 9
  24. 10: 0000000000000528 0 SECTION LOCAL DEFAULT 10
  25. 11: 0000000000000558 0 SECTION LOCAL DEFAULT 11
  26. 12: 0000000000000570 0 SECTION LOCAL DEFAULT 12
  27. 13: 00000000000005a0 0 SECTION LOCAL DEFAULT 13
  28. 14: 00000000000005b0 0 SECTION LOCAL DEFAULT 14
  29. 15: 0000000000000794 0 SECTION LOCAL DEFAULT 15
  30. 16: 00000000000007a0 0 SECTION LOCAL DEFAULT 16
  31. 17: 00000000000007d8 0 SECTION LOCAL DEFAULT 17
  32. 18: 0000000000000820 0 SECTION LOCAL DEFAULT 18
  33. 19: 0000000000200db0 0 SECTION LOCAL DEFAULT 19
  34. 20: 0000000000200db8 0 SECTION LOCAL DEFAULT 20
  35. 21: 0000000000200dc0 0 SECTION LOCAL DEFAULT 21
  36. 22: 0000000000200fb0 0 SECTION LOCAL DEFAULT 22
  37. 23: 0000000000201000 0 SECTION LOCAL DEFAULT 23
  38. 24: 0000000000201030 0 SECTION LOCAL DEFAULT 24
  39. 25: 0000000000000000 0 SECTION LOCAL DEFAULT 25
  40. 26: 0000000000000000 0 SECTION LOCAL DEFAULT 26
  41. 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
  42. 28: 0000000000000000 0 SECTION LOCAL DEFAULT 28
  43. 29: 0000000000000000 0 SECTION LOCAL DEFAULT 29
  44. 30: 0000000000000000 0 SECTION LOCAL DEFAULT 30
  45. 31: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
  46. 32: 00000000000005e0 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
  47. 33: 0000000000000620 0 FUNC LOCAL DEFAULT 14 register_tm_clones
  48. 34: 0000000000000670 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
  49. 35: 0000000000201030 1 OBJECT LOCAL DEFAULT 24 completed.7698
  50. 36: 0000000000200db8 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
  51. 37: 00000000000006b0 0 FUNC LOCAL DEFAULT 14 frame_dummy
  52. 38: 0000000000200db0 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
  53. 39: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
  54. 40: 0000000000201010 4 OBJECT LOCAL DEFAULT 23 _ZL14global_int_var
  55. 41: 0000000000201018 8 OBJECT LOCAL DEFAULT 23 _ZL17global_string_var
  56. 42: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.cpp
  57. 43: 0000000000201020 4 OBJECT LOCAL DEFAULT 23 _ZL14global_int_var
  58. 44: 0000000000201028 8 OBJECT LOCAL DEFAULT 23 _ZL17global_string_var
  59. 45: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
  60. 46: 0000000000000944 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
  61. 47: 0000000000000000 0 FILE LOCAL DEFAULT ABS
  62. 48: 0000000000200db8 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
  63. 49: 0000000000200dc0 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
  64. 50: 0000000000200db0 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
  65. 51: 00000000000007d8 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
  66. 52: 0000000000200fb0 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
  67. 53: 0000000000000790 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
  68. 54: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
  69. 55: 0000000000201000 0 NOTYPE WEAK DEFAULT 23 data_start
  70. 56: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
  71. 57: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 23 _edata
  72. 58: 0000000000000794 0 FUNC GLOBAL DEFAULT 15 _fini
  73. 59: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
  74. 60: 0000000000000709 20 FUNC GLOBAL DEFAULT 14 _Z11global_funcii
  75. 61: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
  76. 62: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
  77. 63: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
  78. 64: 0000000000201008 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
  79. 65: 00000000000007a0 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
  80. 66: 0000000000000720 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
  81. 67: 0000000000201038 0 NOTYPE GLOBAL DEFAULT 24 _end
  82. 68: 00000000000005b0 43 FUNC GLOBAL DEFAULT 14 _start
  83. 69: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
  84. 70: 00000000000006ba 79 FUNC GLOBAL DEFAULT 14 main
  85. 71: 0000000000201030 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
  86. 72: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
  87. 73: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
  88. 74: 0000000000000558 0 FUNC GLOBAL DEFAULT 11 _init

毫无悬念,除了我们预料的符号外,还有很多我们不知道的符号也在符号表里面,这也很容易理解,因为通过上面反汇编的过程我们知道了程序在进入 main() 函数之前先调用了入口函数 _start() 并做了一些初始化的工作,这些动作都是编译器帮我们做好了,这些肯定是会产生一些符号的。再者,我们最终的可执行文件 main 也需要链接系统的一些库来完成输出的操作,这些库里面肯定也是存在符号的。所以有这么多我们不认识的符号也就不奇怪了。

除了 readelf -s 外,我们通常还会对照使用 objdump -t 命令来查看符号表。

  1. zmant@PC-201807220249:~/test$ objdump -t main
  2. main: file format elf64-x86-64
  3. SYMBOL TABLE:
  4. 0000000000000238 l d .interp 0000000000000000 .interp
  5. 0000000000000254 l d .note.ABI-tag 0000000000000000 .note.ABI-tag
  6. 0000000000000274 l d .note.gnu.build-id 0000000000000000 .note.gnu.build-id
  7. 0000000000000298 l d .gnu.hash 0000000000000000 .gnu.hash
  8. 00000000000002b8 l d .dynsym 0000000000000000 .dynsym
  9. 0000000000000378 l d .dynstr 0000000000000000 .dynstr
  10. 0000000000000402 l d .gnu.version 0000000000000000 .gnu.version
  11. 0000000000000418 l d .gnu.version_r 0000000000000000 .gnu.version_r
  12. 0000000000000438 l d .rela.dyn 0000000000000000 .rela.dyn
  13. 0000000000000528 l d .rela.plt 0000000000000000 .rela.plt
  14. 0000000000000558 l d .init 0000000000000000 .init
  15. 0000000000000570 l d .plt 0000000000000000 .plt
  16. 00000000000005a0 l d .plt.got 0000000000000000 .plt.got
  17. 00000000000005b0 l d .text 0000000000000000 .text
  18. 0000000000000794 l d .fini 0000000000000000 .fini
  19. 00000000000007a0 l d .rodata 0000000000000000 .rodata
  20. 00000000000007d8 l d .eh_frame_hdr 0000000000000000 .eh_frame_hdr
  21. 0000000000000820 l d .eh_frame 0000000000000000 .eh_frame
  22. 0000000000200db0 l d .init_array 0000000000000000 .init_array
  23. 0000000000200db8 l d .fini_array 0000000000000000 .fini_array
  24. 0000000000200dc0 l d .dynamic 0000000000000000 .dynamic
  25. 0000000000200fb0 l d .got 0000000000000000 .got
  26. 0000000000201000 l d .data 0000000000000000 .data
  27. 0000000000201030 l d .bss 0000000000000000 .bss
  28. 0000000000000000 l d .comment 0000000000000000 .comment
  29. 0000000000000000 l d .debug_aranges 0000000000000000 .debug_aranges
  30. 0000000000000000 l d .debug_info 0000000000000000 .debug_info
  31. 0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
  32. 0000000000000000 l d .debug_line 0000000000000000 .debug_line
  33. 0000000000000000 l d .debug_str 0000000000000000 .debug_str
  34. 0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
  35. 00000000000005e0 l F .text 0000000000000000 deregister_tm_clones
  36. 0000000000000620 l F .text 0000000000000000 register_tm_clones
  37. 0000000000000670 l F .text 0000000000000000 __do_global_dtors_aux
  38. 0000000000201030 l O .bss 0000000000000001 completed.7698
  39. 0000000000200db8 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry
  40. 00000000000006b0 l F .text 0000000000000000 frame_dummy
  41. 0000000000200db0 l O .init_array 0000000000000000 __frame_dummy_init_array_entry
  42. 0000000000000000 l df *ABS* 0000000000000000 main.cpp
  43. 0000000000201010 l O .data 0000000000000004 _ZL14global_int_var
  44. 0000000000201018 l O .data 0000000000000008 _ZL17global_string_var
  45. 0000000000000000 l df *ABS* 0000000000000000 foo.cpp
  46. 0000000000201020 l O .data 0000000000000004 _ZL14global_int_var
  47. 0000000000201028 l O .data 0000000000000008 _ZL17global_string_var
  48. 0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
  49. 0000000000000944 l O .eh_frame 0000000000000000 __FRAME_END__
  50. 0000000000000000 l df *ABS* 0000000000000000
  51. 0000000000200db8 l .init_array 0000000000000000 __init_array_end
  52. 0000000000200dc0 l O .dynamic 0000000000000000 _DYNAMIC
  53. 0000000000200db0 l .init_array 0000000000000000 __init_array_start
  54. 00000000000007d8 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR
  55. 0000000000200fb0 l O .got 0000000000000000 _GLOBAL_OFFSET_TABLE_
  56. 0000000000000790 g F .text 0000000000000002 __libc_csu_fini
  57. 0000000000000000 w *UND* 0000000000000000 _ITM_deregisterTMCloneTable
  58. 0000000000201000 w .data 0000000000000000 data_start
  59. 0000000000000000 F *UND* 0000000000000000 puts@@GLIBC_2.2.5
  60. 0000000000201030 g .data 0000000000000000 _edata
  61. 0000000000000794 g F .fini 0000000000000000 _fini
  62. 0000000000000000 F *UND* 0000000000000000 printf@@GLIBC_2.2.5
  63. 0000000000000709 g F .text 0000000000000014 _Z11global_funcii
  64. 0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5
  65. 0000000000201000 g .data 0000000000000000 __data_start
  66. 0000000000000000 w *UND* 0000000000000000 __gmon_start__
  67. 0000000000201008 g O .data 0000000000000000 .hidden __dso_handle
  68. 00000000000007a0 g O .rodata 0000000000000004 _IO_stdin_used
  69. 0000000000000720 g F .text 0000000000000065 __libc_csu_init
  70. 0000000000201038 g .bss 0000000000000000 _end
  71. 00000000000005b0 g F .text 000000000000002b _start
  72. 0000000000201030 g .bss 0000000000000000 __bss_start
  73. 00000000000006ba g F .text 000000000000004f main
  74. 0000000000201030 g O .data 0000000000000000 .hidden __TMC_END__
  75. 0000000000000000 w *UND* 0000000000000000 _ITM_registerTMCloneTable
  76. 0000000000000000 w F *UND* 0000000000000000 __cxa_finalize@@GLIBC_2.2.5
  77. 0000000000000558 g F .init 0000000000000000 _init

可以看到,两种方式的输出大同小异,readelf 输出了符号的大小,但是没有输出符号所属的段,而 objdump 输出了符号所属的段,但是没有输出符号的大小。

总之通过这些命令,我们可以详细看到每个符号所表示的类型,比如它是表示函数,变量或者文件等,以及每个符号的地址,大小和它们所属的段等信息。

除此之外还有一个 nm 命令也可以查看符号表,但是它的输出就更简洁了。

  1. zmant@PC-201807220249:~/test$ nm main
  2. 0000000000200dc0 d _DYNAMIC
  3. 0000000000200fb0 d _GLOBAL_OFFSET_TABLE_
  4. 00000000000007a0 R _IO_stdin_used
  5. w _ITM_deregisterTMCloneTable
  6. w _ITM_registerTMCloneTable
  7. 0000000000000709 T _Z11global_funcii
  8. 0000000000201010 d _ZL14global_int_var
  9. 0000000000201020 d _ZL14global_int_var
  10. 0000000000201018 d _ZL17global_string_var
  11. 0000000000201028 d _ZL17global_string_var
  12. 0000000000000944 r __FRAME_END__
  13. 00000000000007d8 r __GNU_EH_FRAME_HDR
  14. 0000000000201030 D __TMC_END__
  15. 0000000000201030 B __bss_start
  16. w __cxa_finalize@@GLIBC_2.2.5
  17. 0000000000201000 D __data_start
  18. 0000000000000670 t __do_global_dtors_aux
  19. 0000000000200db8 t __do_global_dtors_aux_fini_array_entry
  20. 0000000000201008 D __dso_handle
  21. 0000000000200db0 t __frame_dummy_init_array_entry
  22. w __gmon_start__
  23. 0000000000200db8 t __init_array_end
  24. 0000000000200db0 t __init_array_start
  25. 0000000000000790 T __libc_csu_fini
  26. 0000000000000720 T __libc_csu_init
  27. U __libc_start_main@@GLIBC_2.2.5
  28. 0000000000201030 D _edata
  29. 0000000000201038 B _end
  30. 0000000000000794 T _fini
  31. 0000000000000558 T _init
  32. 00000000000005b0 T _start
  33. 0000000000201030 b completed.7698
  34. 0000000000201000 W data_start
  35. 00000000000005e0 t deregister_tm_clones
  36. 00000000000006b0 t frame_dummy
  37. 00000000000006ba T main
  38. U printf@@GLIBC_2.2.5
  39. U puts@@GLIBC_2.2.5
  40. 0000000000000620 t register_tm_clones

或许对 C++ 不太熟悉的同学看到这里会有点疑惑,因为看起来并没有我们定义的 global_int_var, global_string_var, global_func 等这些符号。其实是因为 C++ 有一个符号修饰符号改编(Name Mangling)的机制把原本我们定义的符号重新做了一个修饰导致的。

我们可以使用 c++filt 这个命令来还原修饰之前的符号。

  1. zmant@PC-201807220249:~/test$ c++filt _Z11global_funcii
  2. global_func(int, int)
  3. zmant@PC-201807220249:~/test$ c++filt _ZL14global_int_var
  4. global_int_var
  5. zmant@PC-201807220249:~/test$ c++filt _ZL14global_int_var
  6. global_int_var

当代码中存在命名空间的时候修饰后的符号可能就更加长了,但细看还是可以分辨的出原本的符号名称,若是不确定还可以像上面那样使用 c++filt 这个工具来对符号进行还原。

具体是如何对符号进行修饰的,各个编译器的方式都不尽相同,C++ 标准也没有对此做出规定,这里不再赘述。只是简单说明一下为何 C++ 需要这个符号改编(Name Mangling)的机制而 C 确不需要呢?原因就在于强大又复杂的 C++ 拥有像命名空间,函数重载等这些特性,它们使得符号管理更为复杂了。

举例最简单的一种函数重载的情况:两个相同名字的函数 func(int)func(float), 他们名字相同但参数列表不同,那么编译器和链接器在链接过程中如何区分这两个函数才不会有符号重复定义的错误呢?最简单的当然就是把参数列表也添加到函数符号里面了,事实上大多数编译器都是这样做的。另外我们知道不同命名空间下是可以定义相同的变量而不会冲突,这也是类似的做法,这就是 C++符号改编的机制。

查看文件所依赖的库

被隐藏了的编译过程 这篇文章里我们了解到源代码到可执行文件的转换有一个很重要的步骤的就是链接,而链接需要的目标文件就是链接后形成的可执行文件的依赖库。比如有时我们在 Window 上运行某些软件的时候可能会提示你无法启动程序,因为计算机丢失 xxx.dll 的弹窗。这其实就是程序运行起来后发现找不到它的依赖库导致的。
image.png
同样的,我们可以通过 objdump -xreadelf -d 这两个命令来查看可执行文件的依赖库。

  1. zmant@PC-201807220249:~/test$ readelf -d main
  2. Dynamic section at offset 0xd98 contains 29 entries:
  3. Tag Type Name/Value
  4. 0x0000000000000001 (NEEDED) Shared library: [foo.so]
  5. 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
  6. 0x000000000000001d (RUNPATH) Library runpath: [.]
  7. ....
  8. zmant@PC-201807220249:~/test$
  9. zmant@PC-201807220249:~/test$ readelf -d main
  10. Dynamic section at offset 0xd98 contains 29 entries:
  11. Tag Type Name/Value
  12. 0x0000000000000001 (NEEDED) Shared library: [foo.so]
  13. 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
  14. 0x000000000000001d (RUNPATH) Library runpath: [.]
  15. ....

可以看到除了我们自己写的 foo.so 这个动态库外,程序还需要一个 libc.so 这个动态库,缺少任意一个,程序都无法成功运行起来。

另外我们还可以从上面的输出中看到库的运行时路径是当前目录,当然这个路径是我们在编译的时候通过 gcc 的参数 -Wl,-rpath=. 指定的,除了这个路径外,Linux 动态库的默认搜索路径还有 /lib/usr/lib 这两个路径。

比如我们把 foo.so 这个动态库移动到 ./subdir 这个目录后,再运行程序就会提示你找不到对应的共享库了。

  1. zmant@PC-201807220249:~/test$ mv foo.so ./subdir/
  2. zmant@PC-201807220249:~/test$ ./main
  3. ./main: error while loading shared libraries: foo.so: cannot open shared object file: No such file or directory

再移动到当前目录下程序又可以正常运行了。

  1. zmant@PC-201807220249:~/test$ mv ./subdir/foo.so .
  2. zmant@PC-201807220249:~/test$ ./main
  3. hello world
  4. global_int_var=1024; c=1036

另外,我们还可以通过 ldd 这个命令来查看程序的依赖库以及库的运行时路径。

  1. zmant@PC-201807220249:~/test$ ldd main
  2. linux-vdso.so.1 (0x00007fffc6483000)
  3. foo.so => ./foo.so (0x00007f0d54e20000)
  4. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d54a10000)
  5. /lib64/ld-linux-x86-64.so.2 (0x00007f0d55400000)

总结

其实 readelfobjdump 都还有很多非常有用参数可以查看更多你需要的信息,我这里也只是列举几个相对更常见或更重要的参数聊作说明以期让你对这两个工具有一个大概的认识。下面就以引用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选项载入。

输出不易~都看到这里,不打算鼓励一下作者么~~
wechat_pay.jpg