C++ `objdump` / `readelf`:深入分析可执行文件与共享库结构

哈喽,各位好!今天咱们来聊聊C++世界里的“显微镜”和“解剖刀”——objdumpreadelf。这两个工具就像是侦探,能帮你深入了解你的程序,看看它到底长什么样,是怎么运作的。

一、 为啥要用objdumpreadelf?

想象一下,你辛辛苦苦写了一个C++程序,编译链接后变成了一个可执行文件或者共享库(比如.so文件)。但是,这玩意儿对你来说就像一个黑盒子,你只知道输入和输出,中间发生了什么,一概不知。

这时候,objdumpreadelf就派上用场了。它们可以帮你:

  • 调试问题: 当程序崩溃或者行为异常时,你可以用它们来查看汇编代码,看看哪一步出了问题。
  • 性能优化: 了解程序的执行流程,找到性能瓶颈。
  • 安全分析: 分析程序是否存在漏洞,比如缓冲区溢出。
  • 逆向工程: (当然,要合法合规哈!) 了解别人的程序是如何实现的。
  • 理解编译器和链接器的工作原理: 通过观察编译后的代码和链接后的结构,你可以更深入地理解编译器和链接器是如何工作的。

简单来说,它们能让你从“知其然”到“知其所以然”。

二、 objdump:反汇编利器

objdump最常用的功能就是反汇编,它可以将机器码翻译成汇编代码,让你看到程序是如何一步一步执行的。

1. 基本用法:

objdump -d <可执行文件或共享库>

-d选项告诉objdump进行反汇编。

例子:

假设我们有一个简单的C++程序hello.cpp

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    int sum = x + y;
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

编译:

g++ hello.cpp -o hello

反汇编:

objdump -d hello

你会看到一大堆输出,其中一部分类似下面这样:

0000000000001149 <main>:
    1149:       55                      push   %rbp
    114a:       48 89 e5                mov    %rsp,%rbp
    114d:       48 83 ec 10             sub    $0x10,%rsp
    1151:       c7 45 fc 0a 00 00 00    movl   $0xa,-0x4(%rbp)  # x = 10
    1158:       c7 45 f8 14 00 00 00    movl   $0x14,-0x8(%rbp) # y = 20
    115f:       8b 45 fc                mov    -0x4(%rbp),%eax
    1162:       03 45 f8                add    -0x8(%rbp),%eax
    1165:       89 45 f4                mov    %eax,-0xc(%rbp)  # sum = x + y
    1168:       be 00 00 00 00          mov    $0x0,%esi
    116d:       bf 04 00 00 00          mov    $0x4,%edi
    1172:       e8 00 00 00 00          call   1177 <_ZNSolsEi@plt>
    ...

这段汇编代码就是main函数的实现。你可以看到,程序先将10赋值给x,将20赋值给y,然后将xy相加,结果存到sum里。

2. 常用选项:

选项 作用
-d 反汇编所有代码段
-M intel 使用 Intel 汇编语法(默认是 AT&T 语法)
-S 将 C/C++ 源代码和汇编代码混合显示
-l 显示行号信息
-j <section_name> 反汇编指定 section 的代码 (例如 .text)

例子(使用 Intel 语法):

objdump -d -M intel hello

同样是main函数,这次的汇编代码会变成这样:

0000000000001149 <main>:
    1149:       push   rbp
    114a:       mov    rbp,rsp
    114d:       sub    rsp,0x10
    1151:       mov    DWORD PTR [rbp-0x4],0xa  ; x = 10
    1158:       mov    DWORD PTR [rbp-0x8],0x14 ; y = 20
    115f:       mov    eax,DWORD PTR [rbp-0x4]
    1162:       add    eax,DWORD PTR [rbp-0x8]
    1165:       mov    DWORD PTR [rbp-0xc],eax  ; sum = x + y
    1168:       mov    esi,0x0
    116d:       mov    edi,0x4
    1172:       call   1177 <_ZNSolsEi@plt>
    ...

Intel 语法更容易阅读,因为它将目标操作数放在源操作数的左边。

3. 深入一点:.plt.got

在上面的反汇编代码中,你可能会看到类似call 1177 <_ZNSolsEi@plt>这样的调用。这里的_ZNSolsEi@plt是什么鬼?

  • .plt (Procedure Linkage Table): 过程链接表,用于延迟绑定外部函数。简单来说,就是程序在第一次调用外部函数时,才会去找到它的实际地址。
  • .got (Global Offset Table): 全局偏移表,用于存储全局变量和外部函数的地址。

