好的,咱们今天就来聊聊C++二进制文件的内部结构以及如何使用objdump
和readelf
这两个神器来扒它的皮! 想象一下,你辛辛苦苦写了一堆C++代码,编译链接之后变成了一个黑盒子——可执行文件。你是不是很好奇,这玩意儿里面到底装了些什么东西? 别着急,objdump
和readelf
就是你的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 (存放调试信息)等。 |
这些段不是必须的,但可以提供一些额外的信息。 |
这只是一个简化的模型,实际的二进制文件可能更加复杂。 但是,理解这些基本的段对于我们使用objdump
和readelf
进行分析至关重要。
第二部分: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++程序,然后用objdump
和readelf
来分析它。
// 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
,接下来就可以用objdump
和readelf
来分析它了。
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
查看符号表,找到a
、str
和b
的地址
objdump -t my_program | grep "a|str|b"
这个命令会显示符号表中包含a
、str
或b
的行。
通过查看符号表,我们可以找到a
、str
和b
的地址。 注意,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
。