野人王2:intel IA-32架构简介

来源:百度文库 编辑:偶看新闻 时间:2024/05/04 18:45:32

intel IA-32架构简介

IA32的cpu操作模式

1保护模式:

这个模式是cpu的native状态.它可以在一个保护的多任务的环境中直接执行 "real-address mode"的8086的软件.这个特性叫做virtual-8086模式.

这个模式也就是cpu的一般工作模式.因为这个模式基本上打开了cpu的所有特性.

2 real-address模式:

这个模式它实现了一个8086处理器的环境的扩展.换句话说,可以说是模拟了一个8086的程序环境.

计算机在重启或者开机的时候就处于这个模式,因此每次开机需要操作系统来切换到保护模式.这个模式中很多cpu的特性都是被关闭的.

3 system management 模式(SMM)

这个模式提供给操作系统一个执行平台指定的功能,提供了一个透明的架构.比如系统安全或者电源管理.

当进入到smm中后,cpu保存当前的运行上下文,然后跳转到一个完全隔离的地址空间.然后smm指定的代码会被执行.当从smm返回时,处理器会返回刚才保存的状态.


IA-64的cpu操作模式:

IA-64加了一个模式就是IA-32e.这个模式包含下面两种子模式.

1 compatibility 模式

这个模式主要是用来不重新编译32位或者16位的程序,而能直接在64位处理器上运行.


2 64位模式

这个模式他所要求的环境必须是64位的操作系统,以及64位的应用程序.这个模式下能存取64位的线性地址空间.

一个64位的操作系统既能在64位模式运行64位的应用程序,也能在兼容模式下运行32位程序(不重新编译).


执行环境:

1 地址空间

IA-32 最大有4GB 线性地址空间,以及64GB物理地址空间

内存存取是虚拟为一个栈来进行的.


内存模型,也就是程序用来存取内存的方式:

1 flat memory模型

在这个模型下,内存看起来就是单独的持续的地址空间.这个空间叫做线性地址空间.代码,数据以及栈都是保存在这个地址空间内.

2 segments内存模型.

这个模型下,内存看起来就是由一些不连续的地址空间组成的,这些地址空间就称为段.数据,代码,以及栈包含在不同的段里面.要在一个段里面寻址,则程序必须生成一个逻辑地址.

逻辑地址由一个段选择符和一个位移组成.

在内部,所有的段都被映射为线性地址,因此要存取一个内存位置,处理器必须翻译逻辑地址到线性地址.

3 Real-address mode memory model

这个模式主要是针对8086处理器的.


下面这张图表示了3种内存模型:





寄存器:

下面这张图表示了IA-32架构的所有寄存器:





段选择符必须保存到段寄存器中.一共6个段寄存器.分别代表不同的段.

cs,ss,分别代表代码段以及栈段.剩下的都是数据段寄存器.

General-Purpose Registers有8个,分别是EAX, EBX, ECX, EDX, ESI, EDI, EBP, 和 ESP.

ebp和esp为帧指针和栈指针.


数据类型:

1 byte,words,doublewords,quadwords以及doublequadwords.

分别是8位,16位,32位,64位,128位.

这里要注意doublequadwords只有在拥有sse扩展的处理器才存在.

下面这张图表示了所有的数值类型:







存取words,doublewords,quadwords以及doublequadwords的地址最好都要自然对齐,一般是4字节或者8字节对齐.


如果是一次没有对齐的内存存取,则处理器将会请求两次内存存取,才能取到对应的内存数据.

