计组笔记第四章——指令系统

4.4.1 指令格式

  • 指令(又称机器指令):
    是指示计算机执行某种操作的命令,是计算机运行的最小功能单位。
    一台计算机的所有指令的集合构成该机的指令系统,也称为指令集。
    注:一台计算机只能执行自己指令系统的指令,不能执行其他指令系统的指令。如x86架构和ARM架构。

指令格式:

  • 一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。
  • 一条指令通常要包括操作码字段和地址码字段两个部分。
    操作码(op):指明用户要干什么。
    地址码(A):指明对谁进行操作。
  • 由于不同指令的操作不同,一条指令可能包含的地址码个数也不同,可能为0、1、2、3、4个地址码。根据地址码数目不同,可以将指令分为零地址指令、一地址指令、二地址指令……

零地址指令

  1. 不需要操作数,如空操作、停机、关中断等指令
  2. 堆栈计算机,两个操作数隐含存放在栈顶和次栈顶,计算结果压回栈顶。例如数据结构的“后缀表达式的计算”。

一地址指令

  1. 只需要单操作数,如加1、减1、取反、求补等
    指令含义:\(OP(A_1) \rightarrow A_1\)
    \(A_1\)进行操作之后得到的结果存回\(A_1\)的存储单元。
  2. 需要两个操作数,但其中一个操作数隐含在某个寄存器(如ACC)
    指令含义:\((ACC)OP(A_1) \rightarrow ACC\)
    完成一条指令需要2次访存,取指令和读\(A_1\)
    注:\(A_1\)是某个主存地址,\((A_1)\)表示\(A_1\)所指向的地址中的内容。

二、三地址指令

  • 二地址指令:常用于需要两个操作数的算术运算、逻辑运算相关指令
    指令含义:\((A_1)OP(A_2) \rightarrow A_1\)
    完成一条指令需要访存4次,取指、读\(A_1\)、读\(A_2\)、写\(A_1\)
  • 三地址指令:常用于需要两个操作数的算术运算、逻辑运算相关指令
    指令含义:\((A_1)OP(A_2) \rightarrow A_3\)
    最终运算结果存到\(A_3\)中。
    完成一条指令需要访存4次,取指、读\(A_1\)、读\(A_2\)、写\(A_3\)

四地址指令

  • 指令含义:\((A_1)OP(A_2) \rightarrow A_3\)\(A_4\) = 下一条将要执行指令的地址
    完成一条指令需要访存4次,取指、读\(A_1\)、读\(A_2\)、写\(A_3\)
  • 正常情况下:取指令之后PC+1,指向下一条指令
    四地址指令:执行指令后,将PC的值修改为\(A_4\)所指的地址。
  • 地址码的位数对指令的执行有什么影响吗?
    n位地址码的直接寻址范围 = \(2^n\)
    若指令总长度固定不变,则地址码数量越多,寻址能力越差。

指令分类

指令——按指令长度分类

  • 指令字长:一条指令的总长度(可能会变),半字长指令、单字长指令、双字长指令表明指令长度是机器字长的多少倍。
  • 机器字长:CPU进行一次整数运算所能处理的二进制数的位数(通常和ALU直接相关,是固定不变的)
  • 存储字长:一个存储单元中的二进制代价位数(通常和MDR位数相同,固定不变)
  • 如果某个计算机指令系统中所有指令的长度相等,则成为定长指令字结构
  • 如果某个计算机指令系统中所有指令的长度不相等,则成为变长指令字结构

指令——按操作码长度分类

  • 定长操作码:指令系统中所有指令的操作码长度都相同。如果操作码长度为n位,则最多表示\(2^n\)条指令。
    控制器的译码电路设计简单,但灵活性较低。
  • 可变长操作码:指令系统中各指令的操作码长度可变。
    控制器的译码电路设计复杂,但灵活性高。
  • 扩展操作码指令格式:
    定长指令字结构 + 可变长操作码

