[转组第6天] | 流程控制语句的识别

2018-05-03

《C++反汇编和逆向技术》第五章  流程控制语句的识别读书笔记(写了半天没保存,全没了,慢慢补充)

  1.if语句块

  if语句块的功能是先对运算条件进行比较,然后根据比较结果选择对应的语句块来执行。

cmp    dword ptr[ebp+8],0
jne    main+2Fh(0040103f) 
C语言中if语句argc == 0,对应汇编if是jne(不等于0则跳转)

总结:
;先执行各类影响标志位的指令 ;其后是各种条件跳转指令 jxx xxxx

  汇编语言的if语句跟C语言中if语句是相反的,因为按照C语言中if语句的规定,满足if判定的表达式才能执行if的语句块,而汇编语言的条件跳转却是满足某条件则跳转,绕过代码块。

  既然这样,为什么C语言编译器不将else语句块提到前面呢?

  因为C语言是根据代码行的位置来决定编译后的二进制代码的地址高低的,也就是说,低行数对应低地址,高行数对应高地址,所以有时会使用标号相减得到代码段的长度。鉴于此,C语言的编译器不能随意改变代码行在内存中的顺序。

  CMP指令 对两个操作数进行一次减法操作。涉及到的标志位:

    CF: 是否有进位或借位操作

    ZF: 0标志位,就是结果是否为0

    OF: 溢出标志位,计算机结果是否溢出了,只有有符号数才会溢出

    SF: 符号位,0为正,1为负。】

  【有符号数与无符号数比较后,使用的跳转指令不同。这是由于无符号数只考虑CF,ZF。无符号数还要考虑SF,OF。

    如果比较指令是ja/jae/jb/jbe,是无符号数的比较。

    如果比较指令是jg/jge/jl/jle.是有符号数的比较。

  

  2.if...else语句块

cmp    dword ptr [ebp+8],0
jne    IfElse+2Dh      ;跳转到else语句块
...    ;if语句块
jmp    IfElse+3Ah    ;跳转过else块
...    ;else语句块

总结:
;先执行影响标志位的相关指令
    jxx    ELSE_BEGIN
IF_BEGIN:
    ......    ;if语句块内的执行代码
IF_END:
    jmp    ELSE_END
ELSE_BEGIN:
    ......    ;else语句块内的执行代码
ELSE_END:

  

  3.if...else_if...else语句块

cmp    dword ptr [ebp+8],0
jle    IfElseIf+2Dh      ;跳转到下一个else if语句块
...    ;if语句块
jmp    IfElse+4Fh    ;跳到end
cmp    dword ptr [ebp+8],0
jne    IfElseIf+42h       ;跳转到else语句块
...    ;elseif语句块
jmp    IfElse+4Fh    ;跳到end
...    ;else语句块

总结:
;先执行影响标志位的相关指令
    jxx    ELSE_IF_BEGIN
IF_BEGIN:
    ......    ;if语句块内的执行代码
IF_END:
    jmp    END
ELSE_IF_BEGIN:
;可影响标志位的指令
    jxx    ELSE_BEGIN
    ......    ;else if语句块内的执行代码
ELSE_IF_END:
    jmp    END
ELSE_BEGIN:
    ......    ;else语句块内的执行代码
END:

  

  4. switch的真相

   switch是比较常用的多分支结构,使用起来非常方便,并且效率上也高于if...else if多分支结构。

  case语句不超过3条的switch多分支结构:

