csapp第三章 程序的机器级表示

对于严谨的程序员来说,能够阅读和理解汇编代码仍是一项很重要的技能。

阅读编译器产生的汇编代码,需要具备的技能不同于手工编写汇编代码。——感觉阅读和编写在一个量级了,应该是阅读比较弱一点吧,(也许就是一个级别的)。

精通细节是理解更深和更基本概念的先决条件。

本章基于两种相关的机器语言:Intel IA32和x86-64,前者是当今大多数计算机的主导语言,而后者是前者在64位机器上运行的扩展。本章的内容:

  • 先快速的浏览c语言、汇编语言以及机器代码之间的关系。
  • 然后介绍IA32的细节,从数据的表示和处理以及控制的实现开始。了解c语言中的控制结构是如何实现的。
  • 然后,我们会讲到过程的实现,包括程序如何维护一个运行栈来支持过程间数据和控制的传递,以及局部变量的存储。
  • 接下来,我们会考虑在机器级如何实现像数组、结构和联合这样的数据结构。
  • 结尾,我们会给出一些用GDB调试器检查机器级程序运行时行为的技巧。

3.1 历史观点

Intel处理器的换代:8086——80286——i386——i486——Pentium——PentiumPro——PentiumII——PentiumIII——Pentium4——Pentium4E——Core2——Corei7。

这些所有的代都是Intel系列的,Intel系列本身有很多名字,比如x86,比如IA32,Intel系列中64位扩展称为x86-64。最常用的是x86。也就是说x86就是Intel每一代处理器的统称。

8086和80286的存储器模型都已经过时了,从i386开始提供的平坦寻址模式也是linux使用的模式,这是将存储空间看做一个大的字节数组。

看来i386是一个转折点,从这里开始系统扩展为32位,同时,GCC为32位执行的默认调用仍然假设是为i386机器产生的代码,Intel系列,x86,是向后兼容的,所以i386的可以执行的代码,后面的都可以执行。

3.2 程序编码

提高gcc的优化级别,会使得产生的机器代码和初始源代码之间的关系非常难以理解。-O2,第二级优化是默认选择。这里是LMNOPQ的O,不是零。

机器代码的两种形式:目标代码,可执行代码。目标代码包含了所有的指令但还没有填入地址的全局值,后者是处理器执行的代码格式。

对于机器级编程来说,有两种抽象尤其重要:机器级编程的格式和行为,定义为指令集体系结构。机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。

能够理解汇编代码以及它与原始c代码的联系,是理解计算机如何执行程序的关键一部。

IA32机器代码和原始的c代码差别非常大。一些通常对c语言程序员隐藏的处理器状态是可见的:

程序计数器,PC,用%eip表示,指示要执行的下一条指令在存储器中的地址,这里的存储器是主存。

整数寄存器文件包含8个命名的位置,分别32位,可以保存:地址,程序状态,局部变量,函数返回值。

条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。用来实现if和while等。

一组浮点寄存器存放浮点数据。

机器代码只是将存储器看成一个很大的、按字节寻址的数组。

程序存储器(program memeory)包含:程序的可执行机器代码(代码和数据区),操作系统需要的一些信息(应该也在代码和数据区),用来管理过程调用和返回的运行时栈(栈),以及用户分配的存储器块(堆)。

操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器(processor memory)中的物理地址。

gcc -O1 -s xxx.c 输出 汇编代码

gcc -O1 -c xxx.c 输出 目标代码

objdump -d xxx.o 反汇编机器代码(包括目标代码和可执行代码,两者的区别在于偏移地址)

gdb 可以直接对机器代码使用(包括目标代码和可执行代码)

gcc -s产生的汇编代码中,所有以点开头的行都是用于指导汇编器和链接器的。

gcc和objdump产生的汇编代码是ATT风格,微机原理里面学习的是Intel风格。有所不同,但本质没有改变。

3.3 数据格式

Intel用术语word表示16位数据类型,32为double words,64位为quad words,这是由于最初Intel系列是从16位开始的。

ATT风格的汇编代码指令都有一个字符后缀,表面操作数的大小。Intel风格的汇编代码是没有的。

3.4 访问信息

IA32的cpu中有8个32位的寄存器,%e是前缀,依次是:ax,cx,dx,bx,si,di,sp,bp。前6个可以看作是通用寄存器,大多数情况下。前3个和后3个的保存和恢复惯例不同。最后两个指向程序栈中重要位置的指针。

