**指令(机器指令)**是指示计算机执行某种操作的命令。
指令系统:一台计算机的所有指令的集合构成该机的指令系统,也称指令集。指令系统是指令集体系结构(ISA)中最核心的部分,ISA完整定义了软件和硬件之间的接口,是机器语言或汇编语言程序员所应熟悉的。
ISA规定的内容主要包括:指令格式,数据类型及格式,操作数的存放方式,程序可访问的寄存器个数、位数和编号,存储空间的大小和编址方式,寻址方式,指令执行过程的控制方式等。
一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。一条指令通常包括操作码字段和地址码字段两部分:
操作码:操作码指出指令中该指令应该执行什么性质的操作以及具有何种功能。
地址码:地址码给出被操作的信息(指令或数据)的地址。包括参加运算的一个或多个操作数所在的地址、运算结果的保存地址、程序的转移地址、被调用的子程序的入口地址等。
指令的长度:指一条指令中所包含的二进制代码的位数。
定长指令字结构:在一个指令系统中,若所有指令的长度都是相等的,则称为定长指令字结构。
变长指令字结构:若各种指令的长度随指令功能而异,则称为变长指令字结构。然而,因为主存一般是按字节编址的,所以指令字长多为字节的整数倍。
根据指令中操作数地址码的数目的不同,可将指令分成以下几种格式。
零地址指令:只给出操作码OP,没有显式地址。
一地址指令
1)只有目的操作数的单操作数指令,按A1地址读取操作数,进行OP操作后,结果存回原地址。
2)隐含约定目的地址的双操作数指令,按指令地址A1可读取源操作数,指令可隐含约定另一个操作数由ACC(累加器)提供,运算结果也将存放在ACC中。
注:A1指某个主存地址,(A1)表示A1所指向的地址中的内容
二地址指令
三地址指令
四地址指令
n位地址码的直接寻址范围=2n,若指令总长度固定不变,则地址码数量越多,寻址能力越差
定长操作码指令在指令字的最高位部分分配固定的若干位(定长)表示操作码。
一般位操作码字段的指令系统最大能够表示2n条指令。定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利。当计算机字长为32位或更长时,这是常规用法。
为了在指令字长有限的前提下仍保持比较丰富的指令种类,可采取可变长度操作码,即全部指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上。显然,这将增加指令译码和分析的难度,使控制器的设计复杂化。
设计方案
1)不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码和同。
2)各指令的操作码一定不能重复。
通常情况下,对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。
设地址长度为n,上一层留出m种状态,下一层可扩展出m×2n种状态。
对比
设计指令系统时必须考虑应提供哪些操作类型,指令操作类型按功能可分为以下几种。
数据传送
算术和逻辑运算
移位操作
转移操作
无条件转移(JMP):无条件转移指令在任何情况下都执行转移操作
条件转移(JZ):结果为0;(JO):结果溢出;(JC):结果有进位
调用(CALL)和返回(RETURN)
调用指令和转移指令的区别:执行调用指令时必须保存下一条指令的地址(返回地址),当子程序执行结束时,根据返回地址返回到主程序继续执行;而转移指令则不返回执行。
陷阱(Trap)与陷阱指令
输入输出操作
寻址方式是指寻找指令或操作数有效地址的方式,即确定本条指令的数据地址及下一条待执行指令的地址的方法。寻址方式分为指令寻址和数据寻址两大类。
寻址方式分为指令寻址和数据寻址两大类。寻找下一条将要执行的指令地址称为指令寻址;寻找本条指令的数据地址称为数据寻址。
指令寻址
指令寻址方式有两种:一种是顺序寻址方式,另一种是跳跃寻址方式。
数据导址
数据寻址是指如何在指令中表示一个操作数的地址,如何用这种表示得到操作数或怎样计算出操作数的地址。
指令格式:
隐含寻址
不是明显地给出操作数的地址,而是在指令中隐含着操作数的地址。
例,单地址指令不指出第二操作数的地址,而规定累加器(ACC)作为第二操作数地址,指令格式明显指出的仅是第一操作数的地址。累加器(ACC)对单地址指令格式是隐含地址。
优点:有利于缩短指令字长。
缺点:需增加存储操作数或隐含地址的硬件。
立即(数)寻址
形式地址A就是操作数本身,又称为立即数,一般采用补码形式。图中#表示立即寻址特征。
执行:取指令访存1次,执行指令访存0次
优点:执行指令阶段不访问主存,执行指令时间最短
缺点:A的位数限制了立即寻址的范围。如A的位数为n,且立即数采用补码时,可表示-2n-1~2n-1-1
直接寻址
指令字中的形式地址 A是操作数的真实地址EA,即EA=A。
间接导址
指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址,即EA=(A)。
主存字第一位为1时,表示取出的不是操作数地址,即多次间接寻址
主存字第一位为0时,表示取得的是操作所的地址
寄存器寻址
在指令字中直接给出操作数所在的寄存器编号,即EA=Ri,其操作数在由Ri所指的寄存器内。
寄存器间接寻址
寄存器Ri中给出的不是一个操作数,而是操作数所在主存单元的地址,即EA=(Ri)
相对寻址
把程序计数器PC的内容加上指令格式中的形式地址A而形成操作数的有效地址,即EA=(PC)+A,其中A是相对于PC所指地址的位移量,可正可负,补码表示。
当CPU从存储器中取出一字节时,会自动执行(PC)+1→PC。
当前指令存放地址=1000,若当前指令字长=2B,则PC+2;若当前指令字长=4B,则PC+4。因此取出当前指令后PC可能为1002 or 1004
基址寻址
将CPU中基址寄存器(BR)的内容加上指令格式中的形式地址A,而形成操作数的有效地址,即EA=(BR)+A
注:基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定。在程序执行过程中,基址寄存器的内容不变(作为基地址),形式地址可变(作为偏移量)。
采用通用寄存器作为基址寄存器时,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。
变址寻址
有效地址EA等于指令字中的形式地址A与变址寄存器IX的内容相加之和,即EA=(IX)+A,其中Ix可为变址寄存器(专用),也可用通用寄存器作为变址寄存器
注:变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(IX作为偏移量),形式地址A不变(作为基地址)
基址寻址和变址寻址区别
基址寻址面向系统,主要用于为多道程序或数据分配存储空间,因此基址寄存器的内容通常由操作系统或管理程序确定,在程序的执行过程中其值不可变,而指令字中的A是可变的;
变址寻址立足于用户,主要用于处理数组问题,在变址寻址中,变址寄存器的内容由用户设定,在程序执行过程中其值可变,而指令字中的A是不可变的。
堆栈寻址
操作数存放在堆栈中,隐含使用堆栈指针(SP)作为操作数地址。
堆栈是存储器(或专用寄存器组)中一块特定的按“后进先出(LIFO)”原则管理的存储区,该存储区中被读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)。
寄存器堆栈又称硬堆栈。寄存器堆伐的成本较高,不适合做大容量的堆栈;而从主存中划出一段区域来做堆栈是最合算且最常用的方法,这种堆栈称为软堆栈。
堆栈可用于函数调用时保存咨前函数的相关信息,堆栈寻址大多数都是无操作数,因为操作数地址隐含使用SP,如POP和PUSH指令。
下面简单总结寻址方式、有效地址及访存次数(不含取本条指令的访存)
寻址方式 | 有效地址 | 访存次数 |
---|---|---|
隐含寻址 | 程序指定 | 0 |
立即寻址 | A即是操作数 | 0 |
直接寻址 | EA=A | 1 |
一次间接寻址 | EA=(A) | 2 |
寄存器寻址 | EA=Ri | 0 |
寄存器间接一次寻址 | EA=(Ri) | 1 |
相对寻址 | EA=(PC)+A | 1 |
基址寻址 | EA=(BR)+A | 1 |
变址寻址 | EA=(IX)+A | 1 |
相关寄存器
x86处理器中有8个32位的通用寄存器,为了向后兼容,EAX、EBX、ECX和EDX的高两位字节和低两位字节可以独立使用,E为Extended,表示32位。
除EBP和ESP外,其他几个寄存器的用途是比较任意的。
汇编指令格式
使用不同的编程工具开发程序时,用到的汇编程序也不同,一般有两种不同的汇编格式:AT&T格式和Intel格式。
AT&T格式的指令只能用小写字母,而Intel格式的指令对大小写不敏感
在AT&T格式中,第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然;在Intel格式中,第一个为目的操作数,第二个为源操作数,方向从右向左
在AT&T格式中,寄存器需要加前缀“%”,立即数需要加前缀“$”;在Intel格式中,寄存器和立即数都不需要加前缀。
在内存寻址方面,AT&T格式使用“(”和“)“、而tel格式使用“[”和“]”。
在处理复杂寻址方式时,例如AT&T格式的内存操作数“disp(base,idex,scae)”分别表示偏移量、基址寄存器、变址寄存器和比例因子,如“8(%edx,%eax,2)”表示操作数为M[R[ed]+R[eax]*2+8],其对应的Intel格式的操作数为“[edx+eax*2+8]”
在指定数据长度方面,AT&T格式指令操作码的后面紧跟一个字符,表明操作数大小,"b"表示byte(字节)、“w”表示word(字)或“l”表示long(双字)。Intel格式也有类似的语法,它在操作码后面显式地注明byte ptr、word ptr或dword ptr。
由于32或64位体系结构都是由16位扩展而来的,因此用word (字)表示16位
下表展示两种格式的几条不同指令。
常用指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令,下面以Intel格式为例,介绍一些重要的指令。以下用于操作数的标记分别表示寄存器、内存和常数。
数据传送指令
mov 指令
将第二个操作数 (寄存器的内容、内存中的内容或常数值) 复制到第一个操作数(寄存器或内存)。但不能用于直接从内存复制到内存。
其语法如下:
mov ,
mov ,
mov ,
mov ,
mov ,
举例:
mov eax,ebx #将ebx值复制到eax
mov byte ptr [var],5 #将5保存到var值指示的内存地址的一字节中
push指令
将操作数压入内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压入ESP指示的地址。
其语法如下:
push
push
push
举例(注意,栈中元素固定32位):
push eax #将eax值压栈
push [var] #将var值指示的内存地址的4字节值压栈
pop指令
与push指令相反,pop指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4。
其语法如下:
pop edi #弹出栈顶元素送到edi
pop [ebx] #弹出栈顶元素送到ebx值指示的内存地址的4字节中
常见算数运算指令
除法运算中,s为除数,被除数被提取安排到edx:eax中
add/sub指令:add指令将两个操作数相加,相加的结果保存到第一个操作数中。sub指令用于两个操作数相减,相减的结果保存到第一个操作数中。
其语法如下:
add , / sub ,
add , / sub ,
add , / sub ,
add , / sub ,
add , / sub ,
举例:
sub eax,10 #eax ← eax-10
add byte ptr [var],10 #10与var值指示的内存地址的一字节值相加,并将结果保存在原位置
inc/dec指令:inc、dec指令分别表示将操作数自加1、自减1。
其语法如下:
inc / dec
inc / dec
举例:
dec eax #eax值自减1
inc dword ptr [var] #var值指示的内存地址的4字节值自加1
imul指令。带符号整数乘法指令,有两种格式:
imul ,
imul ,
imul ,,
imul ,,
举例:
imul eax,[var] #eax ← eax * [var]
imul esi,edi,25 #esi ← edi * 25
idiv指令:带符号整数除法指令,它只有一个操作数,即除数,而被除数则为edx:eax中的内容(64位整数),操作结果有两部分:商和余数,商送到eax,余数则送到edx。
其语法如下:
idiv
idiv
举例:
idiv ebx
idiv dword ptr [var]
常见逻辑运算指令
and/or/xor指令。and、or、xor指令分别是逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。
语法如下:
and , / or , / xor ,
and , / or , / xor ,
and , / or , / xor ,
and , / or , / xor ,
and , / or , / xor ,
举例:
and eax,0fh #将eax中的前28位全部置为0,最后4位保持不变
xor edx,edx #置edx中的内容为0
not指令。位翻转指令,将操作数中的每一位翻转,即0→1、1→0。
语法如下:
not
not
举例:
not byte ptr [var] #将var值指示的内存地址的一字节的所有位翻转
neg指令。取负指令。
语法如下:
neg
neg
举例:
neg eax #eax ← -eax
shl/shr 指令。逻辑移位指令,shl为逻辑左移,shr 为逻辑右移,第一个操作数表示被操
作数,第二个操作数指示移位的位数。
语法如下:
shl , / shr ,
shl , / shr ,
shl , / shr ,
shl , / shr ,
举例:
shl eax,1 #将eax值左移1位
shr ebx,cl #将ebx值右移位(n为c1中的值)
控制流指令
x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。
通常用**标签(label)**指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。例如,
mov esi,[ebp+8]
Begin:
xor ecx,ecx
mov eax,[esi]
这样就用Begin指示了第二条指令,控制流指令通过标签就可以实现程序指的跳转。
jmp 指令。jmp 指令控制IP转移到label所指示的地址(从 label 中取出指令执行)。
语法如下:
jmp
举例:
jmp begin #转跳到begin标记的指令执行
jcondition指令。条件转移指令,依据CPU状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。根据条件码ZF和SF来实现转跳。
语法如下:
je
举例:
cmp eax, ebx #比较寄存器eax和ebx里的值
jg NEXT #若 eax> ebx,则跳转到 NEXT:
cmp/test指令。cmp指令用于比较两个操作数的值(同sub),test指令对两个操作数进行逐位与运算,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。
语法如下:
cmp , / test ,
cmp , / test ,
cmp , / test ,
cmp , / test ,
注意,不能同时将内存,常数为地址码
cmp
, / test ,
举例:
cmp dword ptr [var],10 #将var指示的主存地址的4字节内容,与10比较
jne loop #如果相等则继续顺序执行;否则跳转到10p处执行
test eax,eax #测试eax是否为零
jz xxxx #为零则置标志ZF为1,转跳到xxxx处执行
call/ret指令。分别用于实现子程序(过程、函数等)的调用及返回。
语法如下:
call
ret
call指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call指令保存调用之前的地址信息(当call指令结束后,返回调用之前的地址)。
ret指令实现子程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。cal和ret是程序(函数)调用中最关键的两条指令。
常见的选择结构语句有if-then、if-then-else、case(或switch)等。编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。
条件码(标志位)
除了整数寄存器,CPU还维护一组条件码(标志位)寄存器,它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令,最常用的条件码如下:
可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。
if语句
if-else 语句的通用形式如下:
if(a>b){
c=a;
}
else{
c=b;
}
对应汇编代码如下:
mov eax,7 #假设变量a=7,存入eax
mov ebx,6 #假设变量b=6,存入ebx
cmp eax,ebx #比较变量a和b
jg NEXT #若a>b,转移到NEXT:
move ecx,ebx #假设用ecx存储变量c,令c=b ——> else 部分的逻辑
jmp END #无条件转移到END:
NEXT:
mov ecx,eax #假设用ecx存储变量c,令c=a ——> if 部分的逻辑
END:
此时if和else的顺序与原代码相反,可以通过对跳转条件取反(jg→jle),使其顺序与原代码相同:
mov eax,7 #假设变量a=7,存入eax
mov ebx,6 #假设变量b=6,存入ebx
cmp eax,ebx #比较变量a和b
jle NEXT #若a≤b,转移到NEXT:
move ecx,eax #假设用ecx存储变量c,令c=a ——> if 部分的逻辑
jmp END #无条件转移到END:
NEXT:
mov ecx,ebx #假设用ecx存储变量c,令c=b ——> else 部分的逻辑
END:
常见的循环结构语句有while、for和do-while。汇编中没有相应的指令存在,可以用条件测试和转跳组合起来实现循环的效果,大多数编译器将这三种循环结构都转换为d0-while形式来产生机器代码。在循环结构中,通常使用条件转移指令来判断循环条件的结束。
for循环
int result = 0;
for(int i=1;i<=100;i++){
result+=i;
}//求1+2+3+...+100
while循环
int i = 1;
int result = 0;
while(i<=100){
result +=i;
i++;
}//求1+2+3+...+100
汇编指令翻译循环
mov eax,0 #用 eax保存 result,初值为0
mov edx,1 #用 edx保存 i,初始值为1
cmp edx,100 #比较 i和100
jg L2 #若i>100,转跳到 L2 执行
L1: #循环主体
add eax,edx #实现 result +=i
inc edx #inc 自增指令,实现 i++
cmp edx,100 #比较i和100
jle L1 #若 i<=100,转跳到 L1 执行
L2: #跳出循环主体
用条件转移指令实现循环,需要4个部分构成:
用loop指令实现循环
C语言形式:
for(int i=500;i>0;i--){
做某些处理
}//循环500轮
汇编形式
mov ecx 500 #用ecx作为循环计数器
Looptop: #循环的开始
...
做某些处理
...
loop Looptop #ecx--,若ecx!=0,跳转到Looptop
loop指令默认每轮对ecx进行减小操作,只能用ecx存轮次数。loop指令等价于:
dec ecx
cmp ecx,0
jne Looptop
使用loop指令可能会使代码更清晰简洁,与跳转区分开来。
补充:loopx指令一-如loopnz,loopz
loopz——当ecx!=0&&ZF==0时,继续循环
loopnz——当ecx!=0&&ZF==1时,继续循环
上面提到的call/ret指令主要用于过程调用,它们都属于一种无条件转移指令。
高级语言的函数调用
函数的栈帧(Stack Frame):保存函数大括号内定义的局部变量、保存函数调用相关的信息
假定过程P(调用者)调用过程Q(被调用者),过程调用的执行步骤如下:
步骤2是由call指令实现的,步骤6由ret指令返回到过程P。
如何访问栈帧
栈的位置:
切换栈帧
call指令
return指令
从函数的栈顶部找到IP旧值,将其出栈并恢复IP寄存器
enter指令(更新ebp)
等价于push ebp + move ebp,esp;保存上一层函数的栈帧基地址;0地址指令
leave指令(更新esp)
等价于mov esp,ebp + pop ebp;让esp指向当前栈帧底部
栈内内容
函数调用时栈的变化
设计思路:一条指令完成一个复杂的基本功能。
代表:x86架构,主要用于笔记本、台式机等
特点:
问题
80-20规律:典型程序中80%的语句仅仅使用处理机中20%的指令
类别 | CISC | RISC |
---|---|---|
指令系统 | 复杂,庞大 | 简单,精简 |
指令数目 | 一般大于200条 | 般小于100条 |
指令字长 | 不固定 | 定长 |
可访存指令 | 不加限制 | 只有Load/store指令 |
各种指令执行时间 | 相差较大 | 绝大多数在一个周期内完成 |
各种指令使用频度 | 相差很大 | 都比较常用 |
通用寄存器数量 | 较少 | 多 |
目标代码 | 难以用优化编译生成高效的目标代码程序 | 采用优化的编译程序,生成代码较为高效 |
控制方式 | 绝大多数为微程序控制 | 绝大多数为组合逻辑控制 |
指令流水线 | 可以通过一定方式实现 | 必须实现 |