指令——按操作类型分类

  1. 数据传送:
    LOAD: 作用:把存储器中的数据放到寄存器中
    STORE: 作用:把寄存器中的数据放到存储器中
  2. 算术逻辑操作:
    算术:+、-、*、/、+1、-1、求补、浮点运算、十进制运算
    逻辑:&、|、!、异或、位操作、位测试、位清除、位求反
  3. 移位操作:
    算术移位、逻辑移位、循环移位(带进位和不带进位)
  4. 转移操作:
    无条件转移: JMP
    条件转移:JZ:结果为0时转移;JO:结果溢出时转移;JC结果有进位时转移
    调用和返回:CALL和RETURN
    陷阱(Trap)与陷阱指令
  5. 输入输出操作:
    CPU寄存器与IO端口之间的数据传送(端口即IO接口中的寄存器)
  • 上面的5条可被分为4类:
    数据传送类(进行主存与CPU直接的数据传送):1
    运算类:2、3
    程序控制类(改变程序的执行顺序):4
    输入输出类(进行CPU和I/O设备之间的数据传送):5

4.1.2 扩展操作码指令格式

扩展操作码

  • 扩展操作码举例:
    指令字长为16位,每个地址码占4位:
    前4位为基本操作码字段OP,另有3个4位长的地址字段:\(A_1\)\(A_2\)\(A_3\)
    4位基本操作码若全部用于3地址指令,则有16条。
    但至少须将1111留作扩展操作码之用,即三地址指令为15条:
    0000 \(A_1\) \(A_2\) \(A_3\)
    1110 \(A_1\) \(A_2\) \(A_3\)
    二地址指令可以有8位操作码,1111 1111留作扩展,其余二地址指令有15条:
    1111 0000 \(A_2\) \(A_3\)
    1111 1110 \(A_2\) \(A_3\)
    一地址指令可以有12位操作码,1111 1111 1111留作扩展,其余一地址指令有15条:
    1111 1111 0000 \(A_3\)
    1111 1111 1110 \(A_3\)
    零地址指令的后4位可以取全0到全1共十六种状态,零地址指令共16条:
    1111 1111 1111 0000 到
    1111 1111 1111 1111
  • 在设计扩展操作码指令格式时,必须注意以下两点:
    • 不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分代码相同。
    • 各指令的操作码不能重复。
  • 通常情况下,对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。
  • 根据上面的结论,设计另一种扩展操作码:
    设指令长度固定为16位,试设计一套指令系统满足:
    • 有15条三地址指令:
      0000 \(A_1\) \(A_2\) \(A_3\)
      1110 \(A_1\) \(A_2\) \(A_3\)
    • 有12条二地址指令:
      以1111开头
      1111 0000 \(A_1\) \(A_2\)
      1111 1011 \(A_1\) \(A_2\)
    • 有62条一地址指令:
      以1111 11开头
      1111 1100 0000 \(A_1\)
      1111 1111 1101 \(A_1\)
    • 有32条零地址指令:
      以1111 1111 111开头
      1111 1111 1110 0000 到
      1111 1111 1111 1111
  • 设地址长度为n,上一层留出m种状态,下一层可扩展出\(m \times 2^n\)种状态。
  • 扩展操作码的优点:在指令字长有限的前提下仍保持比较丰富的指令种类
  • 扩展操作码的缺点:增加了指令译码和分析的难度,使控制器的设计复杂化。

4.2.1 指令寻址

如何确定下一条指令的存放地址?

顺序寻址

一般情况下,下一条指令的地址:\((PC) + 1 \rightarrow PC\)
注: 按存储字长编址时(PC) + 1, 如果按字节编址,需要(PC) + 存储字长/8

跳跃寻址

由转移指令指出
如果这条指令时条约指令,根据指令的要求修改PC。如JMP指令类似C语言里的goto,它会把这条指令后的PC改为规定的值,之后直接跳转到对应指令。

4.2.2 数据寻址

数据寻址:确定本条指令的地址码指明的真实地址。
指令的地址码有很多种解读方式,有可能是逻辑地址、物理地址还可能是偏移量。所以给定的地址码到底该怎么解读呢?
数据寻址的方式有如下10种:

  • 给指令加上寻址特征字段,寻址特征字段负责说明后面的形式地址对应的寻址方式。寻址方式和寻址特征的对应关系如下图:
    知识总览
    通过形式地址求出的操作数的真实地址,称为有效地址(EA),形式地址用(A)表示。
    多地址指令每个地址之前都要加寻址特征。
  • 在后续的内容中,假设指令字长 = 机器字长 = 存储字长。
    假设操作数为3.

