计算机系统——黑皮书学习(二)

二、程序结构和执行

了解程序和操作系统之间的交互关系

第2章 信息的表示和处理

计算机将信息编码为位(比特),通常组织成字节序列。有不同的编码方式用来表示整数、实数和字符串。不同的计算机模型在编码数字和多字节数据中的字节顺序时使用不同的约定。

二值信号能够很容易地被表示、存储和传输

当把位组合在一起,再加上某种解释(inter-pretation),即赋予不同的可能位模式以含意,我们就能够表示任何有限集合的元素。

三种数字表示:无符号,补码,浮点数

2.1 信息存储

image-20220309084922082

**程序对象:**程序数据,指令,控制信息

程序本身是一个字节序列

指针是C语言的一个重要特性。它提供了引用数据结构(包括数组)的元素的机制。与变量类似,指针也有两个方面:值和类型。它的值表示某个对象的位置,而它的类型表示那个位置上所存储对象的类型(比如整数或者浮点数)。

每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。

可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。

在几乎所有的机器上**,多字节对象都被存储为连续的字节序列,**对象的地址为所使用字节中最小的地址。例如,假设一个类型为int的变量x的地址

排列表示一个对象的字节有两个通用的规则,最低有效字节再最前面为小端法,最高在前为大端法。

在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。

2.2整数表示

C语言的设计可以包容多种不同字长和数字编码的实现。64位字长的机器逐渐普及,并正在取代统治市场长达30多年的32位机器。由于64位机器也可以运行为32位机器编译的程序,我们的重点就放在区分32位和64位程序,而不是机器本身。64位程序的优势是可以突破32位程序具有的4GB地址限制。大多数机器对整数使用补码编码,而对浮点数使用IEEE标准754编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于想使编写的程序能在全部数值范围上正确运算的程序员来说,是很重要的。

在相同长度的无符号和有符号整数之间进行强制类型转换时,大多数C语言实现遵循的原则是**底层的位模式不变。**在补码机器上,对于一个w位的值,这种行为是由函数T2U。和U2T。来描述的。C语言隐式的强制类型转换会出现许多程序员无法预计的结果,常常导致程序错误。

由于编码的长度有限,与传统整数和实数运算相比,计算机运算具有非常不同的属性。当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近于0.0,从而转换成零时,也会下溢

C语言支持所有整型数据类型的有符号和无符号运算。尽管C语言标准没有指定有符号数要采用某种表示,但是几乎所有的机器都使用补码。通常,大多数数字都默认为是有符号的。

Java只支持有符号整数,并且要求以补码运算来实现。正常的右移运算符**>>被定义为执行算术右移。特殊的运算符>>>**被指定为执行逻辑右移。

2.3整数运算

和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比,有一些特殊的属性。例如,由于溢出,表达式x*x能够得出负数。但是,无符号数和补码的运算都满足整数运算的许多其他属性,包括结合律、交换律和分配律。这就允许编译器做很多的优化。

由于整数乘法比移位和加法的代价要大得多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。例如,假设一个程序包含表达式x * 14。利用14=23+22+2,编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1),将一个乘法替换为三个移位和两个加法。无论x是无符号的还是补码,甚至当乘法会导致溢出时,两个计算都会得到一样的结果。(根据整数运算的属性可以证明这一点。)更好的是,编译器还可以利用属性14=2^4-2,将乘法重写为(x<<4)-(x<<1),这时只需要两个移位和一个减法。

在大多数机器上,整数除法要比整数乘法更慢,整数除法总是舍入到0

2.4浮点数

浮点表示通过将数字编码为r×2”的形式来近似地表示实数。最常见的浮点表示方式是由IEEE标准754定义的。它提供了几种不同的精度,最常见的是单精度(32位)和双精度(64位)。IEEE浮点也能够表示特殊值+oo、-oo和NaN。
必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且并不遵守普遍的算术属性.比加结合性

第3章 程序的机器级表示

在本章中,我们窥视了C语言提供的抽象层下面的东西,以了解机器级编程。通过让编译器产生机器级程序的汇编代码表示,我们了解了编译器和它的优化能力,以及机器、数据类型和指令集。

当编写能有效映射到机器上的程序时,了解编译器的特性会有所帮助。我们还更完整地了解了程序如何将数据存储在不同的内存区域中。

应用程序员需要知道一个程序变量是在运行时栈中,是在某个动态分配的数据结构中,还是全局程序数据的一部分。理解程序如何映射到机器上,会让理解这些存储类型之间的区别容易一些。

3.1历史观点

Intel处理器系列俗称x86

**摩尔定律:**晶体管每26个月翻一番

3.2程序编码

命令gcc指的就是 GCC C编译器。因为这是 Linux 上默认的编译器,我们也可以简单地用cc来启动它。编译选项-og告诉编译器使用会生成符合原始C代码整体结构的机器代码的优化等级。

机器级编程两种抽象:

一是由指令集体系结构或指令集架构(Instruction Set Architecture,ISA)来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。

