4.1 指令格式
指令,又称机器指令:
是指示计算机执行某种操作的命令,是计算机运行的最小功能单位
一台计算机的所有指令的集合构成该机的指令系统,也称为指令集。如x86架构、ARM架构
一台计算机只能执行自己指令系统中的指令,不能执行其他系统的指令
指令集:
一台计算机的所有指令的集合构成的指令系统,是指令集体系结构ISA中最核心的部分
指令集体系结构ISA:
完整定义了软件和硬件之间的接口,机器语言或汇编语言程序员必须熟悉的内容
ISA内容包括:
指令格式,指令寻址方式,操作类型,以及每种操作对应的操作数的相应规定
操作数的类型,操作数寻址方式,以及按大端方式还是按小段方式存放
程序可访问的寄存器编号、个数和位数,存储空间和大小和编址方式
指令执行过程的控制方式:包括程序计数器、条件码定义等
一条指令通常要包括操作码字段和地址码字段两部分
操作码(OP):指出指令中该指令应该执行什么性质的操作和具有何种功能。操作码是识别指令、了解指令功能与区分操作数地址内容的组成和使用方法等的关键信息
地址码(A):指明该操作针对的对象
一条指令可能包含0个、1个、2个、3个、4个地址码
n位地址码的直接寻址范围为\(2^n\),若指令总长度固定不变,地址码数量越多,寻址能力越差;同理操作码占用位数越多,地址码的寻址能力也越差
指令字长:一条指令的总长度,指令字长可能会变化,一般取字节的整数倍
区别于以下两个概念
机器字长:CPU进行一次整数运算所能处理的二进制数据的位数(通常和ALU直接相关)
存储字长:一个存储单元中的二进制代码位数(通常和MDR位数相同)
半字长指令、单字长指令、双字长指令:指令长度于机器字长相关
指令字长会影响取指令所需时间。如:机器字长=存储字长=16bit,则取一条双字长指令需要两次访存
4.1.1 指令分类
根据地址码数目不同,可以将指令分为以下几类
零地址
指令格式:OP
指令用途:
不需要操作数,如空操作、停机、关中断等指令
堆栈计算机,两个操作数隐含存放在栈顶和次栈顶,计算结果压回栈顶,同如数据结构中的后缀表达式
一地址指令
指令格式:OP + \(A_1\)
指令用途:
只需要单操作数,如加1、减1、取反、求补等操作
指令含义:OP(\(A_1\)) -> \(A_1\)
\(A_1\)指某个主存地址,类比于C语言的指针
(\(A_1\))表示\(A_1\)所指向的地址中的内容,类比于指针所指位置的内容
完成一条指令需要3次访存:取指 -> 读\(A_1\) -> 写\(A_1\)
需要两个操作数,但其中一个操作数隐含在某个寄存器,如隐含在ACC
指令含义:(ACC)OP(\(A_1\)) -> ACC
完成一条指令需要2次访存:取指 -> 读\(A_1\)
二地址指令
指令格式:OP + \(A_1\)(目的操作数)+ \(A_2\)(源操作数)
指令含义:(\(A_1\))OP(\(A_2\)) -> \(A_1\)
常用于需要两个操作数的算术运算、逻辑运算相关指令
完成一条指令需要访存4次,取指 -> 读\(A_1\) -> 读\(A_2\) -> 写\(A_1\)
三地址指令
指令格式:OP + \(A_1\) + \(A_2\) + \(A_3\)(结果)
指令含义:(\(A_1\))OP(\(A_2\)) -> \(A_3\)
常用于需要两个操作数的算术运算、逻辑运算相关指令,运算结果存入最后一个地址码
完成一条指令需要访存4次,取指 -> 读\(A_1\) -> 读\(A_2\) -> 写\(A_3\)
四地址指令
指令格式:OP + \(A_1\) + \(A_2\) + \(A_3\)(结果)+ \(A_4\)(下一条指令地址)
指令含义:(\(A_1\))OP(\(A_2\)) -> \(A_3\),(PC) -> \(A_4\)
\(A_4\)表示下一条将要执行指令的地址
正常情况下,取指令之后PC+1,指向下一条指令;四地址指令实际上完成了两次动作,执行运算指令后,将PC的值修改为\(A_4\)所指地址
根据指令长度不同,可以将指令分为以下几类
定长指令字结构:指令系统中所有指令的长度都相等
变长指令字结构:指令系统中各种指令的长度不等
根据操作码长度不同,可以将指令分为以下几类
定长操作码:指令系统中所有指令的操作码长度都相同
n位操作码对应\(2^n\)条指令
优点:定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利
缺点:灵活性差,指令数量增加时会占用更多固定位,留给表示操作数地址的位数受限
可变长操作码:指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上
常见的变长操作码方法是扩展操作码,使操作码的长度随地址码的减少而增加,不同地址数的指令可以具有不同长度的操作码,从而在满足需要的前提下,有效地缩短指令字长
优点:在指令字长有限的前提下仍保持比较丰富的指令种类
缺点:增加了指令译码和分析的难度,使控制器的设计复杂化
根据操作类型不同,可以将指令分为以下几类
数据传送:数据传送类指令,进行主存与CPU之间的数据传送
LOAD指令:把存储器(源)中的数据放到寄存器(目的)中
STORE指令:把寄存器(源)中的数据放到存储器(目的)中
算术逻辑操作:用于运算操作
算术:加、减、乘、除、增1、减1、求补、浮点运算、十进制运算
逻辑:与、或、非、异或、位操作、位测试、位清除、位求反
移位操作:特殊运算操作,包括算术移位、逻辑移位、循环移位(带进位和不带进位)
转移操作:程序控制类指令,改变程序执行的顺序
JMP:无条件转移指令
条件转移:
JZ:结果为0
JO:结果溢出
JC:结果有进位
调用和返回:CALL和RETURN
陷阱(Trap)与陷阱指令
输入输出操作:输入输出类(I/O)指令
CPU寄存器与IO端口之间的数据传送(端口即IO接口中的寄存器)
4.1.2 扩展操作码
扩展操作码:不同地址数的指令使用不同长度的操作码
定长指令字结构 + 可变长操作码
采用扩展操作码的设计目的是为了保持指令字长度不变的情况下,通过牺牲一部分寻址范围,增加指令数量
多地址指令牺牲寻址空间越多,单地址、零地址指令获得的数量则越多
假设指令字长为16位,每个地址码占4位
前4位为基本操作码字段OP,另有3个4位长的地址字段\(A_1\)、\(A_2\)和\(A_3\)
4位基本操作码若全部用于三地址指令,则说明至多可以表示16条不同的指令
但至少要将1111留作扩展操作码用,即三地址指令为15条
1111 1111留作扩展操作码之用,二地址指令为15条
1111 1111 1111留作扩展操作码之用,一地址指令为15条
零地址指令为16条
设计扩展操作码指令格式,因满足以下条件
不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码相同,类比于哈夫曼树前缀编码
各指令的操作码一定不能重复
通常情况下,对使用频率较高的指令,分配较短的操作码;
对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间
举例,设指令字长固定为16位,试设计一套指令系统满足:有15条三地址指令,有12条二地址指令,有62条一地址指令,有32条零地址指令
设地址长度位n,上一层留出m种状态,则下一层可扩展出m x \(2^n\)种状态
默认指令长度为4位,至多16条指令,三地址指令有15条,留出1条,因此三地址指令前4位范围为0000-1110
二地址指令至多1 x 16=16条指令,二地址指令有12条,留出4条,因此二地址指令前8位范围为1111 0000-1111 1011
一地址指令至多4 x 16=64条指令,一地址指令有62条,留出2条,因此一地址指令前12位范围为1111 1100 0000-1111 1110 1111、1111 1111 0000-1111 1111 1101
零地址至多2 x 16=32条地址,零地址指令有32条地址,全部使用,零地址指令地址范围为1111 1111 1110 0000-1111 1111 1111 1111
计算机识别指令过程:
先取指令前4位,如果不全为1,则说明是三地址指令
如果指令前4位全1,则取指令前6位,如果不全为1,则说明是二地址指令
如果指令前6位全1,则取指令前11位,如果不全为1,则说明是一地址指令
如果指令前11位全1,说明是零地址指令
4.2 指令寻址
指令寻址:下一条欲执行指令的地址,下一条执行的指令始终由程序计数器PC给出
4.2.1 顺序寻址
CPU每次从PC中读取指令后,PC会自行指向下一条指令,即(PC) + "1" -> PC
这里的"1"理解为1个指令字长,实际加的值会因指令长度、编址方式而不同
当指令字长=存储字长,主存也按照字编址,则(PC) + 1 -> PC
当指令长度不定或者与存储字长不等时,
读入一个字,根据操作码判断这条指令的总字节数n,修改PC的值
(PC) + n -> PC
根据指令的类型,CPU可能还要进行多次访存,每次读入一个字
4.2.2 跳跃寻址
由转移指令指出下一条执行的指令,传给PC,CPU再从PC中读取指令
每次取指令之后,PC一定会自动+1,指向下一条应该执行的指令
JMP为无条件转移指令,会直接把PC中的内容改成该指令的地址码,类似于C语言的goto语句
其中无条件、条件转移指令较多采用相对寻址
4.3 数据寻址
数据寻址:确定本条指令的地址码指明的真实地址
由于主存中有很多并发执行的程序,程序中指令的地址码不一定就是指向主存的真实地址,需要对该地址码进行解读
为了区分不同的寻址方式,将指令的地址码修改为寻址特征/寻址方式位 + 形式地址A
求出操作数的真实地址,称之为有效地址EA,Effective Address
特点:
设计多种不同的寻址方式,目的是缩短指令平均字长,扩大寻址空间,提高编程的灵活性
但不同的寻址方式会让指令译码难度增加
数据寻址指令格式:
操作码OP
寻址特征码(指明寻址方式)
地址码A
如果操作码位数固定n位,则指令数量为2\(^n\)条
寻址特征码与该机器支持的寻址方式数量有关,假设有n种不同的寻址方式,则需要\(\lceil log_2n\rceil\)位寻址特征码,向上取整
数据寻址方式对比汇总,假设指令地址码为A:
寻址方式
有效地址
访存次数
隐含寻址
程序指定特定的存储体
0
立即寻址
A就是操作数
0
直接寻址
EA = A
1
一次间接寻址
EA = (A)
2
寄存器寻址
EA = R\(_i\)
0
寄存器间接一次寻址
EA = (R\(_i\))
1
相对寻址
EA = (PC) + A
1
基址寻址
EA = (BR) + A
1
变址寻址
EA = (IX) + A
1
4.3.1 直接寻址
直接寻址:指令字中的形式地址A就是操作数的真实地址EA,即EA=A
优点:简单,指令执行阶段仅访问一次主存,不需专门计算操作数的地址
缺点:A的位数决定了该指令操作数的寻址范围,操作数的地址不易修改
直接寻址指令执行过程:
取指令,访存1次
执行指令,访存1次
暂不考虑存结果,共访存2次
4.3.2 间接寻址
间接寻址:指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址
所在的存储单元的地址,也就是操作数地址的地址,即EA=(A)
间接寻址可进行一次间接寻址,多次间接寻址
优点:可扩大寻址范围,有效地址EA的位数大于形式地址A的位数;便于编制程序,用间接寻址可以方便地完成子程序返回
缺点:指令在执行阶段要多次访存,一次间址需两次访存,多次寻址需根据存储字的最高位确定几次访存
一次间接寻址执行执行过程:
取指令,访存1次
执行指令,访存2次
暂不考虑存结果,共访存3次
4.3.3 寄存器寻址
寄存器寻址:在指令字中直接给出操作数所在的寄存器编号,即EA =\(R_i\),其操作数在由\(R_i\)所指的寄存器内
缺点:寄存器价格昂贵,计算机中寄存器个数有限
优点:
指令在执行阶段不访问主存,只访问寄存器,指令字短且执行速度快,支持向量/矩阵运算
但是正是因为寄存器的数量较少,所以使用寄存器寻址是,可以缩短地址码字段的长度,从而加快指令的执行效率
寄存器寻址执行执行过程:
取指令,访存1次
执行指令,由于存放在寄存器中,不需要访存,访存0次
暂不考虑存结果,共访存1次
4.3.4 寄存器间接寻址
寄存器间接寻址:寄存器\(R_i\)中给出的不是一个操作数,而是操作数所在主存单元的地址,即EA=(\(R_i\))
指的寄存器内
特点:与一般间接寻址相比速度更快,但因为操作数在主存中,指令的执行阶段需要访问主存
寄存器间接寻址指令执行过程:
取指令,访存1次
执行指令,访存1次
暂不考虑存结果,共访存2次
4.3.5 隐含寻址
隐含寻址:不是明显地给出操作数的地址,而是在指令中隐含着操作数的地址
一般默认将操作数存在在固定的某个存储器或寄存器中,如ACC寄存器、堆栈寄存器,大多指ACC寄存器
优点:有利于缩短指令字长,且极大的简化了指令的地址结构
缺点:需增加存储操作数或隐含地址的硬件,且灵活性很差
4.3.6 立即寻址
立即寻址:形式地址A就是操作数本身,又称为立即数,一般采用补码形式
一般立即寻址使用#标识
优点:指令执行阶段不访问主存,指令执行时间最短
缺点:A的位数限制了立即数的范围。
如A的位数为n,且立即数采用补码时,可表示的数据范围为−2\(^{n-1}\)~2\(^{n-1}\)-1
立即寻址指令执行过程:
取指令,访存1次
执行指令,由于操作数直接在操作码中,不需要访存,访存0次
暂不考虑存结果,共访存1次
4.3.7 偏移寻址
以某个地址作为起点形式地址视为"偏移量",根据偏移的"起点"不一样,分为下面几种
基址寻址
以程序的起始存放地址作为"起点",将CPU中基址寄存器(专用的寄存器BR,Base Address Register)的内容加上指令格式中的形式地址A,而形成操作数的有效地址,即EA=(BR)+A
操作系统中的"重定位寄存器"就是"基址寄存器"
除了使用基址寄存器BR以外,还可以使用通用寄存器进行基址寻址
在指令形式地址A之前中指明\(R_0\),要将哪个通用寄存器作为基址寄存器使用,即指令格式为OP + 寻址特征 + \(R_0\) + A
\(R_0\)的位数由通用寄存器的个数决定,依次编号
基址寻址使用场景:
假设程序从地址100开始存放,形式地址A=5,那么该指令操作数的实际存放地址为100+5=105。基址寄存器会存放该程序的起始地址100,CPU在读取操作数时,会进行(BR) + A运算以获得实际存放地址
程序运行前,CPU将BR的值修改为该程序的起始地址,并存于操作系统PCB中
优点:
由于基址寄存器的位数大于形式地址A的位数,可扩大寻址范围
用户不必考虑自己的程序存于主存的哪一空间区域,故有利于多道程序设计,以及可用于编制浮动程序,整个程序在内存里边的浮动
基址寄存器
是面向操作系统的,其内容由操作系统或管理程序确定。在程序执行过程中,基址寄存器的内容不变,作为基地址;形式地址可变,作为偏移量
当采用通用寄存器作为基址寄存器时,可由用户可使用汇编语言来决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定
变址寻址
程序员自己决定从哪里作为"起点",有效地址EA等于指令字中的形式地址A与变址寄存器IX的内容相加之和,即EA= (IX)+A
其中IX可使用变址寄存器(专用的寄存器IX,Index Register),也可用通用寄存器作为变址寄存器
变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变,IX作为偏移量;形式地址A不变,作为基地址,但形式地址一般为与主存寻址空间相关,往往也会很大
变址寻址使用场景:
在数组处理过程中,可设定偏移量A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序
优点:在数组处理过程中,可设定A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序
基址、变址寻址对比
变址寻址:
有效地址 = 偏移量 + 变址寄存器
程序执行过程中,变址寄存器中内容可以发生变化,偏移量不可变
变址寻址面向用户,主要用于处理数组问题
基址寻址:
有效地址 = 偏移量 + 基址寄存器
程序执行过程中,基址寄存器中内容不可变,偏移量可以变化
基址寻址面向系统,主要用于为多道程序或数据分配空间
除了单纯的变址寻址和基址寻址,还有复合寻址:
先基址后变址寻址:EA=(IX)+(BR)+A
先变址后间址寻址:EA=((IX)+A)
先间址后变址寻址:EA=(A)+(IX)
实际应用中往往需要多种寻址方式复合使用,可理解为复合函数,拿上一次寻址的EA作为新的A进行寻址
相对寻址
以程序计数器PC所指地址作为"起点",把程序计数器PC的内容加上指令格式中的形式地址A而形成操作数的有效地址,即EA=(PC)+A,其中A是相对于PC所指地址的位移量,可正可负,补码表示
相对寻址提供的相对地址A实质上是以下一条指令在内存中首地址为基准位置的偏移量
取出当前指令后,PC = (PC) + "1" 指向下一条指令
这里的"1"表示,加一条指令的字数
假设某计算机按字节编址,当前指令存放地址EA=1000,若当前指令字长=2B,则PC+2;若当前指令字长=4B,则PC+4
因此取出当前指令后PC可能为1002 或者 1004
取完指令,执行指令进行相对寻址赋值给PC,PC = (PC) + A,假设A=20,则执行完指令后PC可能为1020 或者 1024
相对寻址使用场景:
随着代码越写越多,如果想挪动for循环的位置,由于PC默认只能无限增加一条指令,相对寻址支持负数补码作为地址的偏移量,可实现语句的回退
ACC加法指令的地址码,可采用"分段"方式解决,即程序段、数据段分开
优点:操作数的地址不是固定的,它随着PC值的变化而变化,并且与指令地址之间总是相差一个固定值,因此便于程序浮动,这里指的是一段代码在程序内部的浮动。相对寻址广泛应用于转移指令
程序浮动:执行中的程序在主存的位置不固定,由于进程调度发生变化
取出当前指令后,PC会指向下一条指令,相对寻址是相对于下一条指令的偏移
堆栈寻址
操作数存放在堆栈中,隐含使用堆栈指针(SP,Stack Pointer)作为操作数地址
堆栈是存储器(或专用寄存器组)中一块特定的按"后进先出LIFO"原则管理的存储区,该存储区中被读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)
堆栈指针(SP)存放指针长度由堆栈寄存器数量决定,假设堆栈指针长度为n,则至多有\(2^n\)个堆栈寄存器
堆栈可用于函数调用时保存当前函数的相关信息
堆栈可采用硬件实现的堆栈寄存器,也可在主存内申请一段连续空间作为软堆栈,但不可使用硬盘来实现堆栈,读取速度太慢
在指令执行期间,硬堆栈读写操作都在寄存器中完成,不需要访存;而软堆栈每次调用必须访存1次
前者读写速度更快,但成本更高;后者读写速度慢,但成本低,操作系统常常使用软堆栈实现
硬堆栈寻址过程:
假设SP指向栈顶元素,共有\(R_0\)-\(R_3\)几个堆栈寄存器,其中\(R_0\)为栈顶,\(R_3\)为栈底。记栈顶单元为M\(_{SP}\),对堆栈元素进行一次加法运算
执行POP ACC
(M\(_{SP}\)) -> ACC,堆栈寄存器\(R_0\)中的值0001B传送给了累加寄存器ACC
堆栈进行了POP出栈操作,堆栈指针移动,(SP) + 1 -> SP
执行POP X
(M\(_{SP}\)) -> X,堆栈寄存器\(R_1\)中的值1001B传入通用寄存器X
同上出栈,堆栈指针移动,(SP) + 1 -> SP
执行ADD Y,将两个数经过ALU进行相加存入通用寄存器Y,(ACC) + (X) -> Y
执行PUSH Y
由堆栈发生了PUSH入栈操作,堆栈指针移动,(SP) - 1 -> SP
(Y) -> M\(_{SP}\),通用寄存器Y中的运算结果1010B传入堆栈寄存器\(R_1\)
根据题目条件,栈顶是低地址方向还是高地址方向,适当调整语句。但是实际上SP到底+1还是-1不需要开发人员操心,均由硬件完成
4.4 高级语言与低级代码对应
关于低级语言大纲要求:
只需关注x86汇编语言,如考察其他汇编语言会有详细注释,所以不加说明默认为x86汇编语言
题目给出某段简单程序的C语言、汇编语言、机器语言表示。能结合C语言看懂汇编语言的关键语句(看懂常见指令、选择结构、循环结构、函数调用)
汇编语言、机器语言一一对应,要能结合汇编语言分析机器语言指令的格式、寻址方式
不会考察将C语言人工翻译为汇编语言或者机器语言
4.4.1 x86汇编语言基础
起源
intel公司在1978年生产了代号为8086的CPU,该CPU支持很多汇编语言指令
后续开发的CPU如80286、80386均兼容了8086所使用的指令,因此只要是支持这些CPU的就称为x86架构
一条指令由操作码和地址码组成
操作码决定了做什么样的操作,x86汇编语言需要对这些操作进行设计
地址码决定了数据在哪,并且数据可能存放在寄存器中、主存中,或者直接在指令中给出要操作的数
对于在寄存器中的数据,x86架构需要在指令中给出寄存器的名字清单
对于在主存中的数据,x86架构需要在指令中给出主存地址,以及指明读写的长度
对指令中的操作,x86需要定义指令中"立即数"概念
以mov指令为例子,
mov指令格式:mov 目的操作数d(destination),源操作数s(source)
mov指令功能:将源操作数s复制到目的操作数d所指的位置
mov eax,ebx:表示将寄存器ebx 的值复制到寄存器eax
mov eax,5:表示将立即数5 复制到寄存器eax
mov eax,dword ptr [af996h]:表示将内存地址af996H 所指的32bit值复制到寄存器eax
mov byte ptr [af996h],5:表示将立即数5 复制到内存地址af996H 所指的一字节中
在汇编指令中,直接给出常量,即"立即寻址",可用十进制表示、也可用十六进制(常以h结尾)
x86指令中指明内存的读写长度,假设字长为2B:
dword ptr:双字,32bit
word ptr:单字,16bit
byte ptr:字节,8bit
x86架构中常用的寄存器:
如果一个寄存器的名字以E,Extended开头,则该寄存器大小均为32bit
通用寄存器X:EAX、EBX、ECX、EDX。该类寄存器存储目的并未强制规定
变址寄存器I:ESI、EDI。变址寄存器可用于线性表、字符串的处理
I,index;S,Source;D,Destination
堆栈寄存器:堆栈基指针EBP,Base Pointer、堆栈顶指针ESP,Stack Pointer。堆栈寄存器用于实现函数调用
通用寄存器使用更加灵活,而变址寄存器和堆栈寄存器只能固定使用32bit
如果只需要使用低16bit,可直接使用AX、BX、CX、DX寄存器
还可以进一步拆分为AH、AL,BH、BL,CH、CL,DH、DL寄存器,分别表示高8bit,低8bit
程序计数器PC寄存器用于指向下一条即将执行的指令,在intel x86处理器中通常被称为IP,Instruction Pointer
其他mov执行例子:
mov eax,dword ptr [ebx]:表示将ebx 所指主存地址的32bit 复制到eax 寄存器中
相当于寄存器间接寻址,(ebx)寄存器中的内容作为主存地址进行寻址
mov eax,[ebx]:若未指明主存读写长度,默认读取32 bit,即相当于执行mov eax,dword ptr [ebx]
mov dword ptr [ebx],eax:表示将eax 的内容复制到ebx 所指主存地址的32bit
mov eax,byte ptr [ebx]:表示将ebx 所指的主存地址的8bit 复制到eax
mov [af996h],eax:表示将eax 的内容复制到af996h 所指的地址(未指明长度默认32bit)
mov eax,dword ptr [ebx+8]:表示将ebx+8 所指主存地址的32bit 复制到eax 寄存器中
[ebx+8]位置,表示将ebx寄存器中的内容偏移+8,再到主存中查询该地址的内容
mov eax,dword ptr [af996-12h]:表示将af996-12 所指主存地址的32bit 复制到eax 寄存器中
4.4.2 常用的x86汇编代码
算数运算指令,Intel格式:
加法add d,s:计算d+s,结果存入d
减法:sub d,s:计算d-s,结果存入d
乘法
mul d,s:无符号数d*s,乘积存入d
imul d,s:有符号数d*s,乘积存入d
除法:
div s:无符号数除法edx:eax/s,商存入eax,余数存入edx
idiv s:有符号数除法edx:eax/s,商存入eax,余数存入edx
取负数neg d:将d取负数,结果存入d
自增inc d:将d++,结果存入d
自减dec d:将d--,结果存入d
上述指令中的地址码中,目的操作数d 不可以是常量
王道教材中地址码采用
逻辑运算指令:
与and d,s:将d、s 逐位相与,结果放回d
或or d,s:将d、s 逐位相或,结果放回d
非not d:将d 逐位取反,结果放回d
异或xor d,s:将d、s 逐位异或,结果放回d
左移shl d,s:将d逻辑左移s位,结果放回d。通常s是常量
右移shr d,s:将d逻辑右移s位,结果放回d。通常s是常量
其余指令:
用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx
用于实现函数调用的指令:push、pop、call、ret
用于实现数据转移的指令:mov
4.4.3 AT&T格式与Intel格式对比
AT&T格式和Intel格式均是针对x86汇编语言的语法格式
AT&T格式由AT&T公司设计,是Unix、Linux的常用格式
Intel格式是Intel公司设计,是Windows的常用格式
差异方面
AT&T格式
Intel格式
目的操作数d、源操作数s
op s,d源操作数在左,目的操作数在右
op d,s源操作数在右,目的操作数在左
寄存器的表示
mov %ebx,%eax寄存器名之前必须加"%"
mov ebx,eax直接写寄存器名即可
立即数的表示
mov $985, %eax 立即数之前必须加"$"
mov eax, 985直接写数字即可
主存地址的表示
mov %eax , (af996h)用"小括号"
mov [af996h], eax用"中括号"
读写长度的表示
movb $5, (af996h) movw $5, (af996h)movl $5, (af996h)addb $4, (af996h)指令后加b、w、l分别表示读写长度为byte、word、dword
mov byte ptr [af996h], 5mov word ptr [af996h], 5mov dword ptr [af996h], 5add byte ptr [af996h], 4在主存地址前说明读写长度byte、word、dword
主存地址偏移量的表示
movl -8(%ebx), %eax偏移量(基址)movl 4(%ebx, %ecx, 32), %eax偏移量(基址,变址,比例因子)
mov eax, [ebx - 8] [基址+偏移量]mov eax, [ebx + ecx*32 + 4] [基址+变址 x 比例因子+偏移量]
注:关于主存地址偏移量的表示,偏移量(基址,变址,比例因子)中
变址可以类比于循环变量index,表示数组元素的第几个元素
比例因子其实可以类比于数组中每个元素所占的空间大小,即sizeof(ElemType)
偏移量就用于指明某个元素中的某个属性变量,如结构体中的某个变量
4.4.4 选择语句
程序指令默认是顺序执行的,但是选择语句/分支结构可能改变程序的执行流
无条件转移指令
jmp <地址>可以让PC无条件跳转到<地址>位置
<地址>可来自主存、寄存器,常数给出,也可以使用"标号"锚定
"标号"锚定的汇编语句特征为后缀右冒号,名字可以自定义
写汇编语言代码时,一般会以函数名作为"标号",标注该函数指令的起始地址,同时支持基址寻址
"标号"并非一条单独的指令,而是一个标记符,标记在指令上
条件转移指令
cmp a,b:比较a、b两个数,a、b可能来自寄存器/主存/常量
条件转移指令jxxx一般要和cmp指令一起使用
je <地址>:若a==b则跳转
jne <地址>:若a!=b则跳转
jg <地址>:若a>b则跳转
jge <地址>:若a>=b则跳转
jl <地址>:若a
jle <地址>:若a<=b则跳转
选择语句的机器级表示
高级语言代码
if(a>b){
c=a;
}
else{
c=b;
}
汇编语言代码
mov eax,7 #假设变量a=7,存入eax
mov ebx,6 #假设变量b=6,存入eax
cmp eax,ebx #比较变量a,b
jg NEXT #若a>b,转移到NEXT:
mov ecx,ebx #假设用ecx存储变量c,令c=b
jump END #无条件转移到END:
NEXT: #标号NEXT:
mov ecx,eax #假设用ecx存储变量c,令c=a
END: #标号END:
比较语句
高级语言的if条件语句中判断 if(a > b)
对应到汇编语句是使用cmp指令对a、b进行比较(如cmp a,b),实质上采用的是a-b
a-b的结果信息会记录在程序的状态字寄存器PSW中,PSW会保存上一次运算结果的标志位CF、ZF、SF、OF,根据PSW中的符号位进行条件判断,是否进行转移
intel把PSW称为"标志寄存器"
在汇编语言中,条件跳转指令有多种
je 2表示比较结果a = b时,跳转到2位置
jg 2表示比较结果a > b时,跳转到2位置
而汇编语言的无条件跳转指令jmp 2,不管PSW各种标志位,直接跳转到2位置
4.4.5 循环语句
循环语句例子
高级语言代码
int result=0;
for(int i=1;i<=100;i++){
result +=i;
}
/*
//while版本循环
int i=1;
int result=0;
while(i<=100){
result +=i;
i++;
}
*/
汇编语言代码
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: #跳出循环主体
条件转移指令实现循环,需要以下部分
循环前的初始化
是否直接跳出循环
循环主题
是否继续循环
除了使用条件转移执行实现循环外,x86架构还可以使用loop指令实现
高级语言代码
for(int i=500;i>0;i--){
//做某些处理
}
汇编语言代码
mov ecx,500 #用ecx作为循环计数器
Looptop: #循环的开始
#...
#做某些处理
#...
loop Looptop #ecx--,若ecx!=0,跳转到Looptop
在上述汇编代码中,循环计数器寄存器只能使用ecx,其余eax、ebx、edx不具备该功能
使用loop指令可能会让代码更清晰简洁,loop指令相当于执行了三条指令,自增/减、比较、条件跳转
loopnz指令:当ecx!=0 && ZF==0时,继续循环
loopz指令:当ecx!=0 && ZF==1时,继续循环
4.4.6 函数调用
函数发生调用时,需要将函数当前状态压入堆栈中,被压入的部分称之为函数的栈帧,Stack Frame
栈帧:保存函数大括号内定义的局部变量、保存函数调用相关的信息
当前正在执行的函数栈帧,位于栈顶
x86汇编语言的函数调用:
call <函数名>:函数调用指令
call指令的作用:
将IP旧值压栈保存,保存在函数的栈帧顶部
设置IP新值,无条件转移至被调用函数的第一条指令
ret:函数返回指令
ret指令的作用:从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器
函数调用过程:
函数调用者执行cal指令,返回地址压入栈顶、并跳转到被调用函数第一条指令
被调用者函数执行处理后,执行ret指令,从栈顶找到返回地址,出栈并恢复IP值
函数调用中访问栈帧
对于32位系统,会给进程分配4GB的虚拟地址空间
高地址1GB范围用于存放操作系统内核指令,低地址3GB范围开放给用户进程使用
用户区除了用户进程,还包括共享库存储映射区(如printf函数)、堆Heap(malloc分配的区域)、读/写数据区(全局变量、静态变量)、只读代码/数据(程序指令、制度数据)以及未使用区
因此用户栈Stack的栈底为高地址,栈顶为低地址
图示中也通常栈底在上,栈顶在下
在x86系统中,默认以4B为栈的操作单位,因此ebp栈底指针、esp栈顶指针均占用4B空间
堆栈帧内数据的访问,都是基于ebp、esp进行的,当函数发生调用和返回时,ebp、esp指针都会一同发生变化
esp、ebp堆栈指针允许使用sub加/add减法指令对其中的值进行修改
push、pop指令用于实现入栈、出栈操作,x86默认以4字节为单位
push a:先让esp-4,再将a压入栈中。其中a可以是立即数、寄存器、主存地址
pop b:让栈顶元素出栈并写入b,再让esp+4。其中b可以是寄存器、主存地址
push、pop访问栈帧数据示例:
push eax #将寄存器eax的值压栈
push 985 #将立即数985压栈
push [ebp+8] #将主存地址[ebp+8]里的数据压栈
pop eax #栈顶元素出栈,写入寄存器eax
pop [ebp+8] #栈顶元素出栈,写入主存地址[ebp+8]
除了使用push、pop访问栈帧数据外,还可以使用mov指令直接访问,并模拟push、pop过程
mov访问栈帧数据示例:
sub esp,12 #栈顶指针-12
mov [esp+8],eax #将eax的值复制到主存[esp+8]
mov [esp+4],958 #将985复制到主存[esp+4]
mov eax,[ebp+8] #将主存[ebp+8]的值复制到eax
mov [esp],eax #将eax的值复制到主存[esp]
add esp,8 #栈顶指针+8
函数调用中切换栈帧
函数调用时:
调用者函数会先将ebp压入堆栈,然后再将当前IP/PC压入堆栈,随后再移动堆栈指针
因此任何函数开头均有如下语句:
push ebp #保存上一层函数的栈帧基址,即ebp的旧值
mov ebp,esp #设置当前函数的栈帧基址,即ebp新值
这两条语句由于是固定执行的指令,可称为"例行处理"。同时也可简化成enter指令,这是一个零地址指令
函数返回时:
由于每个栈帧底部保存者上一层栈帧的基地址,被调用者返回原函数时,会想让栈顶esp和栈底指针ebp同时指向调用函数栈底,出栈将原函数栈底传给ebp,再将原函数执行位置返回给PC
因此任何函数结尾均有如下语句:
mov esp,ebp #让esp指向当前栈帧的底部
pop ebp #将esp所指元素出栈,写入寄存器ebp
同函数调用时,这两条也可简化成leave指令,这是一个零地址指令。这个处理是在函数执行ret指令前必要执行的指令
函数调用中传递参数和返回值
一个栈帧中需要包含内容:
栈底一定保存着上一个函数的栈帧基地址/栈底地址,即ebp旧值
栈顶一定保存着该函数的返回地址
如果一个函数未进行函数调用,则不需要存放返回地址,即当前函数栈帧不需要存放当前函数的返回地址
通常将局部变量几种存储在栈顶的底部区域
C语言局部变量定义在函数体越靠前,则越靠近栈顶
局部变量可通过栈底指针 ebp - n 进行访问
通常将调用参数集中存储在栈帧顶部区域
C语言调用变量在参数列表越靠前,则越靠近栈顶
用于调用参数在发生函数调用才会使用,且存储在栈底处。当发生函数调用,IP压栈,调用参数在被调用函数栈帧的高地址处,可使用栈底指针 ebp + n 进行访问
gcc编译器将每个栈帧大小设置为16B的整数倍(当前函数栈帧除外),因此栈帧中可能出现空闲未使用的区域
gcc编译器是Linux系统最常用的编译器
除了上述栈帧中包含的内容外,可能还会存放某些寄存器(如:eax、edx、ecx )的值入栈保存,防止中间结果被破坏
这部分数据不一定存在,如果这些寄存器值不是运算的中间结果,则可以不保存
4.5 CISC和RISC
指令系统有两种设计方向:
复杂指令集系统:CISC,Complex Instruction Set Computer
设计思路:一条指令完成一个复杂的基本功能
代表:x86架构,主要用于笔记本、台式机等
精简指令集系统:RISC,Reduced Instruction Set Computer
设计思路:一条指令完成一个基本"动作";多条指令组合完成一个复杂的基本功
代表:ARM架构,主要用于手机、平板等
比较方面
CISC
RISC
指令系统
复杂,庞大
简单,精简
指令数目
一般大于200条
一般小于100条
指令字长
不固定
定长
可访存指令
不加限制
只有Load/Store指令
各种指令执行时间
相差较大
绝大多数在一个周期内完成
各种指令使用频度
相差很大
都比较常用
通用寄存器数量
较少
多
目标代码
难以用优化编译生成高效的目标代码程序
采用优化的编译程序,生成代码较为高效
控制方式
绝大多数为微程序控制
绝大多数为组合逻辑控制
指令流水线
可以通过一定方式实现
必须实现
80-20规律:典型程序中80% 的语句仅仅使用处理机中20% 的指令
比如设计一套能实现整数、矩阵加/减/乘运算的指令:
CISC的思路:
除了提供整数的加减乘指令除之外,还提供矩阵的加法指令、矩阵的减法指令、矩阵的乘法指令
一条指令可以由一个专门的电路完成
有的复杂指令用纯硬件实现很困难,因此采用"存储程序"的设计思想,由一个比较通用的电路配合存储部件完成一条指令
RISC的思路:
只提供整数的加减乘指令
一条指令一个电路,电路设计相对简单,功耗更低"并行"、"流水线"
乘法指令需要进行访存,那么该指令一定是CISC的