1186 字
6 分钟
高级语言的机器码表示
2024-08-10
无标签
分支
三种情况
- if
auto cond = eval_cond
if (cond) {
...
}
auto cond = eval_cond
if (!cond) goto done
// then-stmts
done:
// done-stmts
- if-else
auto cond = eval_cond
if (cond) {
...
} else {
...
}
auto cond = eval_cond
if (!cond) goto else_label
// then-stmts
goto done
else_label:
//else-stmts
done:
// done-stmts
- if-else-if else分支是一个递归的if-else结构
auto cond = eval_cond
if (cond) {
...
} else [if (...) {
...
}] // 可能还有if
auto cond = eval_cond
if (!cond) goto else_label
// then-stmts
goto done
else_label:
//对于else分支的else语句的递归翻译得到语句块
done:
//done-stmts
一些细节
- 短路 对复杂条件表达式不是单单的求值过程,带控制流的
if (cond0 || cond1 || cond2) {
...
} // 这等效于
if(cond0) goto then
if(cond1) goto then
if(!cond2) goto done
then: //then-stmts
done:
//done-stmts
- 另外如果对条件表达式的求值是带副作用的话,条件表达式的短路语义可能带来一些不可意料的结果
条件传送
对于简单的分支语句可以用条件传送翻译
auto max = a > b ? a : b;
auto max = a;
if (b > a) max = b;
cmp rbx, rax
cmovlt rbx, rax
- 跟使用有条件跳转翻译分支语句相比,不会改变程序的控制流从而避免了分支预测失败的惩罚,
- 然而如果对于两个分支的求值有副作用或者会产生非法的行为会导致出乎意料的结果
int foo(long* p) {
return xp != NULL ? *xp : 0;
}
- 条件传送语句会计算两个分支的结果,如果有一个分支计算代价大超过了分支预测失败的乘法那么翻译成条件传送得不偿失,所以仅仅适合简单的分支语句
do-while循环
do {
// loop-body
} while(cond)
这等价于
loop:
//loop-body-stmts
auto cond = eval_cond
if(cond) goto loop
break coutinue
do {
// loop-body
continue
break
} while(cond)
loop:
//loop-body-stmts
goto test
goto done // never reached
test:
auto cond = eval_cond
if(cond) goto loop
done:
//done-stmts
细节
- 短路同上
while循环
while会被降解成do-while的形式然后被进一步翻译
while(eval_cond) {
...
}
跳转到中间
goto test
do {
// body
test: cond = eval_cond
} while(cond)
goto test
loop:
//body
test:
cond = eval_cond
if(cond) goto loop
guarded do
cond = eval_cond
if(!cond) goto done
do {
cond = eval_cond
}while(cond)
done:
...
cond = eval_cond
if(!cond) goto done
loop:
//body
cond = eval_cond
if(cond) goto loop
done:
...
利用这种实现策略,编译器常常可以优化初始的测试,例如认为测试条件总是满足。
for
for(init;test;update) {
...
}
降解为while语句
init;
while(test) {
...
update
}
switch
对于一个整数索引值进行多重分支的跳转,当开关情况数量比较多(例如 4个以上),并且值的范围跨度比较小,范围内的空洞不是很多,就会使用跳转表翻译switch语句。特点是跟if-else-if相比执行开关语句的时间与开关情况的数量无关。
void avitch_eg(long x, long n, long *deat) {
long val = x;
switch (n) {
case 100:
val *= 13;
break;
case 102:
val += 10;
/* Fall through */
case 103:
val += 11;
break;
case 104:
case 106:
val *= val;
break;
default:
val = 0;
}
*deat = val;
}
.text
.file "test.c"
.globl avitch_eg # -- Begin function avitch_eg
.p2align 4, 0x90
.type avitch_eg,@function
avitch_eg: # @avitch_eg
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rdx, -24(%rbp)
movq -8(%rbp), %rax
movq %rax, -32(%rbp)
movq -16(%rbp), %rax
addq $-100, %rax
movq %rax, -40(%rbp) # 8-byte Spill
subq $6, %rax
ja .LBB0_5
# %bb.7:
movq -40(%rbp), %rax # 8-byte Reload
leaq .LJTI0_0(%rip), %rcx
movslq (%rcx,%rax,4), %rax
addq %rcx, %rax
jmpq *%rax
.LBB0_1:
imulq $13, -32(%rbp), %rax
movq %rax, -32(%rbp)
jmp .LBB0_6
.LBB0_2:
movq -32(%rbp), %rax
addq $10, %rax
movq %rax, -32(%rbp)
.LBB0_3:
movq -32(%rbp), %rax
addq $11, %rax
movq %rax, -32(%rbp)
jmp .LBB0_6
.LBB0_4:
movq -32(%rbp), %rax
imulq -32(%rbp), %rax
movq %rax, -32(%rbp)
jmp .LBB0_6
.LBB0_5:
movq $0, -32(%rbp)
.LBB0_6:
movq -32(%rbp), %rcx
movq -24(%rbp), %rax
movq %rcx, (%rax)
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size avitch_eg, .Lfunc_end0-avitch_eg
.cfi_endproc
.section .rodata,"a",@progbits
.p2align 2, 0x0
.LJTI0_0:
.long .LBB0_1-.LJTI0_0
.long .LBB0_5-.LJTI0_0
.long .LBB0_2-.LJTI0_0
.long .LBB0_3-.LJTI0_0
.long .LBB0_4-.LJTI0_0
.long .LBB0_5-.LJTI0_0
.long .LBB0_4-.LJTI0_0
# -- End function
.ident "clang version 17.0.6"
.section ".note.GNU-stack","",@progbits
.addrsig
函数调用约定
函数调用约定是abi的一部分
调用方
- 保存上下文 保存caller-saved寄存器上下文,如果caller-saved寄存器caller调用后还会用到则保存在栈帧中
- 传递实参 把实参放在callee能够拿到的地方
- 正式调用 使用
call指令正式发起对被调用方的调用 - 获取返回值 按函数约定获取返回值
- 恢复上下文 恢复caller-saved寄存器上下文
被调用方
- 准备
- 保存上下文 保存callee-saved寄存器上下文,如果该寄存器在函数执行过程中用得上的话
- 分配栈帧
- 执行
- 获取实参 通过调用约定获取函数实参
- 正式执行 正式执行函数过程
- 传递返回值 按调用规定安放返回值
- 收尾
- 释放栈帧
- 恢复上下文 恢复callee-saved寄存器上下文
- 返回调用方 使用
ret指令返回父过程