当程序调用一个外部函数时,它首先会跳转到.plt中对应的条目。.plt中的代码会负责在.got中查找该函数的地址。如果.got中还没有该函数的地址,则会通过动态链接器(ld-linux.so)去找到该函数的地址,并将其写入.got中。下次再调用该函数时,就可以直接从.got中读取地址,而不需要再次调用动态链接器。

这种机制被称为延迟绑定,它可以提高程序的启动速度,因为程序不需要在启动时就加载所有外部函数的地址。

三、 readelf:文件结构分析大师

readelf可以让你深入了解ELF文件的结构,包括文件头、段(section)头、符号表等等。

1. 基本用法:

readelf -h <可执行文件或共享库>  # 显示文件头
readelf -S <可执行文件或共享库>  # 显示段头表
readelf -s <可执行文件或共享库>  # 显示符号表

例子:

readelf -h hello

你会看到ELF文件的头部信息,包括:

  • Magic number: 标识该文件是ELF文件。
  • Class: 指定文件是32位还是64位。
  • Data: 指定字节序(大端还是小端)。
  • Entry point address: 程序的入口地址。
readelf -S hello

你会看到段头表,其中包含各个段的信息,例如:

  • .text: 代码段,包含可执行指令。
  • .data: 已初始化的数据段,包含全局变量和静态变量。
  • .bss: 未初始化的数据段,包含未初始化的全局变量和静态变量。
  • .rodata: 只读数据段,包含常量字符串。
  • .symtab: 符号表,包含函数名、变量名等符号信息。
  • .strtab: 字符串表,用于存储符号表中使用的字符串。
readelf -s hello

你会看到符号表,其中包含程序中定义的函数和变量的信息,例如:

  • Name: 符号的名称。
  • Value: 符号的地址。
  • Size: 符号的大小。
  • Type: 符号的类型(例如,函数、对象)。
  • Bind: 符号的绑定类型(例如,全局、局部)。
  • Ndx: 符号所在的段的索引。

2. 常用选项:

选项 作用
-h 显示 ELF 文件头
-S 显示段头表
-l 显示程序头表 (用于描述如何将文件加载到内存中)
-s 显示符号表
-d 显示动态段
-r 显示重定位段
-a 显示所有信息

3. 深入一点:动态链接和重定位

  • 动态链接: 动态链接是指程序在运行时才将外部函数链接到程序中。这可以减少程序的大小,并允许多个程序共享同一个共享库。
  • 重定位: 重定位是指在程序加载到内存中时,需要修改某些指令的地址,以便它们指向正确的内存位置。

readelf-d-r选项可以帮助你了解动态链接和重定位的细节。

四、 实际应用场景

1. 调试崩溃的程序

假设你的程序崩溃了,并且你得到了一个core dump文件。你可以使用gdb加载core dump文件,并使用objdump来查看崩溃位置的汇编代码。

gdb <程序名> <core dump文件>
(gdb) list  # 查看源代码
(gdb) disas  # 查看汇编代码

通过分析汇编代码,你可以找到崩溃的原因。

2. 优化性能

你可以使用perf等性能分析工具来找到程序的性能瓶颈。然后,你可以使用objdump来查看性能瓶颈处的汇编代码,看看是否有优化的空间。

3. 分析共享库的依赖关系

你可以使用ldd命令或者readelf -d命令来查看共享库的依赖关系。

ldd <共享库名>
readelf -d <共享库名>

这可以帮助你了解程序需要哪些共享库才能运行。

五、 总结

objdumpreadelf是C++程序员的必备工具。它们可以帮助你深入了解你的程序,从而更好地调试问题、优化性能、分析安全漏洞。虽然刚开始使用时可能会觉得有点复杂,但是只要你多加练习,就会发现它们非常强大。

记住,这两个工具只是冰山一角,还有很多其他的工具和技术可以帮助你更好地理解C++程序。继续学习,不断探索,你一定会成为一名优秀的C++程序员!

希望这篇“讲座”对你有所帮助! 现在,拿起你的武器(objdumpreadelf),去探索你的程序吧! 祝你编程愉快!

发表回复

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