CPU体系架构-指令系统

在学习了处理器的寄存器,寻址方式之后,终于到了指令系统。这可谓千呼万唤始出来。为什么让人如此激动呢,因为指令系统,或者叫做汇编语言,终于和程序员有点关系了。如果一个程序员调试BUG的时候,需要反汇编成汇编语言才得以解决,那么这个BUG肯定可属于千年老妖,该程序员也是万年成仙,具有了抓妖降魔的本事。当然了,仙界也分级别,要看本事有多大,就得看修炼有多深。

我们这里所说的指令,就是指汇编语言。请注意区分在寻址方式中讲的机器指令。在这里呢,我们也不打算讲解每一种处理器(X86,MIPS,ARM)的指令集系统架构(ISA,Instructions Set Architecture),我会试着提取他们的共性,然后,将最容易混淆的地方拿出来。

什么是指令集体系架构(ISA)?ISA常被简称为Architecture(架构),是处理器的一个抽象描述,而ISA在处理器中的实现,被称为Microarchitecture(微架构)。ISA是将编程所需要了解的硬件信息从硬件系统中抽象出来,这样程序员就可以面向ISA编程。从编程人员的角度来看,ISA包括一套指令集和一些寄存器。参考《大话处理器》的第3章 指令集体系架构-处理器的外表。

汇编语言格式总结

跳转指令的命名

跳转指令又叫做分支指令。跳转指令能够迫使程序指针PC指向一个新的地址,改变程序顺序执行的流程。跳转指令使得子程序调用,if-then-else结构,循环结构等变为可能。

MIPS架构对跳转指令使用Motorola命名规则

PC相对跳转指令叫做branch;绝对地址跳转指令叫做jump。在汇编指令中,分别取英文单词的首字母b和j表示。

子程序的调用叫做jump and link或者branch and link。汇编指令以al结束,取and link两个单词的首字母。与普通的跳转的区别就是,子程序调用跳转会保存返回地址(return address)到寄存器ra中。

所有的PC相对跳转指令都是有条件的,例如需要比较两个寄存器。例如:

b label指令本质上是:beq $zero,$zero,off

j指令 :最大跳转范围是256M,28根地址线,高4位使用当前PC寄存器的值。单从这个过程,把j叫做绝对地址跳转其实很勉强。如果涉及到更大范围的跳转,需要j指令配合使用寄存器regs。例如:j r或者jr r。这也是唯一可以在整个4G地址空间内任意跳转的方法。

jal指令 :返回地址为当前PC+8,被保存到返回地址寄存器ra中。为什么返回地址是PC+8呢?涉及到MIPS特有的分支延迟槽的相关问题。

关于相对跳转和绝对跳转,字面上很容易理解,跳转之后的目的PC值一个是在当前PC上加一个偏移量,另一个是使用一个绝对的值(固定值)。下面扩展一点,在编写汇编代码的时候,如果代码需要编译成与地址无关的代码(PIC),则不能够使用jal label这样的指令。因为label地址在编译的时候就确定,该地址在跳转之前就放到了一个寄存器reg中,然后使用j reg来完成跳转。所以,如果要涉及到PIC的代码,需要使用相对跳转指令b。

条件标志&条件执行

条件执行严格的定义是指令可以根据状态位来决定是否执行。所以,条件跳转指令,MIPS下面的根据寄存器是否为零来执行某条指令等,严格意义上来说,并不属于条件执行的范围。

条件执行必须同时满足下面两个条件:

条件执行是指只有当某个特定条件满足时(条件标志位),指令才会执行。这个特征可以减少分支指令的数目,改善性能,提高代码密度。下面我们看看ARM,MIPS,X86的条件标志和条件执行的关系。

MIPS没有条件标志位,不能使用条件标志来实现条件执行,但是MIPS使用其他的方法去判断是否执行某条指令,例如,使用寄存器是否为zero来作为该指令执行的条件。MIPS中的条件move指令就是这样做的。又例如,MIPS的分支指令执行都是有条件的,这里的条件就是比较寄存器的值是否相同。MIPS中的这些举措,也就是为了弥补没有条件执行的缺陷。

ARM条件标志举例

绝大多数ARM的数据操作指令都支持条件执行功能。详见《ARM嵌入式系统开发-软件设计与优化》P76。下面就以经典的计算最大公约数(GCD,Greatest Common Divisor)的例子来说明条件执行指令的作用。

GCD算法的C语言实现:

int gcd(int a,int b)
{
	while(a != b)
	{
		if(a > b)
		a = a - b;
		else
		b = b - a;
	}
	return a;
}

在不使用条件执行时,汇编指令实现GCD算法:

gcd
	CMP r0,r1
	BEQ end
	BLT less
	SUBS r0,r0,r1
	B gcd
less
	SUBS r1,r1,r0
	B gcd
end

在使用条件执行是,汇编指令实现GCD算法:

gcd
	CMP r0,r1
	SUBGT r0,r0,r1
	SUBLE r1,r1,r0
	BNE gcd

在上面的算法中,SUBGT和SUBLE指令可以根据CMP指令产生的状态标志来决定是否执行,采用该类指令可以明显的降低代码的长度。

上面的汇编指令都是程序员根据处理器支持条件执行的特点对汇编代码进行优化得到。那么,如果有gcd的C语言函数,编译器能否也生成这样的代码,使用条件执行来达到优化呢。下面是对gcd的C语言函数的编译和反汇编结果。分别为x86,ARM,MIPS交叉编译。

gcd_x86.o:     文件格式 pe-i386


Disassembly of section .text:

