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的汇编语言中,AL(always execute)为缺省的执行条件。
- 条件标志:位于CPU的某个通用寄存器中,例如8086的状态标志寄存器fr,ARM的程序状态寄存器cpsr。一般一个bit表示一种条件,成为xx条件标志位。只有当相应的条件标志位满足条件码规定的执行条件,该指令才会执行。
条件执行是指只有当某个特定条件满足时(条件标志位),指令才会执行。这个特征可以减少分支指令的数目,改善性能,提高代码密度。下面我们看看ARM,MIPS,X86的条件标志和条件执行的关系。
- 对于8086处理器,状态标志寄存器fr中,使用9位作为标志位。其中,又分为6个状态标志位和3个控制标志位。6个状态标志位就是我们的条件标志位。
- 对于ARM处理器,程序状态寄存器cpsr中,使用4位为条件标志位,从MSB开始,分别是负数(n),零(z),进位(c),溢出(v)。
- 对于MIPS处理器,没有条件标志位。所以,MIPS没有条件标志的条件执行。
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。