顺带一提,作为家庭作业,你可以试着解释以下为什么这个指令如此不方便。

    C/C++循环操作是由for()、while()、do/while()命令发起的。

    让我们从for()开始吧。

    这个命令定义了循环初始值(为循环计数器设置初值),循环条件(比如,计数器是否大于一个阈值?),以及在每次迭代(增/减)时和循环体中做什么。

    所以,它生成的代码也将被考虑为4个部分。

    让我们从一个简单的例子开始吧:

    1. void f(int i)
    2. {
    3. printf ("f(%d)
    4. ", i);
    5. };
    6. int main()
    7. {
    8. int i;
    9. for (i=2; i<10; i++)
    10. f(i);
    11. return 0;
    12. };

    反汇编结果如下(MSVC 2010):

    清单12.1: MSVC 2010

    1. _i$ = -4
    2. _main PROC
    3. push ebp
    4. mov ebp, esp
    5. push ecx
    6. mov DWORD PTR _i$[ebp], 2 ; loop initialization
    7. jmp SHORT $LN3@main
    8. $LN2@main:
    9. mov eax, DWORD PTR _i$[ebp] ; here is what we do after each iteration:
    10. add eax, 1 ; add 1 to i value
    11. mov DWORD PTR _i$[ebp], eax
    12. $LN3@main:
    13. cmp DWORD PTR _i$[ebp], 10 ; this condition is checked *before* each iteration
    14. jge SHORT $LN1@main ; if i is biggest or equals to 10, lets finish loop
    15. mov ecx, DWORD PTR _i$[ebp] ; loop body: call f(i)
    16. push ecx
    17. call _f
    18. add esp, 4
    19. jmp SHORT $LN2@main ; jump to loop begin
    20. $LN1@main: ; loop end
    21. xor eax, eax
    22. mov esp, ebp
    23. pop ebp
    24. ret 0
    25. _main ENDP

    看起来没什么特别的。

    GCC 4.4.1生成的代码也基本相同,只有一些微妙的区别。

    清单12.1: GCC 4.4.1

    现在,让我们看看如果我们打开了优化开关会得到什么结果(/Ox):

    清单12.3: 优化后的 MSVC

    1. _main PROC
    2. push esi
    3. mov esi, 2
    4. $LL3@main:
    5. call _f
    6. add esp, 4
    7. cmp esi, 10 ; 0000000aH
    8. jl SHORT $LL3@main
    9. xor eax, eax
    10. pop esi
    11. ret 0
    12. _main ENDP

    要说它做了什么,那就是:本应在栈上分配空间的变量i被移动到了寄存器ESI里面。因为我们这样一个小函数并没有这么多的本地变量,所以它才可以这么做。 这么做的话,一个重要的条件是函数f()不能改变ESI的值。我们的编译器在这里倒是非常确定。假设编译器决定在f()中使用ESI寄存器的话,ESI的值将在函数的初始化阶段被压入栈保存,并且在函数的收尾阶段将其弹出(注:即还原现场,保证程序片段执行前后某个寄存器值不变)。这个操作有点像函数开头和结束时的PUSH ESI/ POP ESI操作对。

    让我们试一试开启了最高优化的GCC 4.4.1(-03优化)。

    1. main proc near
    2. var_10 = dword ptr -10h
    3. push ebp
    4. mov ebp, esp
    5. and esp, 0FFFFFFF0h
    6. sub esp, 10h
    7. mov [esp+10h+var_10], 2
    8. call f
    9. mov [esp+10h+var_10], 3
    10. call f
    11. mov [esp+10h+var_10], 4
    12. call f
    13. mov [esp+10h+var_10], 5
    14. call f
    15. mov [esp+10h+var_10], 6
    16. call f
    17. mov [esp+10h+var_10], 7
    18. call f
    19. mov [esp+10h+var_10], 8
    20. call f
    21. mov [esp+10h+var_10], 9
    22. call f
    23. xor eax, eax
    24. leave
    25. retn
    26. main endp

    GCC直接把我们的循环给分解成顺序结构了。

    循环分解(Loop unwinding)对这些没有太多迭代次数的循环结构来说是比较有利的,移除所有循环结构之后程序的效率会得到提升。但是,这样生成的代码明显会变得很大。

    好的,现在我们把循环的最大值改为100。GCC现在生成如下:

    清单12.5: GCC

    这时,代码看起来非常像MSVC 2010开启/Ox优化后生成的代码。除了这儿它用了EBX来存储变量i。 GCC也确信f()函数中不会修改EBX的值,假如它要用到EBX的话,它也一样会在函数初始化和收尾时保存EBX和还原EBX,就像这里main()函数做的事情一样。

    让我们通过/Ox和/Ob0编译程序,然后放到OllyDbg里面查看以下结果。

    看起来OllyDbg能够识别简单的循环,然后把它们放在一块,为了演示方便,大家可以看图12.1。

    通过跟踪代码(F8, 步过)我们可以看到ESI是如何递增的。这里的例子是ESI = i = 6: 图12.2。

    9是i的最后一个循环制,这也就是为什么JL在递增的最后不会触发,之后函数结束,如图12.3。

    图12.1: OllyDbg main()开始

    12.1 x86 - 图2

    图12.2: OllyDbg: 循环体刚刚递增了i,现在i=6

    图12.3: OllyDbg中ESI=10,循环终止

    12.1.2 跟踪

    我在IDA中打开了编译后的例子,然后找到了PUSH ESI指令(作用:给f()传递唯一的参数)的地址,对我的机器来说是0x401026,然后我运行了跟踪器:

    tracer.exe -l:loops_2.exe bpx=loops_2.exe!0x00401026

    BPX的作用只是在对应地址上设置断点然后输出寄存器状态。

    在tracer.log中我看到执行后的结果:

    1. PID=12884|New process loops_2.exe
    2. (0) loops_2.exe!0x401026
    3. EAX=0x00a328c8 EBX=0x00000000 ECX=0x6f0f4714 EDX=0x00000000
    4. ESI=0x00000002 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    5. EIP=0x00331026
    6. FLAGS=PF ZF IF
    7. (0) loops_2.exe!0x401026
    8. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    9. ESI=0x00000003 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    10. FLAGS=CF PF AF SF IF
    11. (0) loops_2.exe!0x401026
    12. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    13. ESI=0x00000004 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    14. EIP=0x00331026
    15. FLAGS=CF PF AF SF IF
    16. (0) loops_2.exe!0x401026
    17. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    18. ESI=0x00000005 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    19. EIP=0x00331026
    20. FLAGS=CF AF SF IF
    21. (0) loops_2.exe!0x401026
    22. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    23. ESI=0x00000006 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    24. EIP=0x00331026
    25. FLAGS=CF PF AF SF IF
    26. (0) loops_2.exe!0x401026
    27. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    28. ESI=0x00000007 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    29. EIP=0x00331026
    30. FLAGS=CF AF SF IF
    31. (0) loops_2.exe!0x401026
    32. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    33. ESI=0x00000008 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    34. EIP=0x00331026
    35. FLAGS=CF AF SF IF
    36. (0) loops_2.exe!0x401026
    37. EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
    38. ESI=0x00000009 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
    39. EIP=0x00331026
    40. FLAGS=CF PF AF SF IF
    41. PID=12884|Process loops_2.exe exited. ExitCode=0 (0x0)

    我们可以看到ESI寄存器是如何从2变为9的。

    甚至于跟踪器可以收集某个函数调用内所有寄存器的值,所以它被叫做跟踪器(a trace)。每个指令都会被它跟踪上,所有感兴趣的寄存器值都会被它提示出来,然后收集下来。 然后可以生成IDA能用的.idc-script。所以,在IDA中我知道了main()函数地址是0x00401020,然后我执行了:

    tracer.exe -l:loops_2.exe bpf=loops_2.exe!0x00401020,trace:cc

    bpf的意思是在函数上设置断点。

    结果是我得到了loops_2.exe.idc和loops_2.exe_clear.idc两个脚本。我加载loops_2.idc到IDA中,然后可以看到图12.4所示的内容。

    我们可以看到ESI在循环体开始时从2变化为9,但是在递增完之后,它的值从9(译注:作者原文是3,但是揣测是笔误,应为9。)变为了0xA(10)。我们也可以看到main()函数结束时EAX被设置为了0。

    编译器也生成了loops_2.exe.txt,包含有每个指令执行了多少次和寄存器值的一些信息:

    清单12.6: loops_2.exe.txt

    1. 0x401020 (.text+0x20), e= 1 [PUSH ESI] ESI=1
    2. 0x401021 (.text+0x21), e= 1 [MOV ESI, 2]
    3. 0x401026 (.text+0x26), e= 8 [PUSH ESI] ESI=2..9
    4. 0x401027 (.text+0x27), e= 8 [CALL 8D1000h] tracing nested maximum level (1) reached,
    5. skipping this CALL 8D1000h=0x8d1000
    6. 0x40102c (.text+0x2c), e= 8 [INC ESI] ESI=2..9
    7. 0x40102d (.text+0x2d), e= 8 [ADD ESP, 4] ESP=0x38fcbc
    8. 0x401030 (.text+0x30), e= 8 [CMP ESI, 0Ah] ESI=3..0xa
    9. 0x401033 (.text+0x33), e= 8 [JL 8D1026h] SF=false,true OF=false
    10. 0x401035 (.text+0x35), e= 1 [XOR EAX, EAX]
    11. 0x401038 (.text+0x38), e= 1 [RETN] EAX=0

    生成的代码可以在此使用:

    12.1 x86 - 图4

    图12.4: IDA加载了.idc-script之后的内容