00000000 <_gcd>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	8b 45 08             	mov    0x8(%ebp),%eax
   6:	3b 45 0c             	cmp    0xc(%ebp),%eax
   9:	74 1a                	je     25 <_gcd+0x25>
   b:	8b 45 08             	mov    0x8(%ebp),%eax
   e:	3b 45 0c             	cmp    0xc(%ebp),%eax
  11:	7e 08                	jle    1b <_gcd+0x1b>
  13:	8b 45 0c             	mov    0xc(%ebp),%eax
  16:	29 45 08             	sub    %eax,0x8(%ebp)
  19:	eb e8                	jmp    3 <_gcd+0x3>
  1b:	8b 55 08             	mov    0x8(%ebp),%edx
  1e:	8d 45 0c             	lea    0xc(%ebp),%eax
  21:	29 10                	sub    %edx,(%eax)
  23:	eb de                	jmp    3 <_gcd+0x3>
  25:	8b 45 08             	mov    0x8(%ebp),%eax
  28:	5d                   	pop    %ebp
  29:	c3                   	ret    
  2a:	90                   	nop
  2b:	90                   	nop
  
=====================分割线=================================

gcd_arm.o:     file format elf32-littlearm

Disassembly of section .text:

00000000 <gcd>:
   0:	e1a0c00d 	mov	r12, sp
   4:	e92dd800 	stmdb	sp!, {r11, r12, lr, pc}
   8:	e24cb004 	sub	r11, r12, #4	; 0x4
   c:	e24dd008 	sub	sp, sp, #8	; 0x8
  10:	e50b0010 	str	r0, [r11, -#16]
  14:	e50b1014 	str	r1, [r11, -#20]
  18:	e51b3010 	ldr	r3, [r11, -#16]
  1c:	e51b2014 	ldr	r2, [r11, -#20]
  20:	e1530002 	cmp	r3, r2
  24:	1a000009 	bne	2c <gcd+0x2c>
  28:	ea000017 	b	64 <gcd+0x64>
  2c:	e51b3010 	ldr	r3, [r11, -#16]
  30:	e51b2014 	ldr	r2, [r11, -#20]
  34:	e1530002 	cmp	r3, r2
  38:	da000012 	ble	50 <gcd+0x50>
  3c:	e51b3010 	ldr	r3, [r11, -#16]
  40:	e51b2014 	ldr	r2, [r11, -#20]
  44:	e0623003 	rsb	r3, r2, r3
  48:	e50b3010 	str	r3, [r11, -#16]
  4c:	ea000016 	b	60 <gcd+0x60>
  50:	e51b3014 	ldr	r3, [r11, -#20]
  54:	e51b2010 	ldr	r2, [r11, -#16]
  58:	e0623003 	rsb	r3, r2, r3
  5c:	e50b3014 	str	r3, [r11, -#20]
  60:	ea000004 	b	18 <gcd+0x18>
  64:	e51b3010 	ldr	r3, [r11, -#16]
  68:	e1a00003 	mov	r0, r3
  6c:	ea00001a 	b	70 <gcd+0x70>
  70:	e91ba800 	ldmdb	r11, {r11, sp, pc}


=====================分割线=================================

gcd_mips.o:     file format elf32-bigmips

Disassembly of section .text:

0000000000000000 <gcd>:
   0:	27bdfff8 	addiu	$sp,$sp,-8
   4:	afbe0000 	sw	$s8,0($sp)
   8:	03a0f021 	move	$s8,$sp
   c:	afc40008 	sw	$a0,8($s8)
  10:	afc5000c 	sw	$a1,12($s8)
  14:	8fc30008 	lw	$v1,8($s8)
  18:	8fc2000c 	lw	$v0,12($s8)
  1c:	00000000 	nop
  20:	14620003 	bne	$v1,$v0,30 <gcd+0x30>
  24:	00000000 	nop
  28:	0800001e 	j	78 <gcd+0x78>
  2c:	00000000 	nop
  30:	8fc20008 	lw	$v0,8($s8)
  34:	8fc3000c 	lw	$v1,12($s8)
  38:	00000000 	nop
  3c:	0062102a 	slt	$v0,$v1,$v0
  40:	10400007 	beqz	$v0,60 <gcd+0x60>
  44:	00000000 	nop
  48:	8fc20008 	lw	$v0,8($s8)
  4c:	8fc3000c 	lw	$v1,12($s8)
  50:	00000000 	nop
  54:	00431023 	subu	$v0,$v0,$v1
  58:	08000005 	j	14 <gcd+0x14>
  5c:	afc20008 	sw	$v0,8($s8)
  60:	8fc2000c 	lw	$v0,12($s8)
  64:	8fc30008 	lw	$v1,8($s8)
  68:	00000000 	nop
  6c:	00431023 	subu	$v0,$v0,$v1
  70:	08000005 	j	14 <gcd+0x14>
  74:	afc2000c 	sw	$v0,12($s8)
  78:	8fc20008 	lw	$v0,8($s8)
  7c:	03c0e821 	move	$sp,$s8
  80:	8fbe0000 	lw	$s8,0($sp)
  84:	03e00008 	jr	$ra
  88:	27bd0008 	addiu	$sp,$sp,8
  8c:	00000000 	nop

可以看到,由C语言编译之后,在ARM下面并没有使用条件执行,而且每一次比较之后,都会把变量放回到内存中。由此可见,在这里并没有多少优势存在。这是为什么呢,是不是在编辑的时候需要使用相关的优化选项?或者这个和编译器有关,这里使用的编译器太旧?这些问题现在都无法解决。

MIPS条件move举例

详见《See MIPS Run》P225。