C++ `objdump` / `readelf`:二进制文件结构与符号分析

好的,咱们今天就来聊聊C++二进制文件的内部结构以及如何使用objdumpreadelf这两个神器来扒它的皮! 想象一下,你辛辛苦苦写了一堆C++代码,编译链接之后变成了一个黑盒子——可执行文件。你是不是很好奇,这玩意儿里面到底装了些什么东西? 别着急,objdumpreadelf就是你的X光机,能帮你透视这个黑盒子,看到里面的骨骼和内脏。

第一部分:C++二进制文件的解剖

首先,我们要了解一下C++程序编译链接后生成的二进制文件(比如ELF格式)的大致结构。 可以把它想象成一栋大楼,里面有很多层,每一层都有不同的功能。

区域 作用 备注
文件头 描述文件的类型、架构、入口点等关键信息。 比如,告诉操作系统这是个可执行文件,是32位还是64位的,从哪里开始执行。
.text 存放程序的机器指令代码。 这就是你的C++代码编译后的机器指令,CPU就是靠执行这些指令来完成任务的。
.data 存放已初始化的全局变量和静态变量。 比如,你在程序里定义了一个int a = 10;,这个a和它的初始值10就放在这里。
.bss 存放未初始化的全局变量和静态变量。 你定义了一个int b;,没有给它初始值,那么b就放在这里。操作系统会在程序启动时将这块区域清零。
.rodata 存放只读数据,比如字符串常量。 const char* str = "Hello";,这个字符串"Hello"就放在这里。
.symtab 符号表,存放程序中定义的函数名、变量名等符号的信息。 相当于程序的一个目录,记录了每个函数和变量的地址,方便链接器和调试器使用。
.strtab 字符串表,存放符号表中符号名称的字符串。 因为符号表里只存符号的索引,实际的符号名称就放在这里。
.rel.text 重定位信息,用于在链接时修改代码中的地址。 如果你的代码调用了其他库的函数,就需要重定位信息来告诉链接器,这个函数的实际地址在哪里。
.dynamic 动态链接信息,用于在运行时加载动态链接库。 如果你的程序依赖于动态链接库(.so文件),就需要动态链接信息来告诉操作系统,需要加载哪些库。
.plt & .got 用于动态链接库的延迟绑定。 简单来说,就是为了提高程序的启动速度,只有在真正调用动态链接库的函数时,才去解析它的地址。
其他 还有一些其他的段,比如.comment(存放编译器版本信息)、.debug(存放调试信息)等。 这些段不是必须的,但可以提供一些额外的信息。

这只是一个简化的模型,实际的二进制文件可能更加复杂。 但是,理解这些基本的段对于我们使用objdumpreadelf进行分析至关重要。

第二部分:readelf—— 读懂ELF文件头

readelf是一个专门用来读取ELF格式文件的工具,它可以显示ELF文件的各种头部信息,包括文件头、程序头、段头等等。

1. 查看文件头

最常用的命令是readelf -h <filename>,它会显示ELF文件的头部信息。

readelf -h my_program

