控制语句除了条件分支就是循环, 今天看循环的操作, 以及比较特殊的分支语句, 就是switch.
- do-while循环
- while循环
- for循环
- 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 阶乘的大小
- 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
- 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搞定了, 后边是通过栈来传递数据, 感觉又要掉头发了.....