指令的源数据值可以以常数形式给出,或是从寄存器或存储器中读出。也就是,常数,寄存器,内存。这么记忆:绝大部分源数据类型都是从存储器中读,除了两种:$Imm和%exx,这两种格式分别是立即数和寄存器的值,任何其他的形式都是从存储器中读,计算的结果是存储器某一字节的地址。

IA32的一条限制:数据传送指令的两个操作数不能都指向存储器位置。

数据传送指令的源操作数在左,目的操作数在右(ATT风格),(Intel风格则相反)。

栈在程序的虚拟地址空间的上部,再往上就是内核虚拟空间了,栈底紧挨着内核虚拟空间,栈顶向下增长。%esp保存这栈顶元素的地址。

将一个双字压入栈,先将%esp减小4,然后将双字放入这多出来的4个字节的空间中。出栈则是先读出4个字节,然后%esp加4。

因为栈和程序代码以及其他形式的程序数据都是放在同样的存储器中(虚拟地址空间),所以程序用标准的存储器寻址方法访问栈内任意位置。

3.5 算术和逻辑操作

加载有效地址地址指令,leal,只是将源操作数计算出来的地址交给目的操作数,貌似:源操作数是存储器访问格式的,目的操作数是寄存器。

一元操作:incb/w/l,decb/w/l,negb/w/l,notb/w/l,前面两个好理解,第三个是取负,这里用到了一个概念,取负是指2变成-2,-3变成3,可以认为,就将后面的操作数认为是补码编码的了,然后如果本来是2的补码,现在就要编程-2的补码,也就是进行0-2的运算。第四个是取反,这个好理解,取反吗。

二元操作:addb/w/l,subb/w/l,imulb/w/l,xorb/w/l,orb/w/l,andb/w/l。12两个是位级的运算,不,其实,所有的都是位级运算,因为是汇编吗。但值得注意的是:第3个,乘是先数学上的乘,然后截断一下。这是2章中得到的结论,至少是对于imulb/w/l3者。后3个好理解,异或,或,并。

移位:salb/w/l,shlb/w/l,sarb/w/l,shrb/w/l。向左left,向右right移位。向左总是补0的,向右就是算术移位和逻辑移位了。移位量是单个字节的编码,因为只允许0到31位的移位(只考虑移位量的低五位)。移位量只可以是一个立即数或者单字节寄存器元素%cl。

又有点想法:首先是汇编代码只认位级,不管对于有符号数还是无符号数,汇编都是位级,如果位级的运算对有符号和无符号都正确,就最好了。而无符号表示的位级就是汇编的位级所以没有问题,而补码编码的位级使用位级的计算也是正确的,大部分吧。。。。。。好像不对。

再来点想法:上面的这些指令,既可以用于无符号运算,也可以用于补码运算。前半句好理解,无符号运算。后边也可以用于补码运算是说:incb来说,是对的,总是对的,溢出的情况也是对的。decb也是,neg也是,notb也是。addb等会在某些时候出现溢出,但在不溢出的时候是正确的。好像也实用。

想法:应该是这样,补码运算是补码编码对应的人为制定的运算规则。这种运算规则和IA32汇编语言的指令产生的运算效果是一致的。而IA32本身就是无符号的位级运算,所以IA32适用于无符号也适用于补码运算。

特别的:imull,mull可以是一元操作(上面的二元操作)。divl和idivl是除法。cltd是符号扩展。这5个是特殊的算术操作。

3.6 控制

机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值(是测试,而不是真实计算),然后根据测试的结果来改变控制流或者数据流。

先讲机器级机制,然后说明c语言的结构是如何组成的,然后介绍使用有条件的数据传输来实现与数据相关的行为。

除了前面说的8个寄存器,IA32cpu还有一个条件码寄存器。它们描述了最近的(最近的)算术或逻辑操作的属性。

最常用的条件码:CF进位标志,可以用来检查无符号操作数的溢出。ZF零标志,最近操作的结果为0。SF符号标志,最近的操作得到的结果为负数。OF溢出标志,最近的操作导致了一个补码的溢出,正溢出和负溢出。

有点奇怪,位级不是没有正负的吗?怎么判断正负溢出?

暂且不管他们是如何实现的,这4个先认作是数学上的说明规定吧。

leal不会改变任何条件码,逻辑操作进位标志CF和溢出标志OF都为设置为0,移位操作进位标志CF会设置为最后一个被移出的位,而溢出标志OF会设置为0,INC和DEC指令会设置溢出OF和零ZF标志位(可以理解),但不会改变进位标志CF(不理解,但这时规定)。