大多数ISA,包括x86-64,将程序的行为描述成好像每条指令都是按顺序执行的,一条指令结束后,下一条再开始。处理器的硬件远比描述的精细复杂,它们并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行的行为完全一致。

第二种抽象是,机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。

在整个编译过程中,编译器会完成大部分的工作,将把用C语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比汇编代码的主要特点是它用可读性更好的文本格式表示。

机器级程序和它们的汇编代码表示,与C程序的差别很大。

  • 程序计数器(通常称为“PC”,在x86-64中用%rip表示)给出将要执行的下一条指令在内存中的地址。
  • 整数寄存器文件包含16个命名的位置,分别存储64位的值。这些寄存器可以存储地址(对应于C语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其他的寄存器用来保存临时数据,例如过程的参数和局部变量,以及函数的返回值。
  • 条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如说用来实现if和 while语句。
  • 一组向量寄存器可以存放一个或多个整数或浮点数值。

反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。它不需要访问该程序的源代码或汇编代码。

3.3数据格式

由于是从16位体系结构扩展成32位的,Intel用术语“字(word)”表示16位数据类型。因此,称32位数为“双字(double words)”,称64位数为“四字(quad words)”。图3-1给出了C语言基本数据类型对应的x86-64表示。标准in
t值存储为双字(32位)。

大多数GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小。例如,数据传送指令有四个变种: **movb(传送字节)、movw(传送字)、movl(传送双字)和movq(传送四字)。**后缀‘1’用来表示双字,因为32位数被看成是“长字(longword)”。注意,汇编代码也使用后缀‘1’来表示4字节整数和8字节双精度浮点数。这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器

各种数据类型之间的差别很小。程序是以指令序列来表示的,每条指令都完成一个单独的操作。

3.4访问信息

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。x86-64支持多种操作数格式。

源数据值可以以常数形式给出,或是从寄存器或内存中读出。结果可以存放在寄存器或内存中。因此,各种不同的操作数的可能性被分为三种类型。立即数:用来表示常数值。**寄存器(**register),它表示某个寄存器的内容。内存引用,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。
在这里插入图片描述

最简单形式的数据传输指令——MOV类。这些指令把数据从源位置复制到目的位置,不做任何变化。MOV类由四条指令组成:movb、movw、movl和movq。这些指令都执行同样的操作;主要区别在于它们操作的数据大小不同:分别是1、2、4和8字节。

源操作数指定的值是一个立即数,存储在寄存器中或者内存中。目的操作数指定一个位置,要么是一个寄存器或者,要么是一个内存地址。

3.5算术和逻辑操作

在这里插入图片描述

加载有效地址(load effective address)指令 leaq实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读人数据,而是将有效地址写人到目的操作数。

3.6控制

处理器通过使用流水线(pipelining)来获得高性能,在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分。这种方法通过重叠连续指令的步骤来获得高性能,例如,在取一条指令的良时,这行它前面一条指令的算术运算。

源和目的的值可以是16位、32位或64位长。不支持单字节的条件传送。无条件指令的操作数的长度显式地编码在指令名中(例如 movw和movl),汇编器可以从目标寄存器的名字推断:出条件传送指令的操作数长度,所以对所有的操作数长度,都可以使用同一个的指令名字。

同条件跳转不同,处理器无需预测测试的结果就可以执行条件传送。

汇编中没有循环的指令,可以用条件测试和跳转组合起来实现循环的效果。

3.7过程

过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。

栈,递归

3.8数组分配和访问

C语言中的数组是一种将标量数据聚集成更大数据类型的方式。C语言实现数组的方式非常简单,因此很容易翻译成机器代码。C语言的一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算。
优化编译器非常善于简化数组索引所使用的地址计算。

3.9异质的数据结构

C语言提供了两种将不同类型的对象组合到一起创建数据类型的机制:结构(struc-ture),用关键字struct来声明,将多个对象集合到一个单位中;联合(union),用关键字union来声明,允许用几种不同的类型来引用一个对象。

3.10浮点代码

处理器的浮点体系结构包括多个方面,会影响对浮点数据操作的程序如何被映射到机器上,包括:

  • 如何存储和访问浮点数值。通常是通过某种寄存器方式来完成。
  • 对浮点数据操作的指令。
  • 向函数传递浮点数参数和从函数返回浮点数结果的规则。
  • 函数调用过程中保存寄存器的规则—一例如,一些寄存器被指定为调用者保存,而其他的被指定为被调用者保存。

C++的对象用结构来表示,类似于C的struct。C++的方法是用指向实现方法的代码的指针来表示的。相比而言,Java的实现方式完全不同。Java的目标代码是一种特殊的二进制表示,称为Java字节代码。这种代码可以看成是虚拟机的机器级程序。正如它的名字暗示的那样,这种机器并不是直接用硬件实现的,而是用软件解释器处理字节代码,模拟虚拟机的行为。另外,有一种称为及时编译(just-in-time compilation)的方法,动态地将字节代码序列翻译成机器指令。当代码要执行多次时(例如在循环中),这种方法执行起来更快。用字节代码作为程序的低级表示,优点是相同的代码可以在许多不同的机器上执行。

未完。。。🤐

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值