CSAPP 第三章 汇编指令 - 循环 和 switch

CSAPP 第三章 汇编指令 - 循环 和 switch

控制语句除了条件分支就是循环, 今天看循环的操作, 以及比较特殊的分支语句, 就是switch. do-while循环 while循环 for循环 switch语句 汇编指令并没有直接的指令对应循环, 而是用条件测试和跳转组合起来实现循环的效果. 汇编代码主要基于两种基本的循环模式. 先来看两种基本

控制语句除了条件分支就是循环, 今天看循环的操作, 以及比较特殊的分支语句, 就是switch.
  1. do-while循环
  2. while循环
  3. for循环
  4. switch语句
汇编指令并没有直接的指令对应循环, 而是用条件测试和跳转组合起来实现循环的效果. 汇编代码主要基于两种基本的循环模式. 先来看两种基本循环模式的第一种:

do-while循环

do-while循环的基本模式是:
do {
    body-statement
} while(test-expr);
这个循环的效果是执行一次body之后, 测试test-expr条件表达式的值, 为真则继续循环. 靠近汇编的角度, 这种语句会被翻译成如下结构的语句:
loop:
    body-statement;
    t = test-expr;
    if(t)
        goto loop;
这种语句就很接近汇编了, 其中的关键点在于更新条件表达式的语句.

练习 3.22 阶乘的大小

  1. 32位 int 能表示的最大的n的阶乘, n 是多少? 32位int最大的正数是2147483647 , 直接运行程序列出来部分结果就可以知道 n = 12 还没溢出, 而13就溢出了, 因为最后两位不是00
        1 : 1
        2 : 2
        3 : 6
        4 : 24
        5 : 120
        6 : 720
        7 : 5040
        8 : 40320
        9 : 362880
        10 : 3628800
        11 : 39916800
        12 : 479001600
        13 : 1932053504
        14 : 1278945280
        15 : 2004310016
        16 : 2004189184
    
  2. 64位的long也类似的列出来:
        1 : 1
        2 : 2
        3 : 6
        4 : 24
        5 : 120
        6 : 720
        7 : 5040
        8 : 40320
        9 : 362880
        10 : 3628800
        11 : 39916800
        12 : 479001600
        13 : 6227020800
        14 : 87178291200
        15 : 1307674368000
        16 : 20922789888000
        17 : 355687428096000
        18 : 6402373705728000
        19 : 121645100408832000
        20 : 2432902008176640000
        21 : -4249290049419214848
    
    可见最大n = 20.

练习 3.23 已知C和汇编, 代码, x 一开始存放在%rdi中:

long dw_loop(long x){
    long y = x * x;
    long *p = &x;
    long n = 2 * x;
    do {
        x += y;
        (*p)++;
        n--;
    } while (n > 0);
    return x;
}
long dw_loop(long x)
dw_loop:
    movq   %rdi, %rax               把x复制到%rax中, 由于x是局部变量, 其实这句就相当于*p = &x, 操作x就是操作%rax寄存器
    movq   %rdi, %rcx               把x复制到%rcx中
    imulq  %rdi, %rcx               设置%rcx的值为 x * x, 所以%rcx中存放的是局部变量y = x * x
    leaq   (%rdi, %rdi), %rdx       %rdx的值为 x+x, 则n存放在%rdx中

  .L2:
    leaq   1(%rcx, %rax), %rax      让x = x + y + 1 ? 这是一条指令把两条活都干了, 先是 x = x + y , 然后 x = x + 1.
    subq   $1, %rdx                 n = n - 1
    testq  %rdx, %rdx               测试%rdx 也就是 n
    jg     .L2                      n>0 则跳转到 .L2
    rep;ret                         返回%rax中的值
通过分析可以知道, y 存放在 %rcx 中, n 存放在%rdx中. 编译器通过将x复制到 %rax中 来直接操作, 然后使用leaq 指令合并了两行代码的计算, 相比解引用指针效率要高很多.

普通while循环