CMP指令基于A-B,TEST指令基于A&B。

条件码通常不会直接读取,常用的使用方法有3种:

可以根据条件码的组合,将一个字节设置为0或者1——SET系列指令(目的操作数是8个单字节寄存器之一,或者存储器中的一个字节)。

可以条件跳转到程序的某个其他的部分——JUMP系列指令。

可以条件的传递数据——前两者是控制转移,路径的变化,这一点导致了现代处理器流水线处理的中断(预测错误的时候),而条件数据传递则可以保证数据按照流水线处理,效率要远高于前面的条件控制转换——CMOV系列指令。

我有可能弄错了一件事,机器代码是位级没错,但是机器代码是理解补码的,知道什么是负数。这应该是显然的阿,因为,讲补码的时候讲的不就是位级表示吗。我这一点没有考虑到。同时,前面讲的IA32的指令适用于补码运算,这一点我误会了,这是说:补码运算是一种位级运算,IA32的指令支持这种位级运算。

有一点,补码运算和无符号数运算的位级表示在大部分的时候是相同的,这个时候就使用一个IA32指令。而有些时候是不同的,那么这个时候,IA32就给出了两种指令,比如imull和mull(一元操作)。IA32的指令完全的,封闭的实现了所有的补码运算和无符号运算,并且指令集中很多指令同时适用于两者。这才是正确的理解。

这样的话,我也理解了CF,ZF,OF,SF的意义了。

我们来看SET指令。

看一种特殊情况:a、b和t分别是变量a、b和t的补码形式的整数,多少位不关心,但都是一样的位数。计算表达式t=a-b。

看sete—ZF—相等/零,这好理解阿,表达式的计算结果如果是0,就说明了a-b=0,这没有特殊情况,只有a=b。

看setl—SF^OF—小于,这里有2种情况SF^OF这个表达式是1,就是10,01,异或吗。如果是10,那么SF为1,OF为0,这种情况下无溢出,a-b就是正常计算,SF为1说明结果是负数,也就是a<b了,正确。如果是01,那么OF为1,就代表溢出了,那么a-b溢出了,如果正溢出(正溢出的结果是负数,那么SF就是1),就是说两个正数相加,结果却成了负数,这就是正溢出,那么a-b=a+(-b),a必须是正,-b也必须是正,这只能说明a为正,b为负,也就是说a>b,而SF^OF=0,说明setl结果是0,也就是不小于了;如果是负溢出(结果是正数了,那么SF就为0),就是说两个负数相加,结果却成了正数,这就是负溢出,那么a-b=a+(-b),也就是说a为负数,-b也是负数,也就是说b为正数,所以a<b,而SF^OF=1,也就是小于了。

看一种特殊情况:a、b是无符号形式的整数。

这种情况,在运算t=a-b时,如果a-b<0,那么CMP指令会设置进位标志。这里:是规定阿。无符号比较使用的是进位标志和零标志的组合。setb什么的指令。这也就是说,c里面是分正负的,然后汇编里面条件判断的时候,对有符号和无符号的条件判断指令是不同的,这也是IA32处理补码运算和无符号运算不同的一点吧。

注意机器代码如何区分有符号和无符号值是很重要的。同c语言不同,机器代码不会将每个程序值都合一个数据类型组合起来,机器代码只会将各种命令和后面的位级表示联系起来,后面的位级表示只是位级表示,而不分有符号还是无符号,分别这一点的是前面的各种命令,但很多时候不管是有符号还是无符号前面的命令都是一样的,这就代表了对于这一步来说,机器代码的处理对于补码和无符号是相同的。当然还有一些时候,由于无符号和有符号的不同,前面的命令会有所不同。这时候也就是说在这一步的操作上,无符号和补码的运算过程是不同的。

jmp是无条件跳转指令,其他的都是有条件跳转的。jmp可以间接跳转,也可以直接,其他的,也就是有条件的跳转只能是直接的。

当从汇编代码汇编成机器代码时,跳转指令有几种编码,典型的,都是于PC(程序计数器)相关的。这些编码会将目标指令的地址(要跳转的地址)与本条跳转指令后面的那条指令的地址之间的差作为编码。还有的编码使用“绝对“地址。

通过使用与PC相关的跳转目标编码,指令编码很简洁,而且目标代码可以不做改变就移到存储器中不同的位置。因为指令里面是相对位置。

if else 结构的汇编代码会交替使用有条件和无条件跳转。