cmp    dword ptr [ebp-8],1
je    SwitchIf+4Ah    ;跳转到case1代码块
cmp    dword ptr [ebp-8],3
je    SwitchIf+59h    ;跳转到case3代码块
cmp    dword ptr [ebp-8],100
je    SwitchIf+68h    ;跳转到case100代码块
jmp    end
...    ;case1代码块
jmp    end
...    ;case3代码块
jmp    end
...    ;case100代码块
end
注意:switch的条件跳转指令跟C语言语句本身是一致的

  上述代码片段显示switch语句使用了3次条件跳转指令,如果成功则跳转到相应的执行代码块中。这种结构与if...else if多分支结构很相似,不过也有明显的区别:

  通过对比可以发现,if...else if会在每个条件跳转后紧跟语句块;而switch结构则将所有的条件跳转放置在一起,并且所有case语句块也都是连在一起放置的。这样做是为了实现C语法的要求,当case中没有break语句是,顺序执行后续case语句。[两个跳转地址之间就是一个case语句块]

  在switch分支数小于4时,VC++6.0采用模拟if...else if的方法。这样做并没发挥出switch的优势,在效率上也没有if...else if强。

  分支数大于3,并且case的判定值存在明显的线性组合关系时:

  

  代码中为case语句制作了一份case地址表。这个数组保存了每个case语句块的首地址,并且数组的下标从0开始。而case中最小值是1,所有需要对edx减1调整,使其与地址表下标对应寻址。当然,如果case最小值是0,则不需要调整。

  

  在进入switch后会先进行一次比较,检查输入的数值是否大于case值的最大值,如果大于则跳转到default。看00401128地址处case采用比例因子寻址

  

  case块还是连续放置的。实际没有case4块,为了达到线性有序,编译器将以switch结束地址或default语句块的首地址填充。

  

  如果每两个case值之间的差值小于等于6,并且case语句数大于等于4,编译器就会形成这种线性结构。而且在便携式无需有序排列case值,编译器会在编译过程中对case线性地址表进行排序。如case顺序3,2,1,4,5,在地址表中也是,1,2,3,4,5。

  5. 难以构成跳转表的switch

  对于非线性的switch结构,可以采用制作索引表的方法来优化。索引表需要两张表:一张为case语句块地址表,另一张为case语句块索引表。

  地址表中每一项保存一个case语句块的首地址,有几个case语句块就有几项。deault语句块也在其中,这个结束地址只会保存一份,不会像有序线性地址表那样,重复填充switch结束地址。

  索引表中保存地址表的编号,它的大小等于最大case值和最小case值的差。当差值大于255时,这种优化方式也会很浪费空间,可通过树方式优化,下文再讲。

  比如把上面case7改成case15,如果还用有序线性方式优化,则需要把7-15都填充成switch结束地址。很浪费空间。所以采用二次查表法:

  

  索引表中保存了地址表的下标值。索引表中最多可以存储256项,每项的大小为1个字节,这决定了case值不能超过1字节的最大表示范围(0-255)。

  跟线性地址表相比,制作索引表更节省空间,但是由于查两次表,因此效率会有所下降。【虽然索引表和线性地址表都是连续空间,但索引表单位时1字节,线性地址表单位是4字节,所以还是节省空间的】

  占用总字节数 = ((MAX - MIN) * 1字节)+ (SUM * 4字节)

  

  6. switch降低判定树的高度

  当最大case值与最小case值之差大于255,超出索引1字节的表达范围时,上述优化方案同样会造成空间的浪费。此时采用另一种优化方案——判定树:将每个case值作为一个节点,从这些节点中找到一个中间值作为根节点,以此形成一棵平衡二叉树,以每个节点为判定值,大于和小于关系分别对应左子树和右子树,这样可以提高效率。

  如果打开O1选项——体积优先,由于有序线性优化和索引表优化都需要消耗额外的空间,因此在体积优先的情况下,这两种优化方案是不被允许的。编译器尽量以二叉判定树的方式来降低程序占用的体积。

  

  其对应汇编代码会二分查找,先cmp 10分边再比较两边。

  

  从图中发现,这棵树的左右两侧并不平衡。进行树平衡的效率低于if...else。这时,编译器采取的策略是,当树中的叶子节点数小于等于3时,就会转换形成一个if...else结构。

  如果向左子树插入一个叶子节点10000时,左子树叶子节点大于4。此时if else转换已经不适合,优先查看是否可以匹配有序线性优化、非线性索引表优化,如果可以就转换,如果不行,继续使用判定树。

  在Release版下,使用IDA查看编译器是如何优化的,如下图:

  

  根据流程走向可以看出,有一个根节点,左边多分支结构很像一个switch,而右边则是一个多次比较判断,跟if...else类似。

  总结:为了降低树的高度,在树的优化过程中,检测树的左子树或右子树能否满足if else优化、有序线性优化、非线性索引优化,利用这三种优化来降低树高度。选择哪种优化也是有顺序的,谁的效率最高,又满足其匹配条件,就可以被优先使用。以上三种优化都无法匹配,就会选择使用判定树。

  

  7. do/while/for的比较

  do循环:

...    ;do循环语句块内的执行代码
cmp    edx, dword ptr [ebp -8]
jle    LoopDo+26h(0040b4e6)    ;向上跳转到do循环体,且条件跳转指令比较和C语句是一致的
end

总结:
DO_BEGIN:
    ......    ;循环语句块
;影响标记位的指令
jxx    DO_BEGIN    ;向上跳转

  与if语句区别:if语句的比较是相反的,并且跳转地址大于当前代码的地址,是一个向下跳转的过程;而do中跳转逻辑小于当前代码的地址,是一个向上跳转的过程,所以条件跳转的逻辑与源码中的逻辑相同,且条件跳转指令比较和C语句是一致的

  

  while循环:

  while循环和do循环正好相反,在执行循环语句块之前那,必须要进行条件判断,根据比较结果再选择是否执行循环语句块。

总结:
WHILE_BEGIN:
;影响标志位的指令
 jxx    WHILE_END    ;条件成立跳转到循环语句块结尾处
......    ;循环语句块
jmp    WHILE_BEGIN    ;向上跳转
WHILE_END:

  注意:while比较与if语句一样,也是比较相反,向下跳转。while循环结构中使用了两次跳转指令完成循环,比do循环多一次跳转,因此效率会低一点。

  需要注意的是,while循环结构很可能被有化成do循环结构,如下:

if (xxx)
{
    do
    {
        //....
    }while(xxx)
}

  

  for循环:

  for循环是三种循环中最复杂的一种。

总结:
mov    mem/reg, xxx    ;赋初值
jmp    FOR_CMP    ;跳转到循环条件判定部分
FOR_STEP:
;修改循环变量Step
    mov    reg,Step
    add    reg,xxxx    ;修改循环变量的计算过程,在实际分析中,视算法不同而不同
    mov    Step,eax
FOR_CMP:    ;循环条件判定部分
    mov    ecx,dword ptr Step
;判定循环变量和循环终止条件StepEnd的关系,满足条件则退出for循环
    cmp    ecx,StepEnd
    jxx    FOR_END    ;条件成立则结束循环
......    ;循环体
jmp    FOR_STEP    ;向上跳转,修改流程回到步长计算部分
FOR_END:

  三种循环结构的比较:

  在执行效率上,do循环结构是最高的。因为do循环在结构上非常精简,利用了程序执行时由低地址到高地址的特点,只使用了一个条件跳转指令就完成了循环。

  for循环是执行速度最慢的,他需要三个跳转指令才能完成循环。

  while和for循环经常会优化成do循环。

  

原文地址:https://www.cnblogs.com/nww-570/p/8988347.html