do-while循环其实和汇编代码的对应非常直接.while循环就没有那么直接, while循环的一般模式是:
while(test-expr){
    body-statement
}
这个有两种翻译方法. 第一种与do-while很像, 但直接跳到测试条件的部分:
        //直接跳到测试语句
        goto test;
    //循环体, 执行完之后又到测试条件语句处
    loop:
        body-statement
    //测试条件, 满足就跳到循环体
    test:
        t = test-expr;
        if(t)
            goto loop;
这个本质上就是一个直接跳到测试条件的do-while循环.

练习3.24 根据汇编填写C程序

long loop_while(long a, long b)
a in %rdi, b in %rsi
loop_while:
    movl   $1, %eax            向%rax里放入1 long result = 1
    jmp    .L2                 跳转到L2

  .L3:
    leaq   (%rdi, %rsi), %rdx  long temp = x + y
    imulq  %rdx, %rax          result = result * temp
    addq   $1, %rdi            a++

  .L2:
    cmpq   %rsi, %rdi          比较a : b
    jl     .L3                 如果a < b, 跳转到 L3
    rep; ret                   返回%rax
根据汇编来补充C代码:
long loop_while(long a, long b){
    // 一开始设置result = 1
    long result = 1;

    //这个条件就是L2中测试条件, 只要符合就跳转到L3
    while(a < b){
        //循环体是L3做的事情
        result = result * (a + b);
        a = a + 1;
    }
    return result;
}
第二种翻译方法叫做 guarded-do 模式, 其实就是先进行判断条件分支, 不成立就跳过循环, 成立就进入循环. 这个模式是:
t = test-expr;

if(!t)
    goto done:

loop:
    body-statment
    if(t)
        goto loop:

done:
可以看到, 就相当于在do-while之前先判断一次, 就成了普通的while循环. 毕竟do-while和普通while就差了要不要先执行一次, 如果在do-while之前先执行一次, 那么就相当于是一个普通的while了.

练习 3.25 已知汇编命令, 补全C代码

参数 a 在 %rdi, b 在 %rsi 中.
loop_while2:
    testq   %rsi, %rsi      测试b
    jle     .L8             b<=0的时候跳转 L8
    movq    %rsi, %rax      long result = b

  .L7:
    imulq   %rdi, %rax      result = result * a
    subq    %rdi, %rsi      b = b - a
    testq   %rsi, %rsi      测试 b 后的值
    jg      .L7             如果大于0, 跳回L7, 即一直循环
    rep; ret                当 b-a<=0 的时候, 就返回%rax中的值

  .L8:
    movq    %rsi, %rax      result = b
    ret
这个逻辑实际上第一次测试B的时候, b<=0就直接 结束了, 所以第一次测试必须 b>0 才能进入循环. 在循环内部, 则是每次测试 b-=a之后的值,直到b-a<=0就结束循环:
long loop_while2(long a, long b){
    //无论直接跳到L8还是进入循环, %rax都要用到b的值
    long result = b;
    // 循环条件就是 L7中的测试条件, 即 %rsi 的值大于0
    while (b > 0) {
        //循环体的内容
        result = result * a;
        b = b - a;
    }
    return result;
}

练习 3.26 根据汇编写出C代码

// x in %rdi
long fun_a(unsigned long x)

fun_a:
    movl   $0, eax      这句相当于long val = 0
    jmp    .L5          无条件跳转, 而没有先测试条件, 说明编译器采用的是跳转到中间的方式

  .L6:
    xorq   %rdi, %rax   val = val ^ x
    shrq   %rdi         逻辑右移1位, 最高位补0

  .L5:
    testq  %rdi, %rdi   测试x
    jne    .L6          x 不是 0 的时候, 跳转L6, 所以L6是循环体
    andl   $1, %eax     此时%eax的值与1做与运算, 结果存在%eax中, 实际上的结果就是%eax的最低位
    ret                 返回%rax的值
