《深入理解计算机系统》(二)

字:每台计算机都有一个字长(word  size),指明证书和指针数据的标称大小(nominal  size)。因为虚拟地址是以这样的字来编程的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为n的机器而言,虚拟虚拟地址的范围为(0~2^n)-1,程序最多访问2^n字节。

今天大多数计算机的字长都是32位。这就限制了虚拟地址空间为4千兆字节(写作4GB),也就是说,刚刚超过4x10^9字节。现在随着许多大型科学和数据库应用需要更大的存储器,因此字长为64位的高端机器逐渐变得普遍起来。

数据大小:计算机和编译器使用不同的方式来编码数字,比如不同长度的整数和浮点数,从而支持多种数字格式,比如,许多机器都有处理单个字节的指令,也有处理表示为两个字节、四个字节或者八个字节整数的指令,还有些指令支持表示为四字节和八字节的浮点数。

C语言支持整数和浮点数的多种数据格式。C的数据类型char表示一个单独的字节,尽管“char”这个名字是由于它被用来存储文本串中的单个字符这一事实而来的,但他也能被用来存储整数值。C的数据类型int之前还加上限定词long和short,提供各种大小的整数表示。准确的字节数依赖于机器和编译器,例如:大多数32位机器和Compaq  Alpha  (针对高端应用的64位机器)。大多32位机器使用“典型”的分配方式。可以观察到,“短”整数分配有两个字节,而不加限制的int为四字节,

“长”为整数使用机器的全字长

指针使用机器的全字长,大多数机器还支持两种不同的浮点格式:单精度(C中声明为float)和双精度(C中声明为double)这些格式 分别使用四字节和八字节。

分配的字节数随着机器和编译器的不同而不同

注意:给C的初学者;对于任何数据类型T,声明T *P;

表明p是一个指针变量,指向类型T的一个对象例如:char *p;就将一个指针声明为指向char类型的一个对象。

程序员应该力图使他们的程序在不同的机器和编译器上可移植。可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感

对于跨越多字节的程序对象,我们必须建立两个原则1、这个对象的地址是什么?2、我们在存储器中如何对这些字节排序?在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象地址为所使用字节序列中最小的地址。例如:假设一个类型为int的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100,那么x的四个字节将被存储在存储器的0x100、0x101、0x102、0x103

对表示一个对象的字节序列排序,有两个通用原则1、大端法(big  endian)最高有效字节在最前面2、小端法:最低有效字节在最前面

      对于大多数应用程序员来说,他们机器的字节顺序是完全不可见的,无论哪种类型的机器所编译的程序都会得到同样的结果。不过有时候,1、首先在不同类型的机器之间通过网络传送二进制数据时,一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反之时,接收程序会发现,字里的字节成了反序的,所以为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。

2、字节顺序变得重要的第二种情况是当阅读表示整数数据的字节序列时

反汇编器是一种确定可执行程序文件所表示的指令序列的工具。

3、当编写规避正常的类型系统的程序时。在C语言中,可以通过使用强制类型转换来允许以一种不同于它没创造时的数据类型来引用一个对象。但大多数编程都不推荐这种编码技巧。

给C语言初学者:使用typedef来命名数据类型

    C中的typedef生命提供了一种给数据类型命名的方式能够极大的改善代码的可读性,因为深度嵌套的类型声明很难读懂,typedef的语法与声明变量十分相像,除了他使用的是类型名,而不是变量名。

例如:声明

typedef    int    *  int_pointer;

int_pointer   ip;

将类型“int_pointer”定义为一个指向int的指针,并且声明了这种类型的变量ip,我们还可以直接声明这个变量为int   *ip;

printf函数(还有同类fprintf和sprintf)提供了对格式化细节有相当大控制的输出信息的方式,第一个参数是格式串,其余的参数都是要打印的值,在格式串里,每个以“%”开始的字符序列都表示如何格式化下一个参数,例如:“%d”是输出一个十进制整数,“%f”是输出一个浮点数,而“%C”是输出一个字符,这个字符的编码是由参数给出的。

强制类型转换运算符是一种数据类型转换为另一种。

这些过程使用c的运算符sizeof来确定对象使用的字节数,例如:表达式sizeof(T)返回存储一个类型为T的对象所需要的字节数。。使用sizeof,而不是一个固定的值,是向编写在不同机器类型上可移植的代码买进了一步。