do while,while,for,第一个最符合汇编的风格,第二个借用第一个,第三个借用第二个。控制的条件转移为循环翻译成机器代码提供了基本的机制。但for里面的contiune需要注意下,要加一个额外的goto。

条件传送指令CMOV和SET和JUMP指令对应的比较好。

不是所有的条件表达式都可以用条件传送来编译。如果两种表达式(if下的body和else下的body)的任意一个可能产生错误条件或者副作用,就会导致非法的行为。

总的来说:条件数据传送提供了一种用条件控制转移来实现条件操作的替代策略。

switch,重要的地点在跳转表,挑出标号和跳转表的对应关系即可。

3.7 过程

一个过程调用包括将数据和控制从代码的一部分传递到另一部分。过程调用就是函数。数据就是函数参数和返回值。控制应该就是当前运行的代码吧。另外,过程的局部变量的空间的分配和释放。

IA32中,控制的转移是有相应的指令的,但是,数据传递和局部变量的分配释放通过操纵程序栈来实现。

从这里看,过程调用分成3个部分:控制转移,数据传递,分配与释放局部变量。

为单个过程分配的那部分栈称为栈帧。

栈帧也可以看做有两种,一种是处于最低端的栈帧,也就是当前过程的栈帧;另一种就是处于栈中部的(非最低端)栈帧,也就是之前调用的过程,还没有返回。

每个栈帧的顶端都是%ebp,这里有两个意思,首先当前过程,拥有当前寄存器的值,%ebp寄存器的值是一个地址,地址是本栈帧最高字的地址。这个最高字记录着上一栈帧的最高字地址,以此类推。

控制:其实表示的就是当前cpu在处理哪一个过程的代码,在处理那一个过程的代码,我们就说当前的控制在那一个过程。

当前栈帧总是以%ebp中的值表示栈帧的最高字地址。最低字地址由%esp表示。中间栈帧最高字地址,保持在其下一个栈帧(也就是较低栈帧)的最高字节中,中间栈帧的最低字节都是返回地址,就是说,当较低栈帧代表的过程返回时,将跳转的代码的位置。

栈的重点还是两个寄存器:%ebp和%esp。前者记录当前栈帧的最高字地址,后者记录栈的最低字地址,但其实也就是当前栈帧的最低字地址。一个是帧指针,一个是栈指针。

栈帧中,可以保存:寄存器,本地变量,临时变量。

局部变量应该包括本地变量和临时变量,一般来说,局部变量保存在寄存器中,但也用栈来保存,用栈来保存的时候,一般有如下原因:

没有足够多的寄存器存放所有变量(好理解)

有些局部变量是数组和结构,因此必须通过数组和结构引用来访问。(就是通过地址来访问)

要对一个局部变量使用地址操作符&,必须能够为它生成一个地址(好理解)

当前栈帧的过程使用的参数都存放在上一栈帧中,上一栈帧的过程要使用的参数存放在上上一个栈帧中。

call指令的效果是:将返回地址入栈,并跳转到被调用过程的起始处。(这里的效果对栈来说,只有一点,就是入栈了返回地址,这句话之后,%esp就指向返回地址所在的位置了。这意味着旧栈帧已经封存了,但新的栈帧还没有出现,后半句跳转到被调用过程的起始处,说明了一点,过程的起始处有建立新栈帧的指令,新栈帧的建立是新过程自己完成的)

返回地址,就是call后面的指令的地址。

当调用过程返回时,执行会从此处继续。

ret指令的效果是:从栈中弹出地址,并跳转到这个位置。(这里有个重点,就是ret并不知道当前栈顶存储的是什么,他只是简单的弹出这个值,然后跳转去。所以要正确使用这个指令,必须,先要使得栈指针指向返回地址,这也就是说,新栈已经没有了,栈顶变成了旧栈的最后一个字。这个时候才可以使用ret指令,否则就不对了。)

leave指令的效果是:可以使栈做好返回的准备。(这里包含两点:1使得%ebp指向旧帧的最高字地址,2使得%esp指向旧帧返回地址。;使用这个指令之后,再使用ret,就没有问题。但这个指令不是必须的,其可以通过其他指令代替)

过程要返回整数或者指针的时候,寄存器%eax可以用来返回值。其实汇编级别的返回,就是在栈变化的时候(过程控制变化的时候)保持某一个寄存器不变,这样,返回的过程就可以使用这个寄存器,也就是说返回值了。相信,如果是double类型的,那么可能用两个寄存器返回也不一定,肯定是类似的过程。