根据分析写出的代码是:
long funa(unsigned long x){
    long val = 0;

    while( x!=0 ){
        val = val ^ x;
        x >>= 1;
    }

    val = val & 0x1;
    return val;
}
代码是写出来了, 但是半天没看明白意思. 看了答案才明白是计算奇偶性....

for循环

普通的for循环如下:
for(init-expr; test-expr; update-expr)
实际上可以翻译成如下的while循环:
init-expr:

while(test-expr){
    ......

    update-expr
}
可以看的其实就是一个初始化表达式, 之后加上一个while循环, 因此翻译成汇编的话, 就是在while循环之前设置一个初始化循环表达式. 其后就是while语句的翻译, 有着上边的两种不同翻译方法.

练习 3.27 把例子转换成guarded-do模式

long fact_for_while(long n){
    long i = 2;
    long result = 1;
    while (i <= n) {
        result *= i;
        i++;
    }
    return result;
}
这一段翻译成guarded-do的时候,首先就要测试条件表达式, 如果成功的话进入循环 ,然后在循环体执行完毕之后, 再测试表达式.
long fact_for_while_guarded_do(long n){
    long i = 2;
    long result = 1;

    if(i>n)
        goto done;

  loop:
    result *= i;
    i++;
    if(i<=n)
        goto loop;

  done:
    return result;
}

练习 3.28 根据汇编写出C代码

long fun_b(unsigned long x)
x in %rdi

fun_b:
    movl   $64, %edx        %edx寄存器中存入64, 就是0x40
    movl   $0, %eax         这句对应 long val = 0

  .L10:
    movq   %rdi, %rcx       long temp = x
    andl   $1, %ecx         temp = temp ^ 0x1   x的最低位
    addq   %rax, %rax       val = val * 2
    orq    %rcx, %rax       val = val | temp    最低位如果是1 ,就是真
    shrq   %rdi             逻辑右移1位, x>>=1
    subq   $1, %rdx         装入64的寄存器 减去 1
    jne    .L10             %rdx 不是 0 的情况下 继续循环, 是0的情况下终止循环
    rep; ret
根据汇编代码补齐得到的结果是:
long fun_b(unsigned long x){
    long val = 0;
    long i;

    for(i=64; i>0; i--){
        long temp = x;
        temp = temp ^ 0x1;
        val = val * 2;
        val = val | temp;
        x >>= 1;
    }
}
由于在之前的表达式, i 的初始值是确定的常数即64, 所以没有使用初始表达式. 手工演算了一下, 这个函数实际上是把x按位给反向排列了一下, 比如10011, 会变成11001. 函数的逻辑是最低一位拿过来, 然后乘以2, 再去加上移位后的最低一位,不断重复直到全部位移完.

练习3.29 带有continue语句的for

首先考虑一下continue语句的左右. continue语句的作用是直接跳到下一次循环的开头. 然而要注意的是, 对于汇编代码, 并不能使用continue直接跳过循环开始处的标号, 因为更新条件的语句也在汇编体内. 因此应当考虑将更新条件的语句放在循环体最后, 如果遇到continue语句就直接跳转到更新条件语句. 用goto语句来模拟题目中的循环, 应该写成如下:
long sum = 0;
long i;

i=0;

goto test;

loop-normal:
    sum +=i;
loop-continue:
    i++;
test:
    if(i < 10) {
        if(i&1){
            goto loop-continue;
        }
        goto loop-normal:
    }

done:
可见改写的方式, 就是把loop-normal的部分单独分离出去, 如果启动了continue, 就只进行更新i的操作, 如果未启动continue, 则执行不含continue的完整的循环体. CSAPP的答案其实是一样的,就是在循环体里单独分出一部分是update, 在循环体的最下边, 翻译成汇编基本和我的一样.

switch

