在可执行文件中找到访问内存的机器指令

Locate the machine instructions that access the memory in executable

编辑:我想通过插入断点并比较断点前后的内存来测试系统。
我使用静态分析来获取 C 源代码位置列表和调试信息(即矮人)提供了 C 源代码和可执行文件中的机器指令之间的映射。
但是问题是有很多机器指令映射到一行C源代码,我需要测试所有这些指令。
要测试的机器指令是修改内存状态。 所以我想通过删除不修改内存的指令来减少指令的数量。

例如,我有以下源代码 test.c 并且我有 line number 5.

2   int var1 = 10;
3   void foo() {
4       int *var2 = (int*)malloc(sizeof(int));
5       for(*var2=var1;;) {
6       /* ... */
7       }
8   }

说清楚,line number 5访问全局内存var1和堆内存*var2

我用命令gcc -g test.c编译了上面的程序,结果是

(a.out)
00000000004004d6 <foo>:
  4004d6:   55                      push   %rbp
  4004d7:   48 89 e5                mov    %rsp,%rbp
  4004da:   48 83 ec 10             sub    [=11=]x10,%rsp
  4004de:   bf 04 00 00 00          mov    [=11=]x4,%edi
  4004e3:   e8 d8 fe ff ff          callq  4003c0 <malloc@plt>
  4004e8:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  4004ec:   8b 15 1e 04 20 00       mov    0x20041e(%rip),%edx        # 600910 <var2>
  4004f2:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4004f6:   89 10                   mov    %edx,(%rax)
  4004f8:   eb fe                   jmp    4004f8 <foo+0x22>

dwarfdump -l a.out给我以下结果。

0x004004d6  [   3, 0] NS uri: "/home/workspace/test.c"
0x004004de  [   4, 0] NS
0x004004ec  [   5, 0] NS
0x004004f8  [   5, 0] DI=0x1

现在我知道,在 a.out 中,位置 0x4004ec0x4004f20x4004f60xf004f8 映射到 line number 5 在 C 源代码中。
但是我想排除不访问(堆,全局或本地)内存的0x4004f8 (jmp)

有谁知道如何只获取访问内存的指令?

这只是回答有关查找具有显式内存操作数的 asm 指令的问题。关于将它们与 C 语句相关联的部分在 -O0 编译器输出之外是相当虚假的(其中每个语句被编译成一个单独的指令块以支持 GDB 的 jump 到同一函数中的另一行,或修改在断点处停止时内存中的变量)。请参阅 Basile 的回答,该回答试图对问题中的 C 语句内容有所了解。


Intel 语法反汇编可能很方便,因为所有显式内存操作数都将包含 ptr,如 mov rax, qword ptr [rbp - 0x8],因此您可以进行文本搜索。

在 asm 源代码中,当寄存器操作数暗示操作数大小时,不需要 <size> ptr 语法,但像 objdump -drwC -Mintel 这样的反汇编程序总是把它放在里面。

在 AT&T 语法中,您也可以只查找 () 或纯符号名称作为操作数。

不要忘记过滤掉 lea 指令。 lea 类似于 C 中的 & 运算符。它是一个使用内存操作数语法和机器编码的移位和添加指令。

另外不要忘记过滤掉各种长nop指令,这些指令使用寻址模式在一条指令中获得正确的填充量。例如:

66 2e 0f 1f 84 00 00 00 00 00   nop    WORD PTR cs:[rax+rax*1+0x0]

所以如果助记符是leanop,则忽略该指令。 (32 位代码有时使用其他指令作为 NOP,但通常它实际上是一个 lea,它在编译器 gas / ld 生成的机器代码中为自己设置一个寄存器 .p2align 指令。)


objdump 使用显式操作数反汇编 rep stos,例如 rep stos QWORD PTR es:[rdi],rax。所以你实际上会得到 rep movsrep stos 操作数。 (请注意,rep movsrep cmps 有两个内存操作数,与普通指令不同。它们隐含在机器代码中,但 objdump 使它们显式。)这也会丢失隐式内存操作数就像 push / popcall / ret.

的堆栈