我们的参数12345的十六进制数表示为0x00003039,对于int类型的数据,除了字节顺序外,我们在所有的机器上都得到相同的结果。特别的,我们可以看到在linux、NT和Alpha上,最低有效字节0x39最先输出,这说明它们是小端法机器,而在sun 上最后输出,这说明sun是大端法机器。同样的,float类型的数据,除了字节顺序以外,也是相同的。另一方面指针值却完全不同的,不同的机器/操作系统配置使用不同的存储分配原则,一个值得注意的是linux和sun的机器使用四字节地址,而Alpha使用把自己饿地址。

     尽管浮点和整型数据都是对数值12345编码,但是他们有非常不同的字节模式,整形为0x000033039而浮点数为0x4640E400.一般而言两种格式使用不同的编码方法。

      c中的字符串被编码为一个以null(其值为零)字符结尾的字符数组,每个字符都有某个标准编码来表示,最常见的为ASCII字符码。在使用ASCII码作为字符码的任何系统上都得到相同的结果,与子皆顺序和字大小规则无关。因此,文本数据比二进制数据具有更强的平台独立性。

      生成一张ASCII表:可以通过执行命令main   ascii  来得到一张ASCII字符码的表。

      ASCII字符集适合干编码英文文档,但是他在表达一些特殊字符方面并没有太多办法。

最近16位的unicode字符集被采纳用来支持所有语言的文档,这种双字节字符表示使得大量不同字符的表示变为可能。JAVA编程语言使用Unicode来表示字符串。

NT和Linux机器使用的都是Intel处理器,因此支持相同的机器级指令,但一般而言,一个可执行的NT程序和一个Linux程序的结构式不同的,因此这些机器并不完全是二进制兼容的,二进制代码很少能在不同机器和操作系统组合之间的移植。

计算机系统的一个基本概念就是机器的角度来看,程序仅仅只是字节序列。

因为二进制值是计算机编码、存储和操作信息的核心,所以围绕数值0和1已经演化出了丰富的数学知识体系。布尔观察到通过将二进制值1和0编码为逻辑值TRUE(真)和FALSE(假),能够设计出一种代数,研究命题逻辑的属性。称为布尔代数。

    位向量就是某个固定长度为W的0或1的串。它的一个有用的应用就是表示有限集合。

C的一个很有用的特性就是它支持按位布尔运算。实际上我们在布尔运算中使用的哪些符号就是在C中使用的;II就是OR,&就是AND,!就是NOT,而^就是EXCLUSIVE-OR。这些运算能运用到任何“整数”的数据类型上,也就是那些声明为char或者int的数据类型,无论有没有像long、short或者unsigned这样的限定词。

     确定一个位级表达式的结果的最好方法就是讲十六进制参数扩展成他们的二进制运算,然后再转回十六进制。

  位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的一个组位。

一、逻辑运算很容易和位级运算相混淆,但是他们的功能是完全不同的,逻辑运算认为所有非0参数都表示TRUE,而参数0表示FALSE,他们返回1或0,分别表示结果为TRUE或者为FALSE。按位运算只有在特殊情况下,也就是参数被限制为0或者1时,才能与其对应的逻辑运算有相同的行为。

二、逻辑运算符和他们对应的位级运算符之间第二个重要区别,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算就不会对第二个参数求值。

C还提供了一系列移位运算,以向左向右移动位模式。x<<k会生成一个值。【Xn-1,Xn-2,.....,Xo]变为[Xn-k-1,Xn-k-2,.....,Xo-k,0.....0],也就是说,x向左移动k位,丢弃K个最高位,并在右端补了k个0。 但是应该注意运算符的优先级:1<<5-1应该是1<<(5-1)而不是(1<<5)-1

还有一个相应的右移的运算x>>k,一般而言机器支持两种形式的右移,逻辑的和算术的。逻辑的在左端补k个0,算术右移是在左端补K个最高有效位的拷贝。如加上Xn-1......Xn-1

    C标准没有明确定义应该使用哪种类型的右移,对于无符号数据(以限定词unsigned声明的整型对象),右移必须是逻辑的。对于有符号数据的,算数的或逻辑的都可以。事实上,几乎所有的编译器/机器组合都对有符号数据使用算术右移,且许多程序员也都假设使用这种右移。

C支持多种整型数据类型——表示有限范围的整数。

C、C++、JAVA中,C、C++都支持有符号(默认)和无符号的。JAVA只支持有符号数。

C的标准并没有要求要用二进制补码形式来表示有符号整数,但是几乎所有的机器都是这么做的。

有符号数另外还有两种标准的表示方法:1、二进制反码  2、符号数值   这两种方法都有一个古怪的属性,就是对于数字0有两种不同的编码方式,[00......0]都被解释为+0.而值-0在符号量形式中表示为[10...0],而在二进制反码中表示为[11...1].虽然过去生产过基于二进制反码表示的机器,但是几乎所有的现代机器都使用二进制补码。我们将看到符号数值编码方式使用在浮点数中。

C允许无符号和有符号之间的转换,原则是基本的位表示保持不变。


当C执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C会隐含地将有符号参数强制类型转化为无符号数,并假设这两个数都是非负的,来执行这个运算。

