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指令返回父过程
高级语言的机器码表示
https://blog.pipago360.site/posts/编译汇编链接二进制/高级语言的机器码表示/
作者
Ashenye
发布于
2024-08-10
许可协议
CC BY-NC-SA 4.0