直接寻址

  • 指令字中的形式地址A就是操作数的真实地址EA,即EA = A。
  • 如:LOAD 寻址特征 A
    这条指令就是把主存地址为A的数据存到寄存器ACC中。
    这条指令的执行共需访存2次:
    取指令 访存一次;
    执行指令 访存一次。
  • 优点:简单,指令执行阶段只需访问一次主存,不需要专门计算操作数的地址。
    缺点:A的位数决定了该指令操作数的寻址范围不会很大。而且操作数地址不易修改。

间接寻址

  • 间接寻址:指令的地址字段给出的形式地址不是操作数的真正地址,二十操作数有效地址所在的存储单元的地址。也就是操作数地址的地址,即EA = (A)
  • 间接寻址示意图(一次间址):
    间接寻址示意图
  • 不考虑存指令执行结果的情况下,采用一次间址的间接寻址方式,一条指令的执行共访存3次。取指令访存1次,执行指令访存2次。
  • 间接寻址示意图(两次间址):
    两次间址示意图
  • 间接寻址的优点:可扩大寻址范围(有效地址的位数可能大于形式地址A的位数)
  • 便于编制程序(用间接寻址方式可以方便的完成子程序的返回。)
  • 缺点:指令在执行阶段要多次访存,指令的执行效率变低(一次间址需要两次访存,多次寻址需要根据存储字的最高位确定几次访存。)

寄存器寻址

  • 寄存器寻址:在指令字中直接给出操作数所在的寄存器编号,即\(EA = R_i\),其穿在身上在由\(R_i\)所指的寄存器内。
  • 一条指令的执行(不考虑)存结果,共访存1次:取指令访存1次,执行指令访存0次。
  • 优点:指令在执行阶段不访问主存,只访问寄存器,指令字短且执行速度快,支持向量和矩阵运算。
  • 缺点:寄存器价格昂贵,计算机中寄存器数量有限。

寄存器间接寻址

  • 寄存器间接寻址:寄存器\(R_i\)中给出的不是一个操作数,二十操作数所在主存单元的地址,即\(EA = (R_i)\)
  • 寄存器间接寻址示意图如下:
    寄存器间接寻址示意图
  • 不考虑存结果,一条指令的执行需要访存两次:取指令1次,执行指令访存1次。
  • 比一般的间接寻址速度更快,但指令的执行阶段需要访问主存(因为操作数在主存中)

隐含寻址

  • 隐含寻址:不是明显地给出操作数的地址,而是在指令中隐含着操作数的地址。
  • 隐含寻址可以将另一个操作数隐含在ACC中,以ADD指令为例,隐含寻址的示意图如下:
    隐含寻址的示意图
  • 优点:有利于缩短指令字长
  • 缺点:需要增加存储操作数或隐含地址的硬件。

立即寻址

  • 立即寻址:形式地址A就是操作数本身,又称为立即数,一般采用补码形式。立即寻址的寻址特征用#表示。
  • 不考虑存结果,一条指令的执行共访存1次:取指令1次,执行指令0次。
  • 优点:指令执行阶段不访问主存,指令执行时间最短。
  • 缺点:A的位数限制了立即数的范围。
    如A的位数为n,且立即数采用补码表示,可表示的数据范围为\(-2^{n-1}\)~\(2^{n-1}-1\)

相对寻址

  • 属于偏移寻址的一种
  • 相对寻址:有效地址是以程序计数器PC所指地址作为“起点”,加上偏移量A。A可正可负,补码表示。
  • EA = (PC) + A
  • 相对寻址的示意图如下:
    相对寻址的示意图
  • 注:相对寻址是基于下一条指令的偏移,因为每条指令取出之后PC都会+1,所以(PC) + A相当于在下一条指令的基础上偏移了A。
  • 优点:这段代码在程序内浮动时不用更改跳转指令的地址码。相对寻址广泛应用于转移指令。
  • 拓展:ACC加法指令的地址码可采用“分段”方式解决,即程序段和数据段分开。

