之前的笔记都是边听边写的,这次边听边玩塞尔达呢。。。笔记都是课后整理的,可能会有遗落,会尽力回想

switch语句

switch 语句是 if 语句的简写

switch 要求:

  1. case 后面必须是常量表达式
  2. case 后面常量表达式的值不能一样
  3. switch 后面表达式必须为整数

switch语句的反汇编

常见的有四种:

  • 分支比较少:if…else
  • 分支比较多且值连续:生成大表
  • 分支比较多,不连续且差值小:还是大表
  • 分支比较多,差值小且空缺多:生成大表+小表

接下来详细看一下每种情况

  1. 分支比较少:if…else

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 3:
    printf("3");
    break;
    default:
    printf("5");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(2);
    return 0;
    }

    当 case 的数量比较少时,switch 和 if…else 的逻辑类似,都是 cmp 然后 jcc,只是好在把比较全都放在一起了。但是仍然没有改变本质——还是只能一个一个去判断,两者效率差不多

  2. 分支比较多且值连续:生成大表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 3:
    printf("3");
    break;
    case 4:
    printf("4");
    break;
    default:
    printf("5");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(2);
    return 0;
    }

    大表是在编译的时候就生成好了的,可以直接在内存里面查到。这时再执行时,会先计算要查询的数值是表中的第几个,如果超出了表的大小就直接跳到 default,在表内的话就用表的首地址 + 第几个*4 去查询,非常之快!


    大表内存布局是严格按 case 常量值从小到大连续排列的,case 后面的常量可以是无序的,并不影响大表的生成。
    如果源码中写的 case 是乱序的,大表内部存储的目标指令地址就会是乱序的。因为编译后的语句的地址顺序对应着代码执行的顺序,而大表的索引又必须按数值大小来排列。例如这个样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 2:
    printf("2");
    break;
    case 4:
    printf("4");
    break;
    case 3:
    printf("3");
    break;
    case 1:
    printf("1");
    break;
    default:
    printf("erro"); //拼错了别管了!
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(3);
    return 0;
    }

  3. 分支比较多,不连续且差值小:还是大表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 5:
    printf("5");
    break;
    case 6:
    printf("6");
    break;
    case 7:
    printf("7");
    break;
    default:
    printf("error");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(6);
    return 0;
    }

    跟之前一样的逻辑,只是大表中空缺的部分会用 default 的地址来填充

  4. 分支比较多,差值小且空缺多:生成大表+小表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 9:
    printf("9");
    break;
    case 10:
    printf("10");
    break;
    case 12:
    printf("12");
    break;
    default:
    printf("error");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(6);
    return 0;
    }


    大表屁股后面紧接着一张小表,大表也与之前的有所不同,会在最后面跟着 default 的地址
    而重点就是紫色框内的那句 mov dl, byte ptr (004010e0)[eax],这句话的作用就是拿着 eax 中的值,假设是 n 吧,把 004010e0 这个地址 +n*1 的值存入 dl 中(edx 的低 8 位),我的左右括号大小怎么不一样
    所以小表里的一字节表示的就是第 n 位对应着大表里的第几个,比如例子中的 2 小表里是 1,那么大表对应的地址就是 0040106E,而 6 在小表里是 5,大表里就是 default 对应的 004010AA
    但是这个模式也是有条件的,如果 case 两侧涵盖的数量大于 255 个,就无法用小表了。因为小表中一个字节的最大值为 FF,所以最多只能表示 255 的偏移

do…while语句


总结:

  • 根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址
  • 根据条件跳转指令所在的地址,可以得到循环语句块的结束地址
  • 条件跳转的逻辑与源码相同

while语句


总结:

  • 根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址
  • 根据 jmp 指令所跳转到的地址,可以得到循环语句块的起始地址
  • 在还原 while 比较时,条件跳转的逻辑与源码相反

for语句


总结:

  • 第一个 jmp 指令之前为赋初值部分
  • 第一个 jmp 指令所跳转的地址为循环条件判定部分起始
  • 判断条件后面的跳转指令条件成立时跳转的循环体外面
  • 条件判断跳转指令所指向的地址上面有一个 jmp,jmp 地址为表达式 3 的起始位置

课后作业

  1. 写一个 switch 语句,不产生大表也不产生小表,贴出对应的反汇编

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 3:
    printf("3");
    break;
    default:
    printf("error");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(3);
    return 0;
    }

  2. 写一个 switch 语句,只生成大表,贴出对应的反汇编

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 3:
    printf("3");
    break;
    case 4:
    printf("4");
    break;
    case 5:
    printf("5");
    break;
    default:
    printf("error");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(3);
    return 0;
    }

  3. 写一个 switch 语句,生成大表和小表,贴出对应的反汇编

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include "stdafx.h"
    void function(int x) {
    switch(x) {
    case 1:
    printf("1");
    break;
    case 2:
    printf("2");
    break;
    case 9:
    printf("9");
    break;
    case 15:
    printf("15");
    break;
    case 20:
    printf("20");
    break;
    default:
    printf("error");
    break;
    }
    }
    int main(int argc, char* argv[]) {
    function(3);
    return 0;
    }

  4. 为 do…while 语句生成的反汇编填写注释

  5. 为 while 语句生成的反汇编填写注释

  6. 为 for 语句生成的反汇编填写注释