还有一些指令操作doublequadwords时也要求对齐.如果是非对齐的存储的话,会产生一个general-protection exception (#GP).而他一般是16字节对齐.

IA-32的浮点是符合IEEE 754的标准的.具体格式可以看下面的表:





2 指针类型.

near pointer和far pointer

far pointer是一个逻辑地址由段选择符和位移组成:




还有一些类型比如String,BCD等,这里都没有介绍,详细的请去看intel的官方手册.

函数调用:

主要是4条指令 CALL,RET,ENTER以及LEAVE.

栈只不过是在内存中虚拟出来的一块类似数组的区域(或者说是段)而已,段寄存器SS保存有栈的地址.

使用PUSH和POP指令来操作数据的压栈,入栈.

这里要注意栈是向下增长的.

下图就是栈的结构:





初始化和确立一个栈为当前栈,也就是栈的切换需要下面几个步骤:

1 建立一个stack段.

2 load这个段选择符到SS寄存器.这里能使用MOVE,POP或者LSS.

3 load栈指针到ESP寄存器.


栈的对齐:

栈的地址(其实也就是栈的段描述符)必须是16位或者32位对齐.依赖于段的宽度.

因此我们经常可以看到汇编中这一句:

and    $0xfffffff0,%esp

这句也就是对其的语句。

存取栈的地址宽度:

要么是16位,要么是32位。当为16位宽时则栈寄存器使用sp,当为32位宽则使用esp。使用16位还是32位依赖于当前代码段寄存器的D flag。


过程调用:

一般来说使用stack-frame 的话,都会在push本地变量进栈之前,复制esp到ebp。因此可以这么说当栈建立起来时ebp和esp一样,指向当前栈段的地址。也就是ss寄存器的值。

接下来来看call指令。

call指令:
这个指令应该是条伪指令。

当执行所要跳到的过程第一条指令之前,call指令会先push 当前EIP寄存器的内容到当前栈。这个地址叫做return-instruction pointer,保存这个地址的目的是为了等会函数调用返回后能够正确的返回到当前的位置。

而对应的RET指令就是从栈里面pop出return-instruction pointer到EIP寄存器。

这里要注意处理器并不会保存return-instruction pointer的位置,因此应用程序要确定当函数调用完毕后,栈指针指向return-instruction pointer,这一切都要在ret指令调用之前完成。因此一般都会直接拷贝EBP(这里ebp保存的是调用前的esp的位置)到ESP,然后调用 ret,此时ESp中就保存有调用前我们保存的return-instruction pointer,我们就可以直接pop,然后保存到EIP寄存器。

这里还有一个要注意的,处理器并没有强制要求return-instruction pointer一定要指向调用者的栈,它可以指向任何地址。因此我们可以在ret指令执行之前修改这个值,也就是修改esp的值,然后当ret执行之后,我们就跳到另外的地址了。不过这样非常危险,举个例子,csapp中的那个缓冲区攻击的练习题就是用这个来做的。

CALL指令分两种类型,一种是调用当前代码段的函数,这种叫做near call,一种是调用不同代码段的,这种叫做far call.对应的RET指令也是分为两种类型匹配CALL指令。

接下来来看这两种类型的不同:

当执行一个near call时,处理器会做下面几步:

1 压当前的EIP入栈,也就是保存return-instruction pointer。

2 load被调用函数的位移到EIP寄存器

3 开始执行。

对应的执行RET:

1 弹出保存的return-instruction pointer到EIP。

2 如果ret指令有参数N,则增加栈指针n个字节来释放参数。

3 返回调用者。

当执行一个far call时,处理器会做下面几步:

1 压当前的CS寄存器的值入栈。也就是保存代码段。

2 压当前的EIP入栈。

3 load被调用者函数的代码段的段选择符到CS寄存器。

4 load被调用函数的位移到EIP寄存器

5 开始执行。

对应的ret:

和上面的near类似,只不过会将cs弹出。

下面这张图表示了near call 和far call的区别:






下面来看参数的传递:
参数传递有三种类型,分别是通过General-Purpose寄存器,参数列表或者栈。

1 General-Purpose寄存器

处理器在过程调用中并不会保存General-Purpose寄存器的状态,调用者可以传递6个参数给被调用者,通过复制参数到这些寄存器中的任意一个(除了ESp和EBP).这一切都要在CALL指令之前完成。

2 参数列表

上面通过寄存器的传递,我们最多只能传递6个参数,并且不能传递复杂的数据结构。而我们用参数列表就可以做到,它是将所要传递的参数放到内存中的一个数据段,然后一个保存有这个参数列表的指针就能够通过General-Purpose寄存器传递给被调用者了。

3 栈。

这种也是为了传递更多得参数。它很简单,就是将要传递的参数压入栈,然后通过帧指针很容易存取参数,。


虽然处理器不能保存相应的函数状态信息。可是这里有几条指令,比如PUSHA,POPA,保存和弹出General-Purpose寄存器的值。

下面来看cpu操作的特权级别:
一共有4个级别,可以看下图:





0是最高级别。当低级别想要存取或者操作高级别的段时,必须通过一个叫做gate的接口,否则就会报一个#GP的异常。

这里要知道每一个级别都有自己独立的栈。

这时如果一个低级别的函数调用一个处于高级别的段的函数时,虽然也叫做far call,可是还是有些不同的:

1 CALL 指令所提供的段选择符指向一个特殊的数据结构,叫做call gate descriptor.这个结构包含了下面3个部分,存取权限,被调用者的代码段的段描述符以及代码段的位移。

2 处理器会切换到一个新的栈执行被调用函数。这里还有一个要注意的,对于级别3来说,段选择符和栈指针分别保存在SS和esp寄存器里面,而在级别0,1,2来说,他们则是保存在一个系统段叫做task state segment(TSS).

接下来就来看不同特权级别的CALL和入RET指令的操作。

当调用一个更高特权级别的函数,处理器会做如下操作:

1 执行一个权限检测。

2 保存当前的SS, ESP, CS, and EIP寄存器(注意这里是处理器内部做的)。

3 为新栈load栈指针和段选择符(从TSS段)到SS和esp寄存器中。

4 将第二步保存的SS和esp压入新的栈。

5 从调用者的栈拷贝参数到新的栈。而拷贝多少值,这个值是保存在call gate descriptor中的。

6将第二步保存的CS和EIP压入新栈。

7  从call gate将新的代码段和指令指针保存到CS和EIP。

8 开始执行。

这里RET就不描述了,基本对照CALL和前面的RET的描述就可以理解了。

可以看下面的图,能更好的理解这个过程:





最后还有一对指令那就是ENTER和LEAVE指令.

这两个指令自动的创建,释放被调用者的栈帧.ENTER指令具体的用法以及描述最好还是去看intel 的处理器手册. 看gcc生成的汇编很少使用ENTER,不知道什么原因..

LEAVE指令复制EBP寄存器的内容到ESP,也就是释放掉所有的为了执行当前函数所分配的栈空间.这个指令其实也就是restore ESP,因此我们一般都是在ret之前调用这个指令.