一条给定的 C 语句被编译成几条机器指令,其中有几条可能会访问内存。想想像 ptr->fld = arr[i++] * arr[j]--; .... 顺便说一句,在 一些 情况下,arr[j] 可能早先使用过,可能已经坐在一些 register ,因此可能不需要另一个内存加载(但只需要一个存储,以后可以延迟)。

I want to know the location, in executable, of the machine instruction that accesses (heap, global or local) memory generated by the given code

所以你的问题一般来说可能没有意义。多个机器指令(或其中 none 个)可能会访问内存(与源代码中的单个 C 语句相关)。并且 register allocation 和寄存器溢出可能会发生,因此给定的机器指令可能与 C 变量相关,而 C 变量与 "current" C 指令(没有意义)相去甚远。

一个optimizing compiler is allowed to mix the several C statements and might output intermixed machine code. Read also about sequence points。机器代码指令和 C 语句之间没有明显的映射(特别是启用了优化),这就是为什么您经常在启用较少优化的情况下进行调试(因此 gcc -g 更喜欢与 -O0-Og, 不多了).

使用GCC编译你的src.c源文件使用

gcc -O -S -Wall -fverbose-asm src.c

你会得到一个更具可读性的 src.s 汇编文件。您可以使用某些编辑器或寻呼机查看生成的文件。

Does anyone know how to get only instructions that access memory?

这没有多大意义。优化编译器有时会共享一些与几个不同 C 语句相关的通用机器代码。

顺便说一句,您可能还要求 GCC 转储各种内部表示,例如使用 gcc -O -fdump-tree-all ;然后你会得到数百个(文本)内部转储文件(部分转储各种内部表示)。请记住,GCC 有数百个 optimization passes.

请注意,您可能对处理 GCC 内部表示(例如 GENERIC or GIMPLE or even RTL) by adding your own GCC plugin (or GCC MELT 扩展)更感兴趣。这可能需要数月的工作(尤其是要理解 GCC 内部架构和表示的细节)。

如果不了解您的高级目标和动机,我们就无法为您提供更多帮助。

您应该阅读更多关于 semantics and about undefined behavior 的内容,它与您的问题(间接地)比您所相信的更相关。

请注意,C 语句不对应(一对多)机器指令。 优化编译器不会一个接一个地编译 C 语句,它编译整个翻译单元 一次 (例如可以做 内联扩展 循环展开 堆栈展开常量折叠register allocation和溢出、过程间优化死代码消除)。这就是为什么 C 编译器 如此复杂 拥有数百万行源代码的野兽。顺便说一句,大多数 C 编译器(例如 GCCClang)都是 自由软件 ,因此您可以花几个月或几年的时间研究它们的源代码代码。

也阅读一些关于编译器的好书(例如最新的Dragon Book), some books on semantics, and on programming languages pragmatics

如果您对 GCC 内部机制特别感兴趣,我的 GCC MELT documentation page (also available here) 包含 很多 幻灯片和参考资料。

如果你只关心机器指令,你可能会完全忘记 C 并在一些反汇编程序库的帮助下工作libopcode(参见 this),仅针对目标文件中的机器代码。

另请查看其他静态源代码分析器,包括 Coccinelle & Frama-C and libclang

如果您只对 GCC 发出的代码感兴趣并且有能力重新编译您的 C 源代码,您可以改为在 GIMPLE 级别的 GCC 编译器内部工作(通过您的 GCC 插件或 GCC MELT 扩展)并且检测(并可能转换)那些访问内存的 GIMPLE 指令。检测(并可能转换)修改内存的 GIMPLE 语句可能更简单并且可能就足够了。

I want to test the system by inserting a breakpoint and comparing memory before and after the breakpoint.

这有点类似于例如地址消毒剂GCC 的其他 仪器功能。你可以花几年时间做类似的事情(并转换一些 GIMPLE),然后你可能想在 GCC 中添加几个额外的通道(你可能需要一些额外的运行时支持)。

但是请注意,最近的 GDB 是可编写脚本的(在 Guile 或 Python 中)并且有观察点。如果您只想调试一个特定的程序,这可能就足够了(您可能不需要深入研究编译器内部结构,这需要花费数月或数年的时间)。您还应该 使用 valgrind and address sanitizers.