输出结果类似如下:

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)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          8832 (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:         30
  Section header string table index: 29

这里面有很多信息,咱们挑几个重要的说一下:

  • Magic: 这个是ELF文件的魔数,用来标识文件类型。 所有的ELF文件都以7f 45 4c 46开头,也就是0x7f 'E' 'L' 'F'
  • Class: 表示文件是32位还是64位的。 ELF64表示64位,ELF32表示32位。
  • Data: 表示字节序,2's complement, little endian表示小端模式。
  • Type: 表示文件类型,EXEC表示可执行文件,DYN表示动态链接库,REL表示可重定位文件(.o文件)。
  • Machine: 表示目标架构,Advanced Micro Devices X86-64表示x86-64架构。
  • Entry point address: 程序的入口点地址,也就是程序开始执行的第一条指令的地址。
  • Start of section headers: 段头表的起始位置。
  • Number of section headers: 段头表的条目数。

2. 查看程序头

程序头描述了如何将二进制文件加载到内存中。 使用readelf -l <filename>可以查看程序头信息。

readelf -l my_program

输出结果类似如下:

Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr           FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000690 0x0000000000000690  R      1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000 0x00000000000011e4 0x00000000000011e4  R E    1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000 0x0000000000000238 0x0000000000000238  R      1000
  LOAD           0x0000000000002de0 0x0000000000403de0 0x0000000000403de0 0x0000000000000224 0x0000000000000224  R      1000
  LOAD           0x0000000000004000 0x0000000000404000 0x0000000000404000 0x0000000000000220 0x0000000000000220  R      1000
  LOAD           0x0000000000005000 0x0000000000405000 0x0000000000405000 0x0000000000000220 0x0000000000000220  R      1000
  LOAD           0x0000000000006000 0x0000000000406000 0x0000000000406000 0x0000000000000770 0x0000000000000770  R      1000
  LOAD           0x0000000000007000 0x0000000000407000 0x0000000000407000 0x0000000000000230 0x0000000000000230  R      1000
  LOAD           0x0000000000007e30 0x0000000000407e30 0x0000000000407e30 0x00000000000003c0 0x00000000000003c0  R      1000

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.build-id .note.ABI-tag
   01     .text
   02     .rodata
   03     .eh_frame_hdr .eh_frame
   04     .init_array
   05     .fini_array
   06     .data.rel.ro
   07     .dynamic
   08     .data .bss
  • Type: LOAD表示需要加载到内存中的段。
  • Offset: 该段在文件中的偏移量。
  • VirtAddr: 该段在内存中的虚拟地址。
  • FileSiz: 该段在文件中的大小。
  • MemSiz: 该段在内存中的大小。
  • Flags: 该段的访问权限,R表示可读,W表示可写,E表示可执行。

3. 查看段头

段头描述了每个段的属性,比如名称、大小、地址等。 使用readelf -S <filename>可以查看段头信息。

readelf -S my_program

输出结果类似如下:

There are 30 section headers, starting at offset 0x2280:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.build-i NOTE             0000000000400238  00000238
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .note.ABI-tag      NOTE             000000000040025c  0000025c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .text             PROGBITS         0000000000401000  00001000
       00000000000011e4  0000000000000000  AX       0     0     16
  [ 4] .rodata           PROGBITS         0000000000402000  00002000
       0000000000000238  0000000000000000   A       0     0     8
  [ 5] .eh_frame_hdr     PROGBITS         0000000000403de0  00003de0
       0000000000000034  0000000000000000   A       0     0     4
  [ 6] .eh_frame         PROGBITS         0000000000403e14  00003e14
       00000000000001cc  0000000000000000   A       0     0     8
  [ 7] .init_array       INIT_ARRAY       0000000000404000  00004000
       0000000000000008  0000000000000008  WA       0     0     8
  [ 8] .fini_array       FINI_ARRAY       0000000000405000  00005000
       0000000000000008  0000000000000008  WA       0     0     8
  [ 9] .data.rel.ro      PROGBITS         0000000000406000  00006000
       0000000000000770  0000000000000000   A       0     0     32
  [10] .dynamic           DYNAMIC          0000000000407000  00007000
       0000000000000230  0000000000000010  WA       6    10     8
  [11] .got              PROGBITS         0000000000407230  00007230
       00000000000003c0  0000000000000008  WA       0     0     8
  [12] .data             PROGBITS         0000000000408000  00008000
       0000000000000008  0000000000000000  WA       0     0     8
  [13] .bss              NOBITS           0000000000408008  00008008
       0000000000000008  0000000000000000  WA       0     0     8
  [14] .comment          PROGBITS         0000000000000000  00008008
       0000000000000029  0000000000000001  MS       0     0     1
  [15] .symtab           SYMTAB           0000000000000000  00008034
       0000000000000b70  0000000000000018   S      16    32     8
  [16] .strtab           STRTAB           0000000000000000  00008ba4
       0000000000000445  0000000000000000   S       0     0     1
  [17] .shstrtab         STRTAB           0000000000000000  00008fe9
       000000000000010f  0000000000000000   S       0     0     1
  [18] .rela.dyn         RELA             0000000000400280  00000280
       00000000000001b0  0000000000000018   A      15     0     8
  [19] .rela.plt         RELA             0000000000400430  00000430
       0000000000000210  0000000000000018   A      15     0     8
  [20] .init             PROGBITS         0000000000400500  00000500
       000000000000001a  0000000000000000  AX       0     0     4
  [21] .plt              PROGBITS         0000000000400520  00000520
       0000000000000180  0000000000000010  AX       0     0     16
  [22] .plt.got          PROGBITS         00000000004006a0  000006a0
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .jcr              PROGBITS         00000000004006a8  000006a8
       0000000000000008  0000000000000000  WA       0     0     8
  [24] .tm_clone_table   PROGBITS         00000000004006b0  000006b0
       0000000000000000  0000000000000000  WA       0     0     8
  [25] .data.rel.ro.local PROGBITS         00000000004006b0  000006b0
       0000000000000008  0000000000000000  WA       0     0     8
  [26] .debug_line       PROGBITS         0000000000000000  000006b8
       0000000000000041  0000000000000001           0     0     1
  [27] .debug_str        PROGBITS         0000000000000000  000006f9
       0000000000000001  0000000000000001  MS       0     0     1
  [28] .debug_loc        PROGBITS         0000000000000000  000006fa
       0000000000000000  0000000000000001           0     0     1
  [29] .debug_abbrev     PROGBITS         0000000000000000  000006fa
       0000000000000000  0000000000000001           0     0     1
Key to Flags:
  W (write), A (alloc), E (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), p (processor specific)
  • [Nr] Name: 段的名称。
  • Type: 段的类型,比如PROGBITS表示程序数据,SYMTAB表示符号表,STRTAB表示字符串表。
  • Address: 段的起始地址。
  • Offset: 段在文件中的偏移量。
  • Size: 段的大小。
  • Flags: 段的标志,比如A表示该段会被加载到内存中,W表示可写,E表示可执行。

第三部分:objdump—— 反汇编与符号分析

objdump是一个更加强大的工具,它可以用来反汇编二进制文件,查看符号表,以及显示其他一些有用的信息。

1. 反汇编代码

使用objdump -d <filename>可以反汇编.text段的代码。

objdump -d my_program

输出结果会显示汇编代码,以及对应的机器码。 这对于理解程序的执行流程非常有帮助。

0000000000401000 <_start>:
  401000:       48 83 ec 08             sub    $0x8,%rsp
  401004:       48 8d 3d 95 0f 00 00    lea    0xf95(%rip),%rdi        # 401fa0 <__libc_csu_fini>
  40100b:       e8 80 00 00 00          call   401090 <__libc_csu_init>
  401010:       e8 61 00 00 00          call   401076 <main>
  401015:       b8 3c 00 00 00          mov    $0x3c,%eax
  40101a:       bf 00 00 00 00          mov    $0x0,%edi
  40101f:       0f 05                   syscall
  401021:       c3                      retq
  • 左边是代码的地址,中间是机器码,右边是汇编代码。
  • <_start>表示_start函数的入口点。
  • call 401076 <main>表示调用main函数。

2. 查看符号表

使用objdump -t <filename>可以查看符号表。

objdump -t my_program

输出结果会显示程序中定义的各种符号,包括函数名、变量名等。

0000000000404000 l     O .init_array   0000000000000000              __init_array_start
0000000000405000 l     O .fini_array   0000000000000000              __fini_array_start
0000000000401190 l     F .text          000000000000004f              _Z41__static_initialization_and_destruction_0ii
00000000004011df l     F .text          0000000000000034              _GLOBAL__sub_I_a
0000000000407000 l     O .dynamic       0000000000000000              _DYNAMIC
0000000000400000 l     F .text          0000000000000238              _Z4notev
0000000000400000 l    df *ABS*          0000000000000000              crtstuff.c
0000000000401000 g     F .text          0000000000000021              _start
0000000000401076 g     F .text          000000000000011a              main
0000000000402000 g     O .rodata        0000000000000006              _ZL1s
0000000000401090 g     F .text          00000000000000c3              __libc_csu_init
0000000000401fa0 g     F .text          0000000000000010              __libc_csu_fini
  • 第一列是符号的地址。
  • 第二列是符号的标志,l表示本地符号,g表示全局符号,F表示函数,O表示对象。
  • 第三列是符号所在的段。
  • 第四列是符号的大小。
  • 第五列是符号的名称。

3. 查看动态符号表

如果你的程序依赖于动态链接库,可以使用objdump -T <filename>来查看动态符号表。

objdump -T my_program

输出结果会显示程序依赖的动态链接库中的符号。

4. 其他常用选项

  • -x:显示所有可用的头部信息,相当于readelf -h -l -S的组合。
  • -s:显示指定段的内容,比如objdump -s -j .rodata my_program可以显示.rodata段的内容。
  • -M intel:使用Intel汇编语法,默认是AT&T语法。
  • --source:如果你的二进制文件包含调试信息,可以使用这个选项来显示源代码和汇编代码的对应关系。

第四部分:实战演练

光说不练假把式,咱们来写一个简单的C++程序,然后用objdumpreadelf来分析它。

// my_program.cpp
#include <iostream>

int a = 10;
const char* str = "Hello";

int main() {
    static int b = 20;
    std::cout << str << " " << a << " " << b << std::endl;
    return 0;
}

编译:

g++ my_program.cpp -o my_program

现在我们有了一个可执行文件my_program,接下来就可以用objdumpreadelf来分析它了。

1. 使用readelf查看文件头、程序头和段头

readelf -h my_program
readelf -l my_program
readelf -S my_program

通过查看这些信息,我们可以了解文件的基本属性,比如是64位的,入口点地址,以及各个段的名称、大小、地址等。

2. 使用objdump反汇编main函数

objdump -d -M intel my_program | grep "main>:" -A 20

这个命令会反汇编整个程序,然后用grep过滤出main函数的部分,并显示后面的20行。 -M intel使用Intel汇编语法,看起来更舒服。

通过查看汇编代码,我们可以了解main函数的执行流程,以及变量的访问方式。

3. 使用objdump查看符号表,找到astrb的地址

objdump -t my_program | grep "a|str|b"

这个命令会显示符号表中包含astrb的行。

通过查看符号表,我们可以找到astrb的地址。 注意,b是静态局部变量,它的符号名称可能会比较奇怪。

4. 使用objdump查看.rodata段的内容,找到"Hello"字符串

objdump -s -j .rodata my_program

这个命令会显示.rodata段的内容,我们可以在里面找到"Hello"字符串。

第五部分:C++ 特性与二进制文件

C++的一些特性,比如类、虚函数、模板等,会对二进制文件的结构产生影响。

1. 类与对象

C++的类会将数据成员和成员函数组织在一起。 在二进制文件中,数据成员会被放在.data.bss段,成员函数会被放在.text段。

2. 虚函数

如果一个类包含虚函数,那么它会包含一个虚函数表(vtable)。 vtable是一个函数指针数组,存放了虚函数的地址。 每个包含虚函数的对象都会包含一个指向vtable的指针(vptr)。

在二进制文件中,vtable会被放在.rodata段,vptr会被放在对象的内存布局中。

3. 模板

C++的模板会在编译时生成代码。 如果你的程序使用了模板,那么二进制文件中会包含模板实例化的代码。

4. 名字修饰(Name Mangling)

C++支持函数重载,也就是可以使用相同的函数名,但是参数列表不同。 为了区分这些函数,编译器会对函数名进行名字修饰(name mangling)。

例如,int add(int a, int b)可能会被修饰成_Z3addii

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注