基址寻址

  • 属于偏移寻址的一种
  • 基址寻址:有效地址是以程序的起始存放地址作为“起点” ,加上偏移量A。将CPU内部基址寄存器(BR,base address register)的内容+指令格式中的形式地址A,而形成操作数的有效地址。
  • EA = (BR) + A
  • 基址寻址示意图:
    基址寻址示意图
  • 当计算机内部没有专用的寄存器BR时,就会采用通用寄存器作为基址寄存器。
    这种情况下需要在指令中指明,要将哪个通用寄存器作为基址寄存器使用。示意图如下:
    基址寻址无专用寄存器
  • 程序运行前,CPU将BR的值修改为该程序的起始地址(存在操作系统PCB中)
  • 注:基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定。在程序执行过程中,基址寄存器的内容不变,形式地址可变。当某个通用寄存器被用户指定为基址寄存器后,其内容仍由操作系统确定,用户不可修改。
  • 优点:便于程序“浮动”,方便实现多道程序并发运行。可扩大寻址范围,用户不必考虑自己的程序存在主存的哪个位置。

变址寻址

  • 属于偏移寻址的一种
  • 变址寻址:有效地址是以程序员自己决定从哪里作为“起点”,加上偏移量A。有效地址EA等于指令字中的形式地址A与变址寄存器IX的内容之和,EA = (IX) + A,其中IX为变址寄存器(专用),也可用通用寄存器作为变址寄存器。
  • 注:基址寻址和变址寻址的区别是:变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变。变址寻址过程中,IX一般被视作偏移量,形式地址A不变(作为基地址)
  • 优点:在处理数组元素时,可设定A为数组首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任意数据的地址,特别适合编制循环程序。
  • 实际应用中往往需要多种寻址方式复合使用(可理解为复合函数)

堆栈寻址

  • 堆栈寻址:操作数存放在堆栈中,隐含使用堆栈指针(SP,stack pointer)作为操作数地址。
  • 堆栈是存储器(或专用寄存器组)中一块特定的按“后进先出(LIFO)”原则管理的存储区,该存储区中被读/写单元的堆栈是用一个特定的寄存器给出的,该寄存器称为堆栈指针。
  • 入栈/出栈时EA的确定方式不同。
    出栈时SP指向EA,入栈的时候先要把(SP)+1,之后的地址才是要写入的地址
  • 硬堆栈用寄存器实现,在指令执行期间不访存,软堆栈用主存实现,在指令执行期间访存1次。

扩展:硬件如何实现数的比较

  • 通过cmp指令,比较a和b,实际上就是做a-b
  • 相减的结果信息会记录在程序状态字寄存器(PSW)中
    PSW中有几个比特位记录上次运算的结果:
    • 进位/借位标志CF: 最高位有进位/借位时CF = 1
    • 零标识ZF:运算结果为0则ZF = 1, 否则ZF = 0.
    • 符号标志SF: 运算结果为负,SF = 1,否则为0
    • 溢出标志OF: 运算结果有溢出OF = 1, 否则为0.
  • 根据PSW的某几个标志位进行条件判断,来决定是否转移。

4.3.1 高级语言与机器代码之间的对应

机器语言和汇编语言都是机器级代码

考试要求

  1. 只需关注x86汇编语言;
  2. 题目若给出某段简单程序的C语言、汇编语言、机器语言表示。能结合C语言看懂汇编语言的关键语句(常见指令、选择结构、循环结构、函数调用)
  3. 汇编语言、机器语言一一对应,要能结合汇编语言分析机器语言指令的格式、寻址方式
  4. 不会考:将C语言人工翻译为汇编语言或机器语言。

汇编语言基础知识

