C语言控制流对应的汇编语句

时间:2022-04-19 00:59:00

最近在看《深入理解计算机系统》,发现汇编挺有趣。

1.条件分支:if语句

下面是一个简单的ifelse函数:

int absdiff(int x, int y)
{
    if (x < y)
        return y - x;
    else
        return x - y;
}

对这个程序使用如下命令,得到汇编程序,(注意-S选项大写,并且始终用-O1优化选项)

gcc -S ifelse.c -o ifelse.s –O1

可以看到gcc对改程序的翻译与书上略有不同:

pushl	%ebx
	.cfi_def_cfa_offset 8
	.cfi_offset 3, -8
	movl	8(%esp), %ecx
	movl	12(%esp), %edx
	movl	%edx, %eax
	subl	%ecx, %eax
	movl	%ecx, %ebx
	subl	%edx, %ebx
	cmpl	%edx, %ecx
	cmovge	%ebx, %eax
	popl	%ebx

gcc中,%ecx: x, %edx:y , %eax: y-x, %ebx: x-y. 比较x与y,若x>=y, %eax: x-y. 最终在%eax中存放result。

其中,cmovge使用了后面将要讲到的 条件传送指令,即先计算一个条件操作的两种结果,然后再根据条件是否满足而选取一个。它要求处理器类型在i686以上,在gcc中可以添加'-march=i686'来编译,但是ubuntu11.10的处理器类型就是i686的(使用uname –p查看),所以上面的编译直接得到采用条件传送指令的汇编代码。

使用条件传送并不总是能改进代码效率,对GCC来说,只有很容易计算时(如只有一条加法指令),它才使用条件传送指令。

【题外话】:

下面的语句产生条件传送的汇编代码:

int arith(int x){
    return x / 4;
}

使用-O1选项产生汇编代码如下:

	.cfi_startproc
	movl	4(%esp), %eax	//get x
	leal	3(%eax), %edx	//temp = x+3
	testl	%eax, %eax	
	cmovs	%edx, %eax	//if(x < 0) x = temp
	sarl	$2, %eax	// return x >> 2
	ret
	.cfi_endproc

可以看到,如果是负数,在算术右移时,要加上2^k-1=3的偏置。注意,这里加偏置的原因:一般来说,我们可以直接对补码进行右移操作表示2^k幂,但是真正的除法与补码右移还是有一定区别的:

真正除法一定是舍入到0,所以-2.5得到-2;补码右移则会向下舍入,所以-2.5会得到-3(因为它总是把低位丢弃)

所以,在做真正除法时会加上一个偏置值,(原来CS:APP第65页2.3.7节讲到了这个问题,哎,可惜跳过去了。。)

    int i = -9;
    cout << i/4 << endl;    //get -2
    cout << (i>>2) << endl;     //get -3

-9的右移过程如下:得到原码1001——转为补码0111——右移两位1101——转为原码0011,即得到-3。

-9+偏置3过程: -6原码 0110——转为补码1010——右移两位1110——转为原码0010,得到-2.

2.循环

2.1 do-while循环的翻译

汇编中的循环使用 条件测试和跳转 组合起来实现。大部分编译器根据do-while形式产生循环代码,如下求阶乘的循环代码:

int fact_do(int n)
{
    int result = 1;
    do{
        result *= n;
        n = n-1;
    }while(n>1);
    return result;
}

产生汇编如下:

	.cfi_startproc
	movl	4(%esp), %edx   //get n
	movl	$1, %eax        //set result=1
.L2:
	imull	%edx, %eax      // result *= n
	subl	$1, %edx        //n--
	cmpl	$1, %edx        //compare n-1
	jg	.L2             //if(n>1): goto .L2
	rep
	ret
	.cfi_endproc

2.2 for循环的翻译

// Step1: for循环语句
for(init-expr; test-expr; update-expr)
	body-statement;

// Step2: while循环语句
init-expr;
while(test-expr){
	body-statement;
	update-expr;
}

// Step3: do-while循环语句
init-expr;
if(!test-expr)
	goto done
do{
	body-statement;
	update-expr;
}while(test-expr);
done:

// Step4: goto语句(直观的展示了汇编代码实现)
init-expr;
if(!test-expr)
	goto done
loop:
	body-statement;
	update-expr;
	if(test-expr)
		goto loop;
done:

带continue语句时的特例(练习3.24):

i = 0;
while(i < 10){
	if(i&1)
		continue;	//continue在i++之前,阻止了i的更新
	sum += i;
	i++;
}

i = 0;
if(i >= 10)
	goto done
do{
	if(i&1)
		continue;	//continue在i++之前,阻止了i的更新
	sum += i;
	i++;
}while(i < 10);
done:

do-while循环的continue语句还有一个问题要注意:
翻译为do-while循环时出现了问题,关键是continue的含义是不执行循环体内的内容,直接到达下一个循环点(也就是while处的判断,而不是“do{”处),所以下面语句只会输出1.

int i = 1;
do{
	printf("%d\n", i);
	i++;
	if(i<15)
		continue;
}while(0);

使用goto语句来保证while循环的更新(写代码时,直接在continue前加一个i++即可):

while(i < 10){
	if(i&1)
		goto next;
	sum += i;
next:
	i++;
}

3.switch语句

对switch的汇编,GCC会根据开关数量和稀少程度选择是否使用 跳转表 来翻译开关语句。跳转表是一个数组,表项i是代码短的地址,其执行时间与开关情况的数量无关。如下switch语句:

int switch_eg(int x, int n){
    int result = x;
    switch(n){
        case 100:
            result *= 13;
            break;
        case 102:
            result += 10;
        case 103:
            result += 11;
            break;
        case 104:
        case 106:
            result *= result;
            break;
        default:
            result = 0;
    }
    return result;
}

使用-O1翻译成汇编为:

	.cfi_startproc
	movl	4(%esp), %eax
	movl	8(%esp), %edx
	subl	$100, %edx
	cmpl	$6, %edx
	ja	.L8
	jmp	*.L7(,%edx,4)
	.section	.rodata
	.align 4
	.align 4
.L7:
	.long	.L3
	.long	.L8       //case 101: default
	.long	.L4
	.long	.L5
	.long	.L6
	.long	.L8       //case 105: default
	.long	.L6
	.text
.L3:                                     //case 100: result *= 13
	leal	(%eax,%eax,2), %edx   // get 3*x
	leal	(%eax,%edx,4), %eax   //get x+4*(3x)= 13*x
	ret
.L4:                                     //case 102: result += 10
	addl	$10, %eax
.L5:                                     //case 103: result += 11
	addl	$11, %eax
	ret
.L6:                                     //case 104/106: result *= result
	imull	%eax, %eax
	ret
.L8:                                     //default: result = 0
	movl	$0, %eax
	ret
	.cfi_endproc