swtich 语句可以用一连串if-else 语句实现, 但是这么做的话效率很低. 通常编译器是采取创建一个数组的方式, 其中每个元素是要跳转的地址, 然后根据测试条件直接按照索引找到地址进行跳转. 这样的好处是所用时间基本一致, 而不会进行大量的分支判断. 有意思的是, GCC扩展了C语言, 加入了&&运算符用来取得指向标号的指针, 然后组成一个指针数组. 这样就可以用测试的值运算后得到的值直接进行索引来跳转. 扩展后的C语句与汇编的对应关系就非常容易看出来了.先会创造一个标号指针数组:
static void *jt[7] = {&&loc_A, &&loc_def, &&loc_B, &&loc_C, &&loc_D, &&loc_def, &&loc_D};
然后将测试条件进行一定的转换, 使其结果能够当做数组指针, 不满足这些条件则跳转至default对应的标号:
if (index > 6){
    goto loc_def;
}

goto *jt[index]
每一个swtich语句的末尾的break对应的其实是一个goto done的语句. 当然也可以故意不写break或者将多种情况对应一个跳转地址. 跳转指定标号的指令 jmp *.L4(, %rsi, 8)的写法目前看上去有点奇怪, 要把%rsi乘以8, %rsi中保存着index的值. 看一下跳转表的结构:
//按照8的倍数进行分配地址
.L4:
    .quad    .L3
    .quad    .L8
    .quad    .L5
    .quad    .L6
    .quad    .L7
    ...
所以可以知道, index等于几, 就从L4的开头找几倍8的倍数的地址, 就相当于使用数组索引了. 其实就是再度跳转到对应的汇编代码位置向下执行.

练习题 3.30

void switch2(long x, long *dest)

x in %rdi

switch2:
    addq    $1, %rdi           x = x + 1, 由于会将索引控制在从0开始, 所以x的最小值是-1
    cmpq    $8, %rdi           比较x - 8, 则其实就是 x+1-8>0, 则原始的x最大标号是7
    ja      .L2                超过8, 跳转到L2, 可见L2相当于default
    jmp     *.L4(, %rdi, 8)    没有超过8, 进入跳转表

然后按顺序往下看, 对应关系如下:
.L4
    quad:   .L9    -1
    quad:   .L5    0
    quad:   .L6    1
    quad:   .L7    2
    quad:   .L2    3 注意, L2为default, 所以不存在此情况
    quad:   .L7    4
    quad:   .L8    5
    quad:   .L2    6 注意, 无此标号, 因为L2对应default
    quad:   .L5    7

练习题 3.31

根据汇编代码补全C 函数:
void switcher(long a, long b, long c, long *dest)
a in %rdi, b in %rsi, c in %rdx, dest in %rcx

switcher:
    cmpq    $7, %rdi        比较x和7 , 可见 a 最小 为 0, 最大为7
    ja      .L2             如果超过,就转到L2也就是default.
    jmp     *.L4(, $rdi, 8) 进入跳转表
然后就要仔细观察跳转表和语句的结构: 首先看到对应索引 1, 3, 6的L2是default, 所以case 中不会有1, 3, 6 三个值, 只剩下 0,2,4,5,7.然后查看L7的代码发现没有jmp, 说明是fallthrough ,则L7对应的case 5 就是第一个case. 之后标号剩下 0,2,4,7, 可以看到2和7都跳到L5, 所以中间的连续两个case是 2 和 7. 现在只剩下0和4.仔细观察可以发现 索引4的时候直接跳到结束, 只有可能是最后一个Case E的情况, 而看L3对应的0, 还在继续处理C, 因此是CASE B的对应.
void swticher(long a, long b, long c, long *dest){
    long val;
    switch(a){
        case 5:
            c = b ^ 0xF;

        case 0:
            val = c + 112;

        case 2:
        case 7:
            val = (b + c) << 2;
            break;

        case 4:
            val = a;
            break;

        default:
            val = b;
    }

    *dest = val;
}
感觉这题目不仅是考代码, 连逻辑推理也一并考了.... 循环和Switch搞定了, 后边是通过栈来传递数据, 感觉又要掉头发了.....
LICENSED UNDER CC BY-NC-SA 4.0
Comment