x86汇编语言指令基础

  • 指令的作用:处理数据 和 改变程序执行流
  • 对于处理数据功能:
    指令格式 = 操作码 + 地址码
    操作码决定怎么处理数据
    地址码决定数据在哪(寄存器,主存或者指令里)
  • 以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所指的一字节中
  • 如何指明内存的读写长度?
    • dword ptr —— 双字,32bit
    • word ptr —— 单字,16bit
    • byte ptr —— 字节,8bit
  • x86架构CPU有哪些寄存器?
    x86寄存器以E开头都表示32位。如果把E去掉,表示使用寄存器的低16位。
    AX = AH + AL, BX = BH + BL ...
    AL是寄存器低8位,AH是寄存器9~16位。
    • 通用寄存器(X = 存放内容未知,E = Extended = 32bit):
      EAX EBX ECX EDX
    • 变址寄存器(I = Index, S = Source, D = Destination):
      ESI EDI
      用来处理线性表和字符串。
    • 堆栈基指针(Base Pointer):
      EBP
    • 堆栈顶指针(Stack Pointer):
      ESP
  • 更多例子:
mov eax, dword ptr[ebx] #将ebx所指主存地址的32bit复制到eax寄存器中
mov dword ptr[ebx], eax #将eax的内容复制到ebx所指主存地址的32bit
mov eax, byte ptr[ebx] #将ebx所指的主存地址的8bit复制到eax
mov eax, [ebx] #若未指明主存读写长度,默认32bit
mov [af996h], eax #将eax的内容复制到af996h所指的地址(未指明长度默认32bit)  
mov eax dword ptr[ebx+8] #将ebx+8所指主存地址的32bit复制到eax寄存器中
mov eax, dword ptr[af996h-12h] #将af996-12所指的主存地址的32bit复制到eax寄存器中

常用的x86汇编指令

算术运算指令

下表中的d是目的操作数,s是源操作数。指令的功能是两个操作数运算之后再存回目的操作数的地址里。d可能是寄存器或主存,s可以是立即数、主存、寄存器。

功能 汇编指令 英文 注释
add d, s add #计算d+s, 结果存入d
sub d, s subtract #计算d-s, 结果存入d
mul d, s / imul d, s multiply #无符号数d*s, 乘积存入d; #有符号数d*s, 乘积存入d
div s / idiv s divide 被除数默认放在edx和eax两个寄存器中。#无符号数除法edx:eax/s, 商存入eax, 余数存入edx; #有符号数除法
取负数 neg d negative #将d取负数,结果存入d
自增++ inc d increase #将d++, 结果存入d
自减-- dec d decrease #将d--, 结果存入d

常见逻辑运算指令

常见逻辑运算指令

其他指令

  • 用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx
  • 用于实现函数调用的指令:push、pop、call、ret
  • 用于实现数据转移的指令:mov

AT&T格式和Intel格式

AT&T格式通常在Unix、Linux中使用
Intel是Windows的常用格式。

  • AT&T格式的Intel格式的区别:
    AT&T格式的Intel格式的区别

选择语句的机器级表示

  • 无条件跳转指令 —— jmp
    由于不能确定每次程序要跳转到哪里,所以jmp后面跟的数字可以用“标号”代替。下面进行举例(第一段代码的行首数字是指令地址):
100 mov eax, 7
104 mov ebx, 6
108 jmp 116
112 mov ecx, ebx
116 mov ecx, eax

上面的代码可以用下面代码代替:

mov eax, 7
mov ebx, 6
jmp NEXT
mov ecx, ebx
NEXT:  
mov ecx, eax
  • 条件转移指令 —— jxxx
cpm a, b #比较a,b两个数

je <地址> #jump when equal, 若a==b则跳转
jne <地址> #jump when not equal, 若a!=b则跳转
jg <地址> #jump when greater than, 若a>b跳转
jge <地址> #jump when greater than or equal to, 若a>=b跳转
jl <地址> #jump when less than, 若a<b跳转
jle <地址> #jump when less than or equal to, 若a<=b跳转
  • 扩展:cmp指令的底层原理
    本质上是进行a-b运算,并生成标志位OF、ZF、CF、SF
    其中:
    OF: 溢出标志
    ZF:符号标志
    SF:零标志
    CF:进位/错位标志
    上面的条件转移指令的条件判断依据如下:
cpm a, b #比较a,b两个数