调用者保存寄存器——%eax,%edx,%ecx。被调用者随便用。

被调用者保存寄存器——%ebx,%esi,%edi。被调用者要用这些的话,就需要先保存这些到栈,然后在ret前恢复这些寄存器。

此外,%ebp和%esp是要保持的,%esp还好,但%ebp是很需要维持的,算是被调用者保存寄存器了。因为这两个只用于当前过程。

gcc坚持一个x86编程指导方针:一个函数使用的所有栈空间,必须是16字节的整数倍。

这里突然想到了过程和函数是不同的,一个过程是顺序的,一个函数不一定,一个函数可以是顺序的,也可以不是,比如递归函数。对于递归函数,每一次的递归,对于机器语言来说,就是一个过程。所以过程和函数是不对应的。一个函数至少是一个过程。

一个函数所使用的栈空间,必须是16字节的整数倍。如果是单一过程,也就是一个栈帧的情况,那这个栈帧就应该是16字节的倍数,如果多个过程,那么单个栈帧搞不好就可以没有16字节了,这个不多想了。

一个过程的开始,通常是在call指令执行后,这个时候,旧帧已经封存了,但新帧还没有建立,所以个过程的开始要先建立新栈帧。通常是两个语句:pushl %ebp和movl %esp, %ebp。

也可能多一个pushl %ebx。

编译器根据一组很简单的惯例来产生管理栈结构的代码:

参数在栈上传递给函数,可以从栈中用相对于 %ebp的正偏移量来访问他们。

可以使用push或者从栈指针减去偏移量来在栈上分配空间。

返回前,函数必须将栈恢复到原始条件,可以恢复所有的被调用者保存寄存器和%ebp,并且重置%esp使其指向返回地址。

3.8 数组分配和访问

IA32的存储器引用指令可以用来简化数组访问。movl (%edx,%ecx,4), %eax;%edx放置数组第一元素的地址,%ecx是数组元素的个数,4是数组元素的字长。正好对应。

&E[i]-E计算结果是i,而不是i*4。这是数据运算中唯一值得注意的地方。

D[i][j]的地址是D+L(C*i+j),这里L是类型字长,C是一维的个数,还比较好理解,i为1,就代表了,之前有一行了。一行C个,所以是C(也就是C*1)。然后本行第j个,也就是j。所以是C*1+j。

寄存器溢出:没有足够多的寄存器来保存需要的临时数据,因此编译器必须把一些局部变量放到存储器中。

通常,读存储器完成起来比写存储器要容易的多,因此将只读变量溢出比较好。

c99引入了一种能力,允许数组的维度是表达式,在数组分配的时候才计算出来。这个,暂且放置吧。

3.9 异质的数据结构

结构的所有组成部分都存放在存储器中一段连续的区域内。

当用联合将各种不同大小的数据类型结合到一起时,字节顺序问题就变得很重要了。这时,大端和小端的区别就会体现。

但如果数据类型的大小相同,那么,就不会存在大端小端的差异。(比较好理解的,重点是字长相同的,不同类型)

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值的倍数。就是说,int类型对象放在存储器里面,其地址,要使某个值得倍数。char类型也是,其他的基本数据类型都是如此。

这种限制简化了处理器和存储器之间接口的硬件设计。

这是这个意思:假设,处理器每次从存储器取数据,他就只能取8个字节,从地址0开始,取8个,下次从地址8开始,取8个,然后依次的从0x10,0x18,0x20....,如果一个double类型的对象(8字节)放在0x10地址,一次操作就取到了这个double,但如果这个double放在0x11,那么,先从0x10取后7个字节,然后从0x18取1个字节。这需要两次。

不过这好像是性能上面的问题,不过,反过来想,从硬件设计上来说,只从0x10,0x18这种取肯定要比从0x10,0x11,0x12任意位上取简单些吧。

linux沿用的策略是,2字节数据类型的地址必须是2的倍数;更大的数据类型是4的倍数。对于struct,根据其包含对象的最大类型,比如int和short,按int,short和char,按short。char是1字节对齐,也就是不对齐。

分配存储器的库例程的设计必须是他们返回的指针满足运行机器最糟糕情况的对齐限制。

对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。

3.10 综合:理解指针

每个指针都有一个值。特殊的NULL表示该指针没有指向任何地方。

指针可以指向函数,函数指针的值是该函数机器代码表示中第一条指令的地址。(也比较好理解)

3.11 应用:使用GDB调试器

通常的方法是在程序中感兴趣的地方附近设置断点。

