源程序实际上就是一个由值0和1组成的位(比特)序列,8个位被组织成一组,称为字节,每个字节就表示程序中的某些文本字符。用一个唯一的单字节大小的整数值表示每个字符。下图给出了hello.c程序的ASCII码表示:
系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网上传送的数据,都是用一串比特表示的。
GCC将一个.c程序编译成一个可执行目标文件.o,这个翻译过程可分为四个阶段:
初始时,shell程序执行它的命令,等待我们输入一个命令,当我们再键盘上输入字符串"./hello"后,shell程序将字符串逐一读入寄存器,再把它放到内存中。
当我们在键盘上敲击回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行文件hello,这些指令将hello目标文件中的代码和数据从磁盘复制到主存,数据包括最终被输出的字符串“hello world\n”。
上述示例揭示了一个重要问题,就是系统花费了大量的时间把信息从一个地方复制到另外一个地方。这些复制就是开销,减慢程序真正的工作。
从磁盘驱动器上读取一个字的开销要比主存中读取开销大10000000倍,而处理器从寄存器中读取数据比从主存中读取要快100倍。差距仍在拉大。加快处理器的运行速度比加快主存的运行速度要容易个便宜的多。
存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。
把操作系统看成是应用程序与硬件之间插入的一层软件。
操作系统的基本功能:
1、放置硬件被失控的应用程序滥用
2、向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。通过基本的抽象概念(进程、虚拟内存和文件)来实现这些功能。
一个进程实际上可以由多个线程的执行单元组成,每个线程都在运行进程的上下文中,共享同样的代码和全局数据。
多线程之间比多进程之间更容易共享数据,线程一般来说都比进程更高效,当有多处理器的时候,多线程也是一种使得程序可以运行更快的方法。
抽象概念。为每一个进程提供了一个假象,即每个进程都在独占的使用主存,每个进程看到的内存都是一致的,称为虚拟内存空间。
虚拟内存的基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
网络可以将多态设备连接到一起,从一个单独的设备看,网络也是一个I/O设备。
使用DMA直接存储器存取,数据直接从磁盘到主存。
图所示:
系统是硬件和系统软件互相交织的集合体,共同协作达到运行应用程序的最终目的。
当我们对系统的某个部分加速时,其对整个系统的性能的影响去决定于该部分的重要性和加速程度。虽然我们对系统的一个主要部分做出了重大改进,但是获得的系统加速却明显小于这部分的加速比。这就是Amdahl定律的主要观点——想显著加速整个系统,必须提升全系统中相当大的部分组件的速度。
并发:指一个同时具有多个活动的系统;
并行:用并发来使一个系统运行更快。在多个抽象层次上运用
多处理器的使用可以从两方面提高系统性能。首先它减少了在执行多个任务时模拟并发的需要。其次,可以使应用程序运行的更快。找到书写应用程序的方法利用硬件开发线程级并行性。
指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。早期的微处理器,需要多个始终周期来执行一条指令。最近的处理器可以保持每个时钟周期2-4条指令的执行效率。在流水线中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。这些阶段可以并行地操作,用来处理不同指令的不同部分。
如果处理器可以达到比一周期一条指令更快的执行效率,就称之为超标量super-scalar处理器。大多数现代处理器都支持超标量操作。
单指令、多数据并行
特殊的硬件允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。较新的基带处理器都具有并性地对8对单精度浮点数(C数据类型float)做加法的指令。
抽象的使用是计算机科学中最为重要的概念之一。在处理器里,指令级架构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得就好像运行在一次只执行一条指令的处理器上。底层的硬件远比抽象描述的要复杂精细,并行地执行多条指令,但又总是与简单有序的模型保持一致。
文件 :I/O系统的的抽象
虚拟内存:对程序存储器(主存和磁盘)的抽象
进程:对一个正在运行的程序的抽象
虚拟机:对整个计算机的抽象(程序、主存和I/O设备)
本书这一步分引领读者深入了解如何表示和执行应用程序。帮助写出安全、可靠充分利用计算资源的程序。
现代计算寄存储和处理的信息以二值信号表示。但个位不是非常有用,但是把位组合在一起再加上某种解释,即赋予不同的可能位模式以含义,就能够表示任何有限集合的元素。
研究三种最重要的数字表示:
无符号编码基于传统的二进制表示法,表示大于或者等于0的数字
补码编码是表示有符号整数的最常见的方式,有符号整数就是可以为正负的数字。
浮点数编码是表示实数的科学计数法的以2为基数的版本。
计算机的表示法是用有限数量的位来对一个数字编码,因此方结果太大以致不能表示时。某些运算就会溢出。从而导致令人吃惊的结果。
虽然溢出会产生特殊的值,但是一组正数的乘积总是正的。由于精度有限,浮点运算是不可结合的,在大多数机器上,C
点数虽然可以编码一个较大的数值范围,但是这种表示只是近似的。
8位的块,字节,作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能的地址集合就称为虚拟地址空间。
简称hex,使用数字0-9和字母A-F表示16个值。
每台计算机都有一个字长,指明指针数据的标称大小,虚拟地址是以一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。一个字长为w位的机器而言,虚拟地址的范围为 0 − ( 2 w − 1 ) 0-(2^w-1) 0−(2w−1),程序最多访问 2 w 2^w 2w个字节。
C的数据类型char表示一个单独的字节,尽管char是由于它被用来存储文本串中的单个字符这一事实而得名,但它也能被用来存储整数值。为了避免由于依赖典型大小和不同编译器设置带来的奇怪行为,ISO C99引入了一种数据类型,其数据大小是固定的,不随编译器和机器设置而变化,其中就有数据类型int32_t和int64_t,分别为4个字节和8个字节。
对于跨越多字节的程序对象,必须建立两个规则:**这个对象的地址是什么,以及在内存中如何排列这些字节。**在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器按照从最高有效字节到最低有效字节的顺序存储。前一种规则——最低有效字节在最前面的方式,称为小端法;后一种规则——最高有效字节在最前面的方式,称为大端法。
注意:在字0x01234567中,高位字节的十六进制为0x01,而低位字节值为0x67。
选择何种字节顺序没有技术上的理由,因此争论沦为关于社会政治论题的争论,只要选择了一种规则并始终如一的坚持,对于哪种字节排序的选择都是任意的。
一般来说sizeof(T)返回存储一个类型为的对象所需要的字节数。使用sizeof而不是一个固定的值,是编写在不同机器类型上可移植代码迈进了一步。
监管浮点数和整型数据都是对数值12345编码,但是有截然不同的字节模式,整形为0x00003039,而浮点数为0x4640E400。一般而言,这两种格式使用不同的编码方法。
在C语言中,我们能够用数组表示法来引用指针,同时我们也可以用指针表示法来引用数组元素。
可以通过执行命令man ascii来得到一张ASCII字符码的表。
C语言中字符串被编码为一个以null字符为结尾的字符数组。每个字符由某种标准编码表示,最常见的是ASCII。使用ASCII作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。
相同代码在不同的机器上进行编译生成的字节表示的机器代码是不同的,不同的机器类型使用不同的且不兼容的指令和编码方式。
二进制代码是不兼容的,很少能在不同机器和操作系统组合之间移植。
从机器的角度看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息。
位级运算的一个常见用法就是实现掩码运算,这里的掩码是一个位模式,表示从一个字中选出的位的集合。
逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。
C语言标准定义了每种数据类型必须能够表示的最小的取值范围。
C和C++都支持有符号数和无符号数,java只支持有符号数。
无符号数的二进制表示有一个很重要的属性,也就是每个介于0~2^w-1之间的数都有唯一一个w位的值编码。
无符号数编码的唯一性。
有符号数的计算机表示方式就是补码形式。在这个定义中,将字的最高有效位解释为负权。同无符号表示一样,在可表示的取值范围内的每个数字都有一个唯一的w位的补码编码。
关于整型数据的取值范围和表示,Java标准是非常明确的,要求采用补码表示。在Java中,单字节的数据类型称为byte,而不是char。这些具体要求为了保证无论在什么机器上运行,都能表现完全一样。
C语言允许在各种不同的数字类型之间做强制类型转换。对于大多数C语言的实现,处理同样字长的 有符号数和无符号数之间相互转换的一半规则是:数值可能会改变,但是位模式不会改变。
通常,大多数数字都默认为有符号的,要创建一个无符号常量,必须加上后缀‘U’或者’u’,例如12345U。
要将一个无符号数转换为一个更大的数据类型,只要简单地在表示的开头添加0.这种运算被称为零扩展。
两个正数相加会得出一个负数,比较表达式x 由于编码的长度有限,与传统整数和实数运算相比,计算机运算具有非常不同的属性,当超出标识范围时,有限长度能有引起数值溢出,当浮点数非常接近于0.0时,从而转换为零时,也会下溢。 计算机执行机器代码,用字节序列编码低级的操作,包括数据处理,管理内存,读写存储设备上的数据,以及利用网络通信。 高级语言提供的抽象级别比较高,大多数时候,这种抽象级别上的工作效率会更高,也更可靠。通常情况下,使用现代优化编译器产生的代码至少与一个熟练的汇编语言程序员手工编写的代码一样有效。最大的优点是,高级语言编写的程序可以在不同的机器上编译执行,而汇编代码则是与特定机器密切相关的。 为什么要学习机器代码呢? 程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时要求程序员能够直接用汇编语言编写程序,现在则要求他们能够阅读和理解编译器产生的代码。 IA32机器语言。 Intel处理器系列有好几个名字,包括IA32,也就是Intel 32位体系结构,以及最新的Intel 64,即IA32的64位扩展,也称为x86_64。 1、由指令集体系结构或指令级架构ISA来定义机器级程序的格式和行为,它定义了处理器的状态、指令格式、以及每条指令对状态的影响。 编译器把C语言程序转化为处理器执行的非常基本的指令,汇编代码表示非常接近机器代码。与机器代码的二进制格式相比,汇编代码的主要特点是可读性更好的文本表示。机器代码将内存看作是一个很大的、按字节寻址的数组。程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回运行时栈,以及用户分配的内存块。操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。 一条机器指令只执行一个非常基本的操作。编译器产生这些指令的序列,从而实现像算术表达式求职、循环或过程调用和返回的程序结构。第三章 程序的机器级表示
GCC C语言编译器以汇编语言的形式产生输出,汇编代码是机器代码的文本表示,人类可读,给出程序中的每一条指令。然后GCC调用汇编器和链接器,根据汇编代码生成可执行的机器代码。
严谨的程序员能够阅读和理解汇编代码是一项很重要的技能。通过阅读编译器产生的汇编代码,我们能够理解编译器的优化能力,分析代码中隐含的低效率。
我们的表述集中于现代操作系统为目标,编译C或类似编程语言时,生成的机器程序类型。3.2 程序代码
linux> gcc -0g -o p pi.c
编译选项-og告诉编译器使用会生成符合原始C代码制衡体结构的机器代码的优化等级。3.2.1 机器级代码
2、机器程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组,存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。