循环 pintool 的通用模式
Generic pattern for a loop- pintool
我正在尝试找到一个能够 运行 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 [=10=]x1,-0x4(%rbp)
40c3b7: 81 7d fc ff 00 00 00 cmpl [=10=]xff,-0x4(%rbp)
40c3be: 7e b4 jle 40c374 <makeMaps_e+0x1c>
现在我注意到 Check CMD
并不总是 CMP
...
有没有办法找出 index
值和总迭代次数?
Is there a way of finding out the index value and total number of iterations?
没有
某些循环可能具有非索引逻辑,例如以非平凡的方式推进两个以上的组件键,例如链表遍历(您必须实际遍历树才能告诉total count),其中主要终止条件是(leaf_ptr == nullptr)
,加上一些循环可能有几个不同的终止条件(break;
),那么你会选择哪个作为你的迭代次数?实际触发循环退出的最大值,或者更早的那个 运行?
对于经典的 for (i=...)
循环,您可以在代码中手动找到它,但是 "index" 几乎可以隐藏在任何东西中,没有通用模式。例如,一些循环在机器代码中从 -10
到 -1
(而 C++ 源代码是 for(i=0; i <10; ++i)
),通过使用简单的 jnz
使终止条件更便宜,其他循环可能使用地址比较而不是整数计数器(而 C++ 源代码使用数组索引)等。
一般来说,每个字节可以包含256个不同的值。如果你有一段 200 字节长的代码(这就像一些非平凡的循环实际上在做某事),你有 256200 种可能的机器代码。其中只有一小部分是有意义的机器代码构造一个循环,但其中只有一小部分仍然是非常多的选项,代码如何运行和操作。寻找一种乐观的通用模式。
同样在纯 theory/abstract 级别上,这听起来非常接近原始 "halting problem",即您有一些代码,并且您希望能够判断它是否会停止,何时停止执行(有一个额外的请求,可以实际告诉它在停止之前将执行多少次迭代)。所以你可以研究一下,为什么这实际上无法解决。
你仍然可以产生某种循环 pintool 启发式,它可能像最常见的那样捕获,如果你将针对一些不太高级的机器代码(比如一些旧编译器的未优化的 C),你可能会检测到令人惊讶的大量循环,但这完全是 "non-generic" 解决方案,即使是这种不可靠的启发式方法也需要大量的努力和思考。
正如@Ped7g 所说,并非所有循环都具有在第一次迭代运行之前已知的行程计数。
但是对于循环,它们通常在循环内部(通常在底部)仅使用一个条件分支进行编译。像这样的启发式方法可能会检测到它们。
找到循环分支。
如果循环内部有多个条件分支,其中一个离开循环不再返回,或者跳过计数器递增,则这不是一个简单的循环。
循环体内的嵌套循环或条件不是问题,如果您可以验证它们不会修改您正在查看的(外)循环的循环计数器。
找到它依赖的寄存器。例如cmp p, end_pointer / jne
或 dec eax / jnz
例如从循环分支向后工作到第一个标志设置指令。 (如果编译器正在针对宏融合进行优化,那么它们将背靠背,但不幸的是,在通用调优中并不总是启用。)编译器通常会避免部分标志恶作剧(如 sub ecx, 1
/ dec eax
/ jnc
), 但如果你想更加安全,请确保标志设置指令实际设置分支读取的标志。
检查这些寄存器中只有一个在循环中实际被修改。
- 检查修改后的寄存器(循环计数器)是否每次迭代只修改一个常数
给定循环计数器的起始值和与之比较的结束值,您可以计算行程计数。如果这些检查中的任何一个失败,根据这个启发式,它不是 "simple loop"。
如果你想让它在未优化的代码上工作,你必须通过 store/reload 跟随循环计数器到内存。即使是优化编译器有时也会发出多条指令来触及循环计数器,例如 lea ecx, [rdi+1]
然后使用 ecx 一段时间,然后 mov edi, ecx
。通常这看起来像是一个优化失误(循环中的更多指令在编译器需要的地方有循环外的东西)但它确实发生了。
我正在尝试找到一个能够 运行 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 [=10=]x1,-0x4(%rbp)
40c3b7: 81 7d fc ff 00 00 00 cmpl [=10=]xff,-0x4(%rbp)
40c3be: 7e b4 jle 40c374 <makeMaps_e+0x1c>
现在我注意到 Check CMD
并不总是 CMP
...
有没有办法找出 index
值和总迭代次数?
Is there a way of finding out the index value and total number of iterations?
没有
某些循环可能具有非索引逻辑,例如以非平凡的方式推进两个以上的组件键,例如链表遍历(您必须实际遍历树才能告诉total count),其中主要终止条件是(leaf_ptr == nullptr)
,加上一些循环可能有几个不同的终止条件(break;
),那么你会选择哪个作为你的迭代次数?实际触发循环退出的最大值,或者更早的那个 运行?
对于经典的 for (i=...)
循环,您可以在代码中手动找到它,但是 "index" 几乎可以隐藏在任何东西中,没有通用模式。例如,一些循环在机器代码中从 -10
到 -1
(而 C++ 源代码是 for(i=0; i <10; ++i)
),通过使用简单的 jnz
使终止条件更便宜,其他循环可能使用地址比较而不是整数计数器(而 C++ 源代码使用数组索引)等。
一般来说,每个字节可以包含256个不同的值。如果你有一段 200 字节长的代码(这就像一些非平凡的循环实际上在做某事),你有 256200 种可能的机器代码。其中只有一小部分是有意义的机器代码构造一个循环,但其中只有一小部分仍然是非常多的选项,代码如何运行和操作。寻找一种乐观的通用模式。
同样在纯 theory/abstract 级别上,这听起来非常接近原始 "halting problem",即您有一些代码,并且您希望能够判断它是否会停止,何时停止执行(有一个额外的请求,可以实际告诉它在停止之前将执行多少次迭代)。所以你可以研究一下,为什么这实际上无法解决。
你仍然可以产生某种循环 pintool 启发式,它可能像最常见的那样捕获,如果你将针对一些不太高级的机器代码(比如一些旧编译器的未优化的 C),你可能会检测到令人惊讶的大量循环,但这完全是 "non-generic" 解决方案,即使是这种不可靠的启发式方法也需要大量的努力和思考。
正如@Ped7g 所说,并非所有循环都具有在第一次迭代运行之前已知的行程计数。
但是对于循环,它们通常在循环内部(通常在底部)仅使用一个条件分支进行编译。像这样的启发式方法可能会检测到它们。
找到循环分支。
如果循环内部有多个条件分支,其中一个离开循环不再返回,或者跳过计数器递增,则这不是一个简单的循环。
循环体内的嵌套循环或条件不是问题,如果您可以验证它们不会修改您正在查看的(外)循环的循环计数器。
找到它依赖的寄存器。例如
cmp p, end_pointer / jne
或dec eax / jnz
例如从循环分支向后工作到第一个标志设置指令。 (如果编译器正在针对宏融合进行优化,那么它们将背靠背,但不幸的是,在通用调优中并不总是启用。)编译器通常会避免部分标志恶作剧(如
sub ecx, 1
/dec eax
/jnc
), 但如果你想更加安全,请确保标志设置指令实际设置分支读取的标志。检查这些寄存器中只有一个在循环中实际被修改。
- 检查修改后的寄存器(循环计数器)是否每次迭代只修改一个常数
给定循环计数器的起始值和与之比较的结束值,您可以计算行程计数。如果这些检查中的任何一个失败,根据这个启发式,它不是 "simple loop"。
如果你想让它在未优化的代码上工作,你必须通过 store/reload 跟随循环计数器到内存。即使是优化编译器有时也会发出多条指令来触及循环计数器,例如 lea ecx, [rdi+1]
然后使用 ecx 一段时间,然后 mov edi, ecx
。通常这看起来像是一个优化失误(循环中的更多指令在编译器需要的地方有循环外的东西)但它确实发生了。