一个常见的运算时在不同字长的整数之间转换,同时又保持数值不变。当目标类型太小了,以至于不能表示想要的值时,这可能根本就是不可能的。一个较小的数据转换到一个较大的类型,应该总是可能的。将要一个无符号数据转换为一个更大的数据类型,我们只要简单地在表示的开头添加0.这种运算被称为零扩展。要将一个二进制补吗转换为一个更大的数据类型,规则是执行一个符号扩展,在表示中添加最高有效位的值。

截断数字:

假设不用额外的位来扩展一个数值,我们会减少表示一个数字的位数。

为16位的short int,当我们把它强制类型转换回int时,符号扩展把高16位设置为1

有符号数到无符号数的隐式强制类型转换为导致了某些与直觉不相符的行为,而这些与直觉不相符的特性经常导致程序错误,并且包含隐式强制类型转换的细微差别的错误很难被发现,因为这种强制类型转换时看不到的,我们经常忽视了它的存在。

事实上,除了c以外很少有语言支持无符号整数,比如:JAVA只支持有符号整数,并且要求以二进制补码运算来实现的,正常的右移运算符>>被定义为执行算术右移,特殊的运算符>>被指定为执行逻辑右移。

   当我们想,要把字仅仅看做是位的集合而没有任何数字意义时,无符号数值是非常有用的。例如:1、当往一个字中放入布尔条件的标记(flag)时。地址自然是无符号的,所以程序员会发现无符号类型是很有帮助的。

2、当实现模运算和多精度运算的数学包,数字是由字的数组来表示的,无符号值也非常有用。

如果 我们保持和为一个w+1位的数字,并且把他加上另外一个数值,我们可能需要w+2个位,以此类推,这种“字长膨胀”意味着,想要完整地表示算术运算的结果,我们不能对字长做任何限制。一些编程语言,例如LISP,实际上就是支持无限精度的运算,允许任意的(要在机器的存储器限制之内)整数运算。更常见的是,编程语言支持固定精度的运算。

说一个算术运算溢出来了,是指完整的整数结果不能放到数据类型的字长限制中去。当执行C程序时,不会将溢出作为错误而发出信号,不过有时候我们可能希望判定是否发生了溢出。

模数加法形成了一种数学结构,成为阿贝尔群

    两个数的W位二进制补码之和与无符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器指令来执行无符号或者有符号加法。

一种有名的用来执行位级二进制补码的非得技术是,对每个位取反(或取补),然后将结果加1.在c中可以写成x+1.




舍入:当一个数字不能被准确的表示为这种格式,因此必须被向上调整或者向下调整时,就会出现舍入。

理解浮点数的第一步是考虑含有小数值的二进制数字。



IEEE浮点表示:不能很有效地表示非常大的数字。

非格式化数有两个目的,首先,他们提供了一种表示数值0的方法,因为使用规格化数,我们必须总是M大于等于1.因此我们不能表示0.



的结果,在实际中,浮点单元的设计者使用一些聪明的小技巧来避免执行这种精确的计算,因为运算只要精确到能够保证得到一个正确舍入的结果就可以啦。

IEEE标准中指定浮点运算行为的方法的一个优点在于,它可以独立于任何具体的硬件或者软件实现。

       浮点加法不具有结合性,这是缺少的最重要的群属性。对于科学程序员和编译器编写者来说着具有重要的意义。


  C语言中的浮点:C提供了两种不同的浮点数据类型;float和double。在支持IEEE浮点格式的机器上,这些数据类型就对应于但进度和双精度浮点。另外这类机器使用向偶数舍入的舍入方式。不行的是,因为C标准不要求机器使用IEEE浮点,所以没有标准的方法来改变舍入方式或者得到诸如-0、正无穷、负无穷或者NaN之类的特殊值。


  Intel   IA32处理器,这种处理器大量地应用于今天的个人计算机中,这种机器有一个特性,用GCC编译的时候,它能够严重影响程序对浮点数运算的行为。






小结:






Intel处理器的发展过程:







每个时间上相继的处理器设计都是向后兼容的,也就是,较早版本上编译的代码是可以再较新的处理器上运行的。











     我们写一个c代码文件,然后在命令行上使用“-s”就能看到c编译器产生的汇编代码:

unix>gcc  -02  -s  code.c

这会使汇编器产生一个汇编文件 code.c但是不做其他进一步的工作,(通常状况下,它还会调用汇编器产生目标代码文件)命令gcc表明就是GNU  C编译器GCC,因为这是LINUX上默认的编译器,我们也可以简单地用CC来启动它。


如果我们使用“-c”命令行选项,GCC会编译并汇编该代码。

UNIX>gcc  -02   -c    code.c

就会产生目标代码文件code.o,它是二进制格式的,所以无法直接读。

   如何找到程序的字节表示?

首先,我们用反汇编器来确定函数sum的代码长是19字节,然后我们在文件code.o上运行GNU调试工具,并输入命令:(gdb)   x/19xb  sum