断点可以设置在函数入口后面,或是一个程序的地址处。

GNU的调试器GDB支持机器级程序的运行时评估和分析——就是说可以在任意一条机器代码处设置断点。

在断点处,我们能够以各种方式查看各个寄存器和存储器位置。

命令暂时不表。

3.12 存储器的越界引用和缓冲区溢出

 对越界的数组元素进行写操作会破坏存储在栈中的状态信息。

缓冲区溢出:在栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。

缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数——通常给程序一个字符串,字符串包含两部分:可执行代码的字节编码(攻击代码),以及,会用一个指向前面可执行代码的指针覆盖返回地址。这样,当ret的时候,控制就交给了可执行代码。

可执行代码的字节编码(攻击代码)的攻击形式

  • 一种方式是攻击代码会使用系统调用启动一个外壳程序,给攻击者提供一组操作系统函数。
  • 另一种方式是,攻击代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行ret指令,(表面上)正常返回给调用者。

一个想法:缓冲区溢出真的就是指字节数组保存字符串时候发出的超出分配空间的情况。很NB。

对抗缓冲区溢出攻击:

  • 栈随机化(栈的位置在程序每次运行时都有变化)在linux系统中,栈随机化已经变成了标准行为。这可以加大攻击一个系统的难度,但总是可以暴力破解的。是一个相乘的关系,空操作雪橇是一段nop,当程序运行时,如果返回地址被篡改到这一部分中,那么着一部分会划过,然后到达攻击代码,程序运行次数乘上nop的个数,如果大于随机地址的范围,总能暴力破解掉。
  • 栈破坏检测——栈保护者,金丝雀。
  • 限制可执行代码区域——消除攻击者向系统中插入可执行代码的能力。是否能将代码限制在由编译器在创建原始程序时产生的那个部分中,取决于语言和操作系统。

在c程序中更包括汇编代码只能针对莫一类机器,因此只有当相应的特性只能用这种方式才能访问时,才使用这种方法。

3.13 x86-64:将IA32扩展到64位

今天使用的IA32格式中的大部分是在1985年随着i386微处理器的出现所定义的,当时将原来8086的16位指令集扩展到了32位。注意,这里表明的意思是:IA32中的大部分命令什么的都是在1985年定义的。很老了,但还在使用。

虽然后续的处理器系列引入了新的指令类型和格式,但是为了保持向后兼容性,许多编译器,包括GCC,都避免使用这些新特性。

我们正在经历一个相intel指令集64位版本的过渡。最初由AMD在2003年提出并命名为x86-64,x86-64是指令集,IA32也是指令集,IA32的大部分指令都是在1985年定义的。x86-64则较新一些,虽然是intel64位指令集,但最初却使由AMD提出的。

IA32的32位字长已经成为限制微处理器能力不断增长的主要因素。

最重要的是机器的字长定义了程序能够使用的虚拟地址范围,32位字长就是4GB虚拟地址空间。

虽然AMD将x86-64指令集命名为AMD64,但是大家都比较喜爱x86-64这个名字。

intel将自己的x86-64指令集命名为Intel64,但是大家都比较喜爱x86-64这个名字。

本书中用IA32指代基于intel的机器上运行传统32位linux版本时,硬件和GCC代码的组合。

用x86-64指代在AMD和Intel的较新的64位机器上运行的硬件和代码组合。

用linux和GCC的话说,这两个平台分别称为i386和x86-64。

Intel和AMD提供的新硬件和以这些硬件为目标的GCC新版本的组合,使得x86-64代码和IA32机器生成的代码有极大的不同。

  • 指针和长整数是64位长
  • 通用目的寄存器从8个扩展到16个
  • 许多程序状态都保存在寄存器中,而不是栈上。整形和指针类型的过程参数(最多6个)通过寄存器传递,有些过程根本不需要访问栈。
  • 如果可能,条件操作用条件传送指令实现,会得到比传统分支代码更好的性能。
  • 浮点操作用面向寄存器的指令集(SSE)来实现,而不用IA32支持的基于栈的方法来实现。

(细化的讲述x86-64的不同,暂时不看了)

3.14 浮点程序的机器级表示

我们把浮点存储模型,指令和传递规则的组合称为机器的浮点体系结构。x86的历史中有多种浮点体系结构,目前有两种还在使用:x87和SSE。

3.15 小结

(OVER)

posted @ 2012-05-01 16:32  ray hill  阅读(1131)  评论(0编辑  收藏  举报