汇编 —— 控制指令

写在前面:从腾讯实习回来之后,就感觉到自己的知识体系过于散乱。于是萌生了写一个自己的操作系统这样的心思,此为系列第一章,主要是讲解一些汇编知识的,内容大多从CSAPP中也可以获得。
本篇内容主要讲解汇编指令:跳转、条件以及循环指令

跳转指令

之前简单介绍了直线代码的指令情况。而jump指令可以改变一组机器代码指令的执行顺序,jump指令控制应该被传递到某个其他部分,可能是依赖于某个测试结果。而测试就是等会要介绍的条件指令,现在先讲跳转指令。

直接跳转指令

在汇编代码中,跳转目标用符号标号书写。其一般采用PC相对寻址,也就是将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差值作为编码。另外一种则是给出“绝对”地址,用4字节直接指定目标。

而其中最常用的就是jmp指令,它可以直接跳转,即跳转目标是作为指令的一部分编码的;也可以是间接跳转,即跳转目标是从寄存器或内存位置读出的,而间接跳转的写法是*后面跟一个操作指示符。例如:

jmp *%rax		//用寄存器%rax中的值作为跳转目标
jmp *(%rax)		//以%rax的值作为读地址,从内存中读出了跳转目标

下面是一个相对寻址的例子:

	movq %rdi, %rax
	jmp  .L2
.L3:
	sarq %rax
.L2:
	jmp   .L3
	rep; ret

其反汇编版本如下:

0:	48 89 f8	 mov  %rdi, %rax		//%rax = %rdi
3:  eb 03		 jmp  8 <loop+0x8>		//跳转到标号为 8 那一行,03+5 = 8
5:  48 d1 f8     sar  %rax				//%rax 右移三位
8:  7f f5        jmp  5<loop+0x5>		//跳转到标号为 5,f5+a --> -5+a = 5
a:  f3 c3        repz retq


标号 3 是标号 0 那一行有三个字节,所以 0+3 = 3

有条件跳转 if-else

刚刚说的都是无条件跳转,现在讲讲有条件跳转,也就是C语言中的if-else在汇编层面的实现。

条件跳转指令根据条件码(等下讲)的某种组合,或者跳转,或者继续执行序列代码的另一条指令。下表是一些常用的条件跳转指令:
在这里插入图片描述


我们对jle这个命令来解析,其实它就是jmp less equal,意味小于等于。这样就很好记忆了。

对于刚刚的例子来说:

	movq %rdi, %rax
	jmp  .L2
.L3:
	sarq %rax
.L2:
	testq %rax, %rax		//测试%rax	
	jg   .L3				//g是greater,意思是 %rax > 0时,跳转到L3
	rep; ret

跳转指令 练习题

在这里插入图片描述