je <地址> #jump when equal, ZF==1?
jne <地址> #jump when not equal, ZF==0?
jg <地址> #jump when greater than, ZF==0 && SF==OF?
jge <地址> #jump when greater than or equal to, SF==OF?
jl <地址> #jump when less than, SF!=OF?
jle <地址> #jump when less than or equal to, SF!=OF || ZF==1?

循环语句的机器级表示

对于下面C语言代码:

int result = 0;
int i = 1;
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: #跳出循环主体
  • 总结:用条件转移指令实现循环,需要4个部分构成:
    • 循环前的初始化
    • 是否直接跳过循环?
    • 循环主体
    • 是否继续循环?
  • 还可以用loop指令实现循环,代码如下:
for(int i = 500; i > 0; i--) {
    做某些处理
}

对应汇编语言如下:

mov ecx, 500 #用ecx作为循环计数器
Looptop: #循环开始
...
做某些处理
...
loop Looptop #ecx--, 若ecx!=0, 跳转到Looptop

理论上,能用loop指令实现的功能一定能用条件转移指令实现
使用loop指令可能会使代码更清晰简洁

Call和ret指令

  • 函数调用的机器级表示
  • 以下面函数调用为例:
    在函数调用的过程中,系统会给函数分配一片内存区域,称为函数调用栈。栈底保存一些关于硬件和系统的信息,每一个程序都是从main函数进入的,所以在硬件和系统信息之上存放main的栈帧(保存函数大括号以内定义的局部变量、保存函数调用相关信息),main()中调用了P函数,所以main的栈帧上面有P的栈帧。P()中调用了Q(), 再把Q的栈帧压入函数调用栈,Q()执行结束后,Q的栈帧将从栈顶删除,P()又调用了caller函数,再把caller的栈帧压栈。caller函数大括号内定义了temp1、temp2和sum这三个变量,这些变量就存放在Caller的栈帧里。caller调用了add函数,add函数的栈帧压入栈顶。add函数执行完毕后出栈,当前执行的函数(caller)栈帧位于栈顶。后面逐渐执行,每个函数的栈帧依次出栈。
void main() {
    ...
    P();
    ...
    return;
}

void P() {
    ...
    Q();
    ...
    caller();
    ...
    return;
}

void Q() {
    ...
    return;
}

int caller() {
    int temp1 = 125;
    int temp2 = 80;
    int sum = add(temp1, temp2);
    return sum;
}

int add(int x, int y) {
    return x+y;
}
  • x86汇编语言的函数调用
    caller和add的函数汇编代码如下:
caller: 
push edp
mov ebp, esp
sub esp, 24
mov [ebp-12], 125
mov [ebp-8], 80
mov eax, [ebp-8]
mov [esp+4], eax
mov eax, [ebp-12]
mov esp, eax
call add
mov [ebp-4], eax
mov eax, [ebp-4]
leave
ret
add:
push ebp
mov ebp, esp
mov eax, [ebp+12]
mov edx, [ebp+8]
add eax, edx
leave 
ret

汇编语言中,会用call和ret指令实现函数的调用。
通常用函数名作为函数起始地址的标号。

  • call指令的作用:
    • 将IP(PC)旧值压栈保存(保存在函数的栈帧顶部)
    • 设置IP新值,无条件转移至被调用函数的第一条指令。
  • ret指令的作用:
    从函数栈帧的顶部找到IP旧值,将其出栈并恢复IP寄存器。

如何访问栈帧

  • 函数调用的机器级表示
  • 为什么函数调用栈在画的时候栈底在上面,栈顶在下面?
    函数调用栈
    对于一个32位的系统,OS会给每个进程分配4GB的虚拟空间。地址从0x0000 0000到0xFFFF FFFF.高地址的1GB为OD内核曲,低地址的3GB为用户区。函数调用栈在用户区的高地址处,向低地址区延伸。如下图所示:
    4GB的虚拟空间
  • 标记栈帧范围:EBP、ESP寄存器
    寄存器EBP是堆栈基指针,指向栈顶的函数栈帧的底部,ESP是堆栈顶指针,指向栈顶。
  • 访问栈帧数据:push和pop指令
    push和pop指令实现入栈、出栈操作,x86默认以4字节为单位。
    push X #先让ESP-4,再将X压入
    pop Y #栈顶元素出栈写入Y,再让ESP+4
    注:X可以是立即数、寄存器、主存地址
    Y可以是寄存器、主存地址
  • 访问栈帧数据:mov指令
    通过sub和add指令改变栈顶指针的指向,用mov把栈中元素复制出来或者把别的地方的元素复制到栈里。

