我试图找到一种通用的模式来运行pintool程序,以便它始终为我提供index
的位置或含义,以及循环的值。
例如,这是某个循环的组合:
40c374: 48 8b 55 e8 mov -0x18(%rbp),%rdx
40c378: 8b 45 fc mov -0x4(%rbp),%eax
40c37b: 48 98 cltq
40c37d: 0f b6 84 02 80 00 00 movzbl 0x80(%rdx,%rax,1),%eax
40c384: 00
40c385: 84 c0 test %al,%al
40c387: 74 2a je 40c3b3 <makeMaps_e+0x5b>
40c389: 48 8b 45 e8 mov -0x18(%rbp),%rax
40c38d: 8b 40 7c mov 0x7c(%rax),%eax
40c390: 89 c1 mov %eax,%ecx
40c392: 48 8b 55 e8 mov -0x18(%rbp),%rdx
40c396: 8b 45 fc mov -0x4(%rbp),%eax
40c399: 48 98 cltq
40c39b: 88 8c 02 80 01 00 00 mov %cl,0x180(%rdx,%rax,1)
40c3a2: 48 8b 45 e8 mov -0x18(%rbp),%rax
40c3a6: 8b 40 7c mov 0x7c(%rax),%eax
40c3a9: 8d 50 01 lea 0x1(%rax),%edx
40c3ac: 48 8b 45 e8 mov -0x18(%rbp),%rax
40c3b0: 89 50 7c mov %edx,0x7c(%rax)
40c3b3: 83 45 fc 01 addl $0x1,-0x4(%rbp)
40c3b7: 81 7d fc ff 00 00 00 cmpl $0xff,-0x4(%rbp)
40c3be: 7e b4 jle 40c374 <makeMaps_e+0x1c>
现在我注意到Check CMD
并不总是CMP
...
是否可以找出index
的值和迭代总数?
答案 0 :(得分:1)
有没有一种方法可以找出索引值和迭代总数?
否。
某些循环可能具有非索引逻辑,例如以非平凡的方式推进了两个+组件键,例如,链表遍历(实际上,您必须遍历树以甚至能够算出总数) ),其中主要终止条件为(leaf_ptr == nullptr)
,加上某些循环可能具有几种不同的终止条件(break;
),那么您将为哪个迭代次数选择一个?在实际运行中会触发循环退出的最大次数还是较早的次数?
对于经典的for (i=...)
循环,您可以在代码中手动找到它,但是“索引”几乎可以隐藏在任何东西中,没有通用模式。某些循环例如在机器码中从-10
到-1
(而C ++源为for(i=0; i <10; ++i)
)通过使用简单的jnz
使终止条件更便宜,其他循环可能使用地址比较而不是整数计数器(而C ++源代码正在使用数组索引)。
通常,每个字节可以包含256个不同的值。如果您有一段200字节长的代码(就像某些非平凡的循环实际上在做某件事),那么您就有256 200 个可能的机器代码。其中只有一小部分是有意义的机器代码,它构成了一个循环,但其中只有一小部分仍然有很多选择,包括代码的行为方式和操作方式。寻找一种通用的模式是乐观的。
在纯理论/抽象水平上,这听起来也非常接近原始的"halting problem",即您有一些代码,并且希望能够知道它是否会停止执行,何时执行(使用奖金请求,以便实际上能够知道它将停止多少次迭代)。因此,您可以研究为什么它实际上无法解决。
您仍然可以产生某种类似于最常见的循环pintool启发式算法,并且如果您要针对一些不太高级的机器代码(例如某些旧编译器的未优化C语言),则可能会发现出乎意料的高大量的循环,但这完全是“非通用”解决方案,即使是这种不可靠的启发式方法也需要大量的努力和思考。
答案 1 :(得分:1)
正如@ Ped7g所说,并不是所有循环都具有在第一次迭代运行之前就知道的跳闸计数。
但是对于这样的循环,通常只在循环内只有一个条件分支(通常在底部)编译它们。这样的启发式方法可能会检测到它们。
找到循环分支。
如果循环内有多个条件分支,而其中一个条件分支离开循环却不返回或跳过计数器增量,那么这不是简单的循环。
如果可以验证它们没有针对您正在查看的(外部)循环修改循环计数器,则循环体内的嵌套循环或条件语句不是问题。
找到它依赖的寄存器。例如cmp p, end_pointer / jne
或dec eax / jnz
例如从循环分支向后执行第一条标志设置指令。 (如果编译器针对宏融合进行了优化,那么它们将是背靠背的,但不幸的是,在通用调优中并不总是启用该功能。)编译器通常避免使用部分标志的恶作剧(例如sub ecx, 1
/ {{1 }} / dec eax
),但是如果您想更加安全,请确保标志设置指令实际上设置了分支读取的标志。
检查循环中是否只有这些寄存器中的一个实际上被修改了。
给出循环计数器的起始值和与之比较的终止值,就可以计算出行程数。如果这些检查中的任何一项失败,那么根据这种启发式方法,它就不是“简单循环”。
如果希望它在未优化的代码上运行,则必须跟踪循环计数器,直到存储/重新加载到内存。即使是优化的编译器,有时也会发出涉及循环计数器的多个指令,例如jnc
,然后使用ecx一段时间,然后是lea ecx, [rdi+1]
。通常,这看起来像是优化遗漏(循环中有更多指令,使编译器希望在循环外找到某些东西),但是它确实发生了。