练习题 答案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cbRaV0sc-1631876322318)(https://img- blog.csdnimg.cn/6b709d76aa884de7a8c8beab9720a611.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5rKh5pyJ5a-56LGh55qE6YeO5oyH6ZKIX2NvcHk=,size_20,color_FFFFFF,t_70,g_se,x_16)]

条件指令

条件码

除了整数寄存器之外,CPU还维护着一组单个位的条件码寄存器,他们描述了最近的算数或逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。常用的条件码有:

CF:进位标志	//最近的操作使最高位产生了进位,可用来检查无符号操作的溢出
ZF:零标志	//最近的操作得出的结果为 0
SF:符号标志	//最近的操作得到的结果为负数
OF:溢出标志	//最近的操作导致一个补码溢出(正溢出或负溢出)


其实计算机中并没有单个位的寄存器,其都被存储在flags这个寄存器中。这个寄存器在实模式下面是16位的,在32位保护模式下面其会被扩展,被称为eflags寄存器。

比如说,假如我们用ADD操作完成表达式 t = a + b,这里变量a、b、t 都是整型。然后,根据下面的 C表达式来设置条件码:

CF	(unsigned)t < (unsigned)a			//无符号溢出
ZF	(t == 0)							//零
SF  (t < 0)								//负数
OF  (a < 0 == b < 0)&&(t < 0 != a < 0)	//有符号溢出

eflags寄存器的布局

eflags作为flag的扩展,其必然是兼容flags的。下图是eflags中各个条件码的情况:
在这里插入图片描述

除了刚刚我们介绍的四种常用的条件码,以下几个也是值得了解到的:

  • PF:parity flag,即奇偶位
  • AF:auxiliary carry flag,即辅助进位标志,用于记录运算结果低4位的进、借情况,如果有进或借位,其为1
  • TF:trap flag,即单步工作标志位,此步若为1,表示CPU进入单步运行方式,这也是gdb实现调试的核心原理
  • IF:interrupt flag,即中断标志位。若其为1,表示中断开启,CPU可以响应外部可屏蔽中断;若为0,表示中断关闭,CPU不再相应中断,这就是中断实现的底层原理
  • DF:direction flag,即方向标志位。为字符串操作提供方向用
  • OF:overflow flag,即类型溢出位。用来表示计算的结果是否超过了数据类型可表示的范围。
  • IOPL:input output privilege level,即系统调用标志位,它用来标志当前CPU执行的特权级权限

条件码的设置

还记得之前汇编 —— 算术和逻辑操作中我们提到的那些指令吗?其中除了leaq指令之外,其他指令都会设置条件码!比如说:

对于逻辑操作,例如xor,进位标志和溢出标志会被置为0
对于移位操作,进位标志将设置为最后一个移出的位,溢出标志会被设置位0
对于inc、dec命令会设置溢出和零标志,但是不改变进位标志

当然除了这些命令之外,CMPTEST类的指令也会设置条件码,但是不改变其他寄存器
在这里插入图片描述


CMP指令根据两个操作数之差设置条件码,除了只设置条件码而不更新目的寄存器之外,CMP指令和SUB指令的行为是一样的
TEST指令的行为与AND指令一样,除了它们只设置条件码而不设置内存值,,而且TEST的比较对象是和 0 进行比较

条件码访问

条件码通常不能够直接读取,常用读取条件码的方法有三个:

  1. 根据条件码的某种组合,将一个字节设置为0或者1
  2. 可以根据条件跳转到程序的某个其他的部分
  3. 可以有条件地传送数据

对于第一种方式,常用的是set指令,一条set指令的目的操作数是低位单字节寄存器元素之一,或者一个字节的内存位置,指令会将这个字节设置位 0 或者 1 。
在这里插入图片描述
我们看一下下面这个例子:

//int comp(data_t a, data_t b)
//a in %rdi, b in %rsi
comp :
cmpq	%rsi,%rdi		//比较 a 和 b
setl	%al				// a < b 
movzbl 	%al,%eax		//结果零扩展传送至 %eax
ret 


这里我们看到这里比较小于用的是SF 和 OF异或^,这是怎么做到的呢?
SF 表示的是负数,OF表示的是符号溢出
那么当 SF = OF,那么异或的结果表示为 1,表示大于,为什么呢?
首先看第一种 SF = OF的情况:
SF = 0,OF = 0。表示 a-b >0 且结果没有溢出,那么很自然的
SF = 1,OF = 1。表示 a-b < 0且结果溢出了,在有溢出的情况下,小于的情况应该是大于
那么对于SF != OF的情况也很容易推断出它是表示小于的情况了

条件码 练习题

在这里插入图片描述
在这里插入图片描述

练习题答案

在这里插入图片描述
在这里插入图片描述

条件控制指令

将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。那么是怎么去实现的呢?
我们看一下以下的C代码:

long lt_cnt = 0;
long ge_cnt = 0;

long absdiff_se(long x,long y)
{
    long result;
    if(x<y)
    {
        lt_cnt++;
        result = y - x;
    }
    else
    {
        ge_cnt++;
        result = x - y;
    }
    return result;
}

下面是这个代码的反汇编版本:

//long absdiff_se(long x,long y)
//x in %rdi, y in %rsi
absdiff_se:
cmpq	%rsi, %rdi				//比较 x 和 y
jge		.L2						//如果 x>=y,跳转到 L2
addq	$1, lt_cnt(%rip)		//lt_cnt++
movq	%rsi, %rax				//%rax = y
subq	%rdi, %rax				//%rax = y - x
ret 							//return %rax
.L2:
addq	$1ge_cnt(%rip)		//ge_cnt++
movq	%rdi, %rax				//%rax = x
subq	%rsi, %rax				//%rax = x - y
ret								//return %rax 

条件控制指令 练习题

在这里插入图片描述
在这里插入图片描述

练习题答案

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

条件传送指令

实现条件操作的传统方法是通过是刚刚讲到的条件控制指令,另一种替代的策略则是使用数据的条件转移:这种方法计算一个条件操作的两种结果,然后再根据条件是否满足从中选择一个。
在这里插入图片描述

以以下的C代码为例:

long absdiff(long x, long y)
{
    long result;
    if (x < y)
    {
        result = y - x;
    }
    else
    {
        result = x - y;
    }
    return result;
}

其反汇编代码如下:

//long absdiff(long x, long y)
//x in %rdi, y in %rsi
absdiff :
movq	%rsi,%rax		//%rax = y
subq	%rdi,%rax		//%rax = %rax - x = y-x
movq	%rdi, %rdx		//%rdx = x
subq	%rsi,%rdx		//%rax = %rdx - y = x-y
cmpq	%rsi, %rdi		//比较 x y
cmovge	%rdx,%rax		//如果 x>=y,%rax = %rdx
ret						//return %rax

CPU流水线

也许有同学有疑惑了,这个指令不仅计算了y-x,还计算了x-y,而原来只需要计算两个中的一个,这样做有什么好处吗?

这是因为处理器一般会采用流水线来获得高性能。那么什么是流水线呢?
在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分。

这种方法通过重叠连续指令的步骤来获得高性能。例如,在获取一条指令的同时,执行它前面一条指令的算数运算。要做到这一点,要求能够实现确定要执行的指令序列,这样才能保持流水线中充满了待执行的指令。

那么这又和条件传送指令有什么关系呢?
在执行条件跳转的时候,只有当分支求值完成之后,才能够知道分支往哪儿走,处理器才能够去预读指令。一般来说,处理器会采用非常精密的分支预测逻辑来猜测每条跳转指令是否会执行,一般有90%以上的准确率,流水线中会充满待执行的指令。另一方面,错误预测的一个跳转要求处理器丢掉它为跳转该指令后所有指令已做的工作,然后在开始用从正确位置处起始的指令去填充流水线,一般来说,这会浪费大概15~30个CPU时钟周期,导致性能严重下降。

条件传送指令 练习题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

练习题答案

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

循环指令

C语言中主要提供了三种循环结垢:do-whilewhilefor。虽然汇编中是没有提供直接循环指令,但是可以用条件测试和跳转组合实现循环的效果。

do-while示例

比如说我们下面的示例,用do-while实现计算n!的计算:

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

其反汇编版本如下:

//n in %rdi
fact_do:
	movl	$1, %eax		//%rax = 1
.L2:
	imulq	%rdi, %rax		//%rax = %rax * n
	subq 	$1, %rdi		//n = n - 1
	cmpq	$1, %rdi 		//比较 1和 n
	jg		.L2				//如果n > 1
	rep; ret

练习题

在这里插入图片描述

练习题答案

在这里插入图片描述

while 循环

刚刚我们在do-while循环的之后,可以看到do-while基本是执行到末尾之后,再跳转到开头的L2去实现循环的。而while则是一种跳转到中间的方法,那么他是怎么跳转到中间的呢?

我们看一下下面这个例子:

long fact_while(long n)
{
    long result = 1;
    while(n > 1)
    {
        result *= n;
        n = n - 1;
    }
    return result;
}

其反汇编版本如下:

//long fact_while(long n)
//n in %rdi
fact_while:
	movl	$1, %eax		//%rax = 1
	jmp		.L5				//无条件跳转到L5
L6:
	imu1q	%rdi, %rax		//%rax = %rax * n
	subq	$1, %rdi		//%rdi = %rdi - 1 = n - 1
.L5:
	cmpq	$1, %rdi		//比较1 和 n
	jg		.L6				//如果 n > 1,跳转到 L6
	rep; ret

可以看到这里先到L5之后判断条件会跳转到中间标记L6

while 练习题

在这里插入图片描述

练习题答案

在这里插入图片描述

for 循环

本质上for循环和while并没有什么太大的不同,我们看一下具体的例子吧:

long fact_for_while(long n)
{
    long i = 2;
    long result = 1;
    while (i <= n)
    {
        result *= i;
        i++;
    }
    return result;
}

我们看一下反汇编版本:

// n in %rdi
fact_for:
	movl	$1, %eax		//%rax = 1
	movl	$2, %edx		//%rdx = 2
	jmp		.L8				//无条件跳转到L8
.L9:
	imulq	%rdx, %rax		//%rax = %rax * %rdx
	addq	$1, %rdx		//%rdx = %rdx + 1
.L8:
	cmpq	%rdi, %rdx		//比较 %rax 和 n
	jle		.L9				//如果 n <= %rax,转到L9
	rep;ret

for 练习题

在这里插入图片描述
在这里插入图片描述

练习题答案

在这里插入图片描述
在这里插入图片描述

switch指令

switch可以根据一个整数索引值进行多重分支,其通过关键数据结构跳转表实现。
跳转表是一个数组,表项 i 是一个代码段的地址,类似于虚函数表。


跟大量的if-else相比,跳转表的优点是执行开关语句的时间与开关数量无关,当开关情况数量比较多(4个以上),且值跨度比较小时就会使用跳转表。

我们现在看一个例子看一下switch的汇编实现:

void switch_eg(long x,long n,long *dest)
{
    long val = x;
    switch(n)
    {
        case 100:
            val *= 13;
            break;
        case 102:
            cal += 10;
        case 103:
            val += 11;
            break;
        case 104:
        case 106:
            val *= val;
            break;
        default:
            val = 0;
    }
    *dest = val;
}

其反汇编:

//void switch_eg(long x,long n,long *dest)
//x in %rdi, n in %rsi, dest in %rdx
switch_eg:
	subq	$100, %rsi				//%rsi = n - 100
	cmpq	$6, %rsi				//比较 %rsi和 6
	ja		.L8						//如果 %rsi<6,就转到 L8 
	jmp		*.L4(,%rsi,8) 			//绝对跳转到跳转表,根据 %rsi的值选择跳转	
.L3:
	leaq	(%rdi, %rdi,2), %rax	//%rax = 3x
	leaq	(%rdi, %rax,4), %rdi	//%rdi = 13x
	jmp		.L2						//无条件跳转到L2
.L5:
	addq	$10, %rdi				//%rdi += 10
.L6:
	addq	$11, %rdi				//%rdi += 11
	jmp		.L2						//无条件跳转到 L2
.L7 :
	imulq	%rdi, %rdi				//%rdi = %rdi*%rdi
	jmp		. L2					//无条件跳转到 L2
.L8:
	movl	$O, %edi				//%rdi = 0
L2:
	movq	%rdi, (%rdx)			//*dest = %rdi
	ret

可以看到其中jmp *.L4(,%rsi,8) 命令跳转到跳转表中了,那么这个表中的内容又是什么呢?

	section			.rodata
	.align 8				//8字节对齐
.L4:
	.quad		.L3			//当输入为 0时跳转到 L3
	.quad		.L8			//当输入为 1时跳转到 L8
	.quad		.L5
	.quad		.L6
	.quad		.L7
	.quad		.L8
	.quad		.L7

参考文献

[1] 深入理解计算机系统 第三章 程序的机器级表示
  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenmingik

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值