如何切换栈帧

  • 函数调用的机器级表示
    根据caller和add的代码分析:
caller: 
push edp
mov ebp, esp
sub esp, 24
mov [ebp-12], 125
mov [ebp-8], 80
mov eax, [ebp-8]
mov [esp+4], eax
mov eax, [ebp-12]
mov esp, eax
call add
mov [ebp-4], eax
mov eax, [ebp-4]
leave
ret
add:
push ebp
mov ebp, esp
mov eax, [ebp+12]
mov edx, [ebp+8]
add eax, edx
leave 
ret

caller函数的call语句执行后会把当前的IP值压栈。esp-4, 指向当前IP值。之后跳转到add函数中,add的开始语句是push ebp和mov ebp, esp先把caller函数的ebp压栈,然后把让ebp和esp指向都指向栈顶。每一个函数开头都有这样的指令。保存上一层函数的栈帧基址,并设置当前函数的栈帧基址。
push ebp和mov ebp, esp这两条汇编指令也可以合起来叫enter。
函数返回时如何切换栈帧?

mov esp, ebp #让esp指向当前栈帧的底部
pop ebp #将esp所指元素出栈,写入寄存器ebp

这两条指令可以用leave代替。
ret指令的作用:从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器。

如何传递参数和返回值

  • 函数调用的机器级表示
  • 一个函数的栈帧内部可能包含哪些内容?
    • 栈帧最底部保存上一层栈帧基址(ebp旧值)
    • 栈帧最顶部一定是返回地址(当前函数的栈帧除外)
    • 通常将局部变量集中存储在栈帧底部区域。
      C语言中越靠前定义的局部变量越靠近栈顶。
    • 通常将调用参数集中存储在栈帧顶部区域。
      参数列表中越靠前的参数越靠近栈顶。
    • gcc编译器将每个栈帧大小设置为16B的整数倍(当前函数的栈帧除外),所以栈帧内可能出现空闲未使用的区域。
  • 所以在调用函数传参的时候需要先将参数的值存到栈帧顶部的区域。
  • 内层函数给外层函数返回值的时候通过eax寄存器,先将返回值存到eax寄存器中,然后再将eax寄存器里的值复制到外层函数的对应位置。
  • 如果调用者用到了一些寄存器,被调用的函数也可能用到,则可以把这些寄存器压栈保存,等被调函数返回后再出栈还原。所以函数调用栈可能还有一部分区域用于保存一些寄存器的值。

4.4 CISC和RISC

CISC

CISC:Complex Instruction Set Computer 复杂指令集的计算机系统
设计思路:一条指令完成一个复杂的基本功能
代表:x86架构,主要用于笔记本、台式机等。

RISC

RISC: Reduced Instruction Set Computer 精简指令集的计算机系统
设计思路:一条指令完成一个基本“动作”;多条指令组合完成一个复杂的基本功能,语句相对简单,便于实现“并行”、“流水线”
代表:ARM架构,主要用于手机、平板等

CISC和RISC的对比

对比项目 CISC RISC
指令系统 复杂、庞大 简单、精简
指令数目 一般大于200条 一般少于100条
指令字长 不固定 定长
可访存指令 不加限制 只有Load/Store指令
各种指令执行时间 相差较大 绝大多数在一个周期内完成
各种指令使用频度 相差很大 都比较常用
通用寄存器数量 较少
目标代码 难以用优化编译生成高效的目标代码程序 采用优化的编译程序,生成代码较为高效
控制方式 绝大多数为微程序控制 绝大多数为组合逻辑控制
指令流水线 可以通过一定方式实现 必须实现
posted @ 2024-07-23 17:32  菜鸟小楠  阅读(71)  评论(0编辑  收藏  举报