这条命令告诉我们GDB检查(简写为“x”)19个十六进制格式(也简写为“x”)的字节(简写为“b”)

    要查看目标代码文件的内容,有一类称为反汇编器的程序的价值无法估量。这些程序根据目标代码生成一种类似于汇编代码的格式。


GCC产生的汇编代码有点难读,它包含一些我们不需要关心的信息,另外,它不提供任何程序的描述或它是如何工作的描述。

      每一行的左边都有编号供引用,右边是注释,简单地描述指令的效果以及他与原始C代码中的计算操作的关系,这是一种汇编语言程序员写代码的风格。


注意:人大多数常用数据类型都是作为双字存储的,其中,包括普通整数和长整数,无论他们是否带有符号。

此外所有指针都是4字节双字。处理字符串数据时,通常用到字节。浮点数有三种形式:单精度(4字节)值,对应于c数据类型float;双精度(8字节)值,对应于c数据类型double;和扩展精度(10字节)值。GCC用数据类型long  double来表示扩展精度的浮点值。,为了提高存储器的性能,他将这样的浮点数存储成12字节数。虽然ANSIC标准包括long  double 数据类型,但是对大多数编译器和机器组合来说,它的实现和普通double格式是一样的。对GCC和LA32的组合来说,支持扩展精度是很少见的。


  GAS中的每个操作都有一个字符后缀,表明操作数大小。例如:mov(传送数据)其指令有三种形式    movb(传送字节)    movw(传送字)    movl(传送双字)后缀“l”用来表示双字,因为在许多机器上,32位数都称为“长字(long  word)”,这是沿用以16位字为标准的时代的习惯造成的。注意:GAS使用后缀“1”来同时表示4字节的整数和8字节的双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

  一个IA32中央处理器(CPU)包含一组八个存储32位值的寄存器,这些寄存器用来存储整数数据和指针。

操作数指示符:大多数指令有一个或多个操作数,指示出执行一个操作中要引用的源数据值,以及放置结果的目的位置。IA32支持多种操作数格式。源数据值可以以常数的形式给出,或是从寄存器或存储器中读出,结果可以存放在寄存器或存储器中。因此各种操作数的可能性被分为三种类型。第一种是立即数。也就是常数值。在GAS中采用标准C的表示方法,立即数的书写方式为“$”后面跟一个整数,比如,$-577或$0x1f。任何32位的字都可以叫做立即数,不过汇编器在可能时会使用一个或两个字节的编码。第二种类型是寄存器,他表示某个寄存器的内容,对双字操作来说,可以是八个32位寄存器中的一个。对字节操作来说可以是八个单字节寄存器元素中的一个。a     .


  数据传送指令:最频繁使用的指令是执行数据传送的指令。操作数符号的通用性使得一条简单的传送指令能够完成许多机器中要好几条指令才能完后才能完成的功能。最常用的是传送双字的movl指令。源操作数指定一个值,它可以使立即数,可以存放在寄存器中,也可以存放在存储器中,目的操作数指定一个位置,它可以使寄存器,也可以是存储器地址。IA32加了一条限制,传送指令的两个操作数不能都指向存储器位置,将一个值从一个存储器位置拷到另一个存储器位置需要两条指令-----第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的位置。类似地movb传送一个字节,movw指令传送两个字节。




双字整数操作分为四类,二元操作有两个操作数,而一元操作只有一个操作数。


一元操作,只有一个操作数,既做源,也做目的。这个操作数可以是一个寄存器,也可以是一个存储器位置。

二元操作,源操作数是第一个,目的操作数是第二个,这是不可交换操作特有的。

移位操作:先给出移位量,然后是待移位的值。可以进行算术和逻辑右移。移位量用单个字节编码,因为只允许进行0到31位的移位。移位量可以使一个立即数,或者放在单字节寄存器元素中。    

除了右移操作,所有的指令都不区分有符号和无符号操作数,对列出的所有指令来说,二进制补码运算和无符号运算有同样的位移行为。

程序执行除了访问数据和操作数据的方法,另一个重要的部分就是控制操作的顺序。对C和汇编代码中的语句,默认的方式是顺序的控制流,按照语句或指令在程序中出现的顺序来执行。C中的某些程序结构,比如条件语句、循环语句、分支语句,允许控制按照非顺序方式进行,即根据程序数据的值来确定顺序。

        




访问条件码:两种最常用的访问条件码的方法不是直接读取他们,而是根据条件码的某个组合,设置一个整数寄存器或是执行一条件分支指令。


  跳转指令和他们的编码:在正常执行的情况下。指令按照他们出现的顺序一条一条的执行。跳转指令会导致执行切换到程序中一个全新的位置。这些跳转的目的通常用一个标号(label)指明。
















评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值