deep in computer system 2 note

第二版
2011年4月第1版第4印刷
本书的主要论题包括:数据显示、C程序的机器级表示、处理器结构、程序优化、存储器层次结构、链接、异常控制流、虚拟存储器和存储器管理、系统级I/O、网络编程和并发编程。 


目录 
1,计算机系统漫游 
2,信息的表示和处理    -- 第一部分 程序结构和执行 
3,程序的机器级表示 
4,处理器体系结构 
5,优化程序性能 
6,存储器层次结构 
7,链接                -- 第二部分 在系统上运行程序 
8,异常控制流 
9,测量程序执行时间 
10,虚拟存储器 
11,系统级I/O          -- 第三部分 程序间的交互和通信 
12,网络编程 
13,并发编程 


0. 不能用x-y<0代替x<y,因为可能会发生溢出,也不能用-y<-x来代替,因为在二进制补码表示中负数和正数的范围是不对称的。
1. 文本文件与二进制文件
2. 预处理器,编译器,汇编器,链接器
让我们以一个hello程序为例: 
Java代码  
#include <stdio.h>  
  
int main()  
{  
  printf("hello, world\n");  
}  




1.1 信息就是位+上下文 
hello程序的生命周期是从源程序hello.c开始的 
该源程序实际上就是一个由0和1组成的位(比特)序列,每8个一组,称为字节 
大部分现在系统使用ASCII标准来表示文本字符 
hello.c程序是以字节序列的方式存储在文件中的,每个字节都有一个整数值来对应于某个字符 
如#是35,i是105,换行符\n是10 
hello.c这种由ASCII字符构成的文件称为文本文件,其他则称为二进制文件 


hello.c的表示方法说明了一个基本的思想:系统中所有的信息--包括磁盘文件、存储器中的程序、存储器中存储的用户数据以及网络上传输的数据,都是由一串比特表示的 
区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文,比如在不同的上下文中,同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令 


1.2 程序被其他程序翻译成不同的格式 
为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令 
这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来,目标程序也称为可执行目标文件 
Java代码  
unix> gcc -o hello hello.c  


这个过程分四个阶段,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)构成编译系统 


1,预处理阶段根据#开头的命令修改原始的C程序,#include <stdio.h>告诉预处理器读取系统头文件stdio.h的内容并插入到程序文本中,从而得到另一个C程序,通常以.i作为文件扩展名 
2,编译阶段将文本文件hello.i翻译成文本文件hello.s,它为一个汇编语言程序。汇编语言为不同高级语言的不同编译器提供了通用的输出语言,如C编译器和Fortran编译器产生的输出文件用的是一样的汇编语言 
3,汇编阶段将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,结果保存在目标文件hello.o中,hello.o是二进制文件,它的字节编码是机器语言指令而不是字符 
4,链接阶段由于hello程序调用了printf这个标准C库中的一个函数,而printf函数存在于一个名为printf.o的单独预编译目标文件中,链接器就负责将它并入到我们的hello.o程序中,结果就得到hello文件,它是一个可执行目标文件,可执行文件加载到存储器后由系统负责执行 


1.3 了解编译系统如何工作室大有益处的 
一些重要原因促使程序 员必须知道编译系统是如何工作的: 
1,优化程序性能 
switch和if-then-else、while和do、指针引用和数组索引,IA32(32bit)和X86-64((64bit))
2,理解链接时出现的错误 
链接器报告说无法解析一个引用、静态变量和全局变量、静态库和动态库 
3,避免安全漏洞 
多年来,缓冲区溢出错误是造成大多数网络和Internet服务器上安全漏洞的主要原因。


1.4 处理器读并解释储存在存储器中的指令 
Java代码  
unix> ./hello  
hello, world  
unix>  


shell等待命令输入并执行,如果命令的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,要加载和执行该文件 
这里shell将加载和执行hello程序,然后等待程序终止,hello程序在屏幕上输出信息,然后终止 


系统的硬件组成: P5图! CPU,ALU,PC,USB的全称
总线 -- 贯穿整个系统的是一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递,通常总线被设计成传送定长的字节块,也就是字,字中字节数(字长,Intel中字长是2字节,见第3章的13点)是基本的系统参数 
I/O设备 -- 系统与外界的联系通道,例如用户输入的键盘和鼠标、用户输出的显示器、长期存储数据和程序的磁盘,每个I/O设备都是通过一个控制器或适配器与I/O总线连接起来,它们在I/O总线和I/O设备之间传递信息 
主存 -- 主存是一个临时存储设备,在处理器执行程序时,它被用来存放程序和程序处理的数据,物理上来说就是DRAM芯片,逻辑上来说存储器是由一个线性的字节数组组成的,每个字节都有自己唯一的地址(数组索引),这些地址从零开始 
处理器 -- 中央处理单元(CPU)简称处理器,是解释(或执行)存储在主存中指令的引擎,处理器的核心是一个被称为程序计数器(PC)的字长大小的存储设备(寄存器),任何时间点上,PC都指向主存中的某条机器语言指令(内含其地址)。从系统通电开始直到系统断电,处理器一直重复执行相同的基本任务:从程序计数器指向的存储器处读取指令,解释指令中的位,执行指令指示的简单操作,然后更新程序计数器指向下一条指令,而这条指令并不一定在存储器中和刚刚执行的指令相邻 
操作在主存、寄存器文件和算术逻辑单元(ALU)之间循环 
CPU在指令要求下可能会执行这些操作: 
加载:从主存拷贝一个字节或者一个字到寄存器,覆盖寄存器原来的内容 
存储:从寄存器拷贝一个字节或者一个字到主存的某个位置,覆盖这个位置上原来的内容 
操作:拷贝两个寄存器的内容到ALU,ALU将两个字相加,并将结果存放到一个寄存器中,覆盖该寄存器中原来的内容 
I/O读:从一个I/O设备中拷贝一个字节或者一个字到一个寄存器 
I/O写:从一个寄存器中拷贝一个字节或者一个字到一个I/O设备 
跳转:从指令本身中抽取一个字,并将这个字拷贝到程序计数器中,覆盖PC中原来的值 


程序执行过程: 
shell等待我们输入字符串"./hello"后,shell逐一读取字符到寄存器,然后放到存储器中 
当我们敲回车键时,shell知道我们已经结束了命令的输入,然后shell执行一系列指令 
这些指令将hello目标文件的代码和数据从磁盘拷贝到主存,从而加载hello文件,数据包括最终被输出的字符串"hello, world\n" 
利用DMA(直接存储器存取)技术,数据可以不通过处理器而直接从磁盘到达主存 
一旦hello目标文件中的代码和数据被加载到存储器,处理器就开始执行hello程序的主程序中的机器语言指令 
这些指令将"hello, world\n"串中的字节从存储器中拷贝到寄存器文件,再从寄存器文件拷贝到显示设备,最终显示在屏幕上 


1.5 高速缓存 P9图!L0为寄存器。
上面的例子可以看出,系统花费了大量的时间把信息从一个地方挪到另一个地方 
hello程序最初位于磁盘上,程序加载时拷贝到主存,处理器运行程序时,指令拷贝到处理器 
类似的,数据串"hello, world/n"开始在磁盘上,再被拷贝到主存,然后拷贝到显示设备 
磁盘驱动器可能比主存大100倍,对处理器而言,从磁盘读取一个字的时间开销要比从主存读取的开销大1000万倍,处理器从寄存器文件中读数据比主存中读取则要快几乎100倍 
针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memories,简称高速缓存) 
位于处理器芯片上的L1高速缓存的容量可达数万字节,访问速度几乎和寄存器文件一样快 
容量为数十万到数百万的更大的L2高速缓存是通过一条特殊的总线连接到处理器的 
访问L2的时间开销比L1大5倍,但仍然比访问主存快5~10倍 
L1和L2高速缓存是用一种叫静态随机访问存储器(SRAM)的硬件技术实现的 


1.6 形成层次结构的存储设备 
每个计算机的存储设备组织成一个存储器层次模型,从上至下设备变得更慢、更大,并且每字节的造价也更便宜 
寄存器文件位于层次模型的最顶部,第0级记为L0,L1高速缓存为第一层,L2高速缓存为第二层,主存为L3,本地磁盘等本地二级存储为L4,分布式文件系统、Web服务器等远程二级存储为L5 
层次结构的主要思想是一个层次上的存储器作为下一层次上存储器的高速缓存,寄存器文件是L1的高速缓存,L1是L2的高速缓存,L2是主存的高速缓存,主存是磁盘的高速缓存,本地磁盘是分布式文件系统的高速缓存 


1.7 操作系统管理硬件 
当shell加载和运行hello程序时,当hello程序输出自己的消息时程序并没有直接访问键盘、显示器、磁盘或者主存储器,而是依靠操作系统提供的服务 
操作系统可以看作是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统 
操作系统的两个基本功能:访问硬件被失控的应用程序滥用;在控制复杂而又通常广泛不同的低级硬件设备访问为应用程序提供简单一致的方法 
操作系统通过几个基本的抽象概念(进程、虚拟存储器和文件)来实现这两个功能 
文件是对I/O设备的抽象表示,虚拟存储器是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示 
Unix有很多分支,所以出现Posix标准,各Unix分支的差异正在渐渐消失。


进程是操作系统对运行程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件,我们称之为并发运行,实际上是说一个进程的指令和另一个进程的指令是交错执行的。注意与并行的区别(并行<parallelism>:指的是用并发使一个系统运行得更快。P15)。
操作系统实现这种交错执行的机制称为上下文切换(context switching) P11图
操作系统保存进程运行所需的所有状态信息,这种状态就是上下文(context),包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容 
在任何一个时刻,系统都只有一个进程正在运行 
当操作系统决定从当前进程转移控制权到某个新进程时,它就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权转移到新进程,新进程就会从它上次停止的地方开始 
进程这个抽象概念还暗示着由于不同的进程交错执行,打乱了时间的概念,使得程序员很难获得运行时间的准确和可重复测量 


尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据 
由于网络服务器中对并行处理的要求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般都比进程更高效 


虚拟存储器是一个抽象概念,它为每个进程提供了一个假象,好像每个进程都在独占地使用主存 
每个进程看到的存储器都是一致的,称之为虚拟地址空间,地址从小到大为: 
1,程序代码和数据 
2,运行时堆(运行时动态地扩展和收缩) 
3,共享库(C标准库和数学库等) 
4,栈(编译器用它来实现函数调用,调用一个函数时栈就会增长,从函数返回时栈就会收缩) 
5,内核虚拟存储器(内核总是驻留在内存中!是操作系统驻留在存储器中的部分,进程虚拟地址空间(4G)顶部四分之一的部分即为内核预留(1G),不允许应用程序:读写这个区域的内容或者直接调用内核代码定义的函数) 


文件就是字节序列!每个I/O设备,包括磁盘、键盘、显示器甚至网络都可以看成文件 
系统中所有输入输出都是通过使用称为Unix I/O的以小组系统函数调用读写文件来实现的 
文件这个简单而精致的概念非常强大,因为它使得应用程序能够统一地看待系统中可能含有的所有各式各样的I/O设备,如处理磁盘文件内容的程序员可以非常幸福地无需了解具体的磁盘技术 


1.8 利用网络系统和其他系统通信 
从一个单独的系统来看,网络可以视为又一个I/O设备 
当系统从主存拷贝一串字符到网络适配器时,数据流经过网络到达另一台机器,而不是到达本地磁盘驱动器 
相似地,系统可以读取从其他机器发送来的数据,并把数据拷贝到自己的主存,最后如有必要再从主存拷到磁盘中保存起来。
例如telnet连接远程服务器并运行hello程序的例子,命令和结果在网络I/O中传输 


1.9 重要主题:
1. 并发与并行
线程级并发:单处理器系统,多处理器系统,超线程(书上原话:有时称为同时多线程(simultaneous multi-threading),是一项允许一个CPU执行多个控制流的技术。举例来说,Intel Core i7处理器可以让一个核执行两个线程,所以一个4核的系统实际上可以并行地执行8个线程。百度百科上的解释:让单个CPU可以同时执行多重线程,就能够让CPU发挥更大效率,即所谓“超线程(Hyper-Threading,简称“HT”)”技术。来自百度百科:http://baike.baidu.com/view/39.htm),多核处理器(将多个CPU(称为“核”)集成到一个集成电路芯片上,比如intel core i7处理器有4个CPU核(见P16的Intel core i7组织结构图:4年处理器集成到一个芯片上),每个核都有自己的L1和L2高速缓存,但是它们共享更高层次的高速缓存,以及到主存的接口,业内人士说将来可能集成几十个甚至上面个CPU核到一个集成芯片上)
指令线并发(现代处理器可以同时执行多条指令的属性称为指令级并行):流水线(pipelining)中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系统的阶段,每个阶段执行一个步骤。这些阶段可以并行地操作,用来处理不同指令的不同部分,如一个简单的硬件设计使它够达到接近于一个时钟周期执行一条指令的执行速率。
单指令、多数据并行(允许一条指令产生多个可以并行执行的操作,也即SIMD并行。例如,较新的Intel和AMD处理器都具有并行地对4对单精度浮点数(C中的float类型)做加法的指令。提供这些SIMD指令多是为了提高处理影像、声音和视频数据应用的执行速度。虽然有些编译器试图从C程序中自动抽取SIMD并行性,但是更可靠的方法是使用编译器支持的特殊向量数据类型来编写程序,例如GCC就支持向量数据类型。)




小结 
计算机系统由硬件和系统软件组成,它们共同协作以运行应用程序 
计算机内部的信息被表示为一组组的位,它们依据不同的上下文又有不同的解释方式 
程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件 


处理器读取并解释存放在主存里的二进制指令 
计算机花费了大量的时间在存储器、I/O设备和CPU寄存器之间拷贝数据,所以系统中的存储设备就被按层次排列,CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存储器和磁盘存储器 
在层次模型中位于更高层的存储设备比低层的存储设备要快,单位比特造价也更高 
程序员通过理解和运用这种存储层次结构的知识,可以优化他们C程序的性能 


操作系统内核是应用程序和硬件之间的媒介,它提供三个基本的抽象概念:文件是对I/O设备的抽象概念;虚拟存储器是对主存和磁盘的抽象概念;进程是对处理器、主存和I/O设备的抽象概念 


网络提供了计算机系统之间通信的手段,从某个系统的角度来看,网络就是一种I/O设备


第2章:信处的表示和处理
1. 补码:two's complement, 反码:one's complement P20 P43
三种重要的数字表示:无符号,补码,浮点数
溢出:结果大到不能正确表示时就发生溢出。如:200*300*400*500会得到一个负数!


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


浮点数具有完全不同的数学属性。虽然溢出会产生特殊的值:正无穷大,但是一组正数的乘积总是正的。由于表示的精度有限,浮点运算是不可结合的。三个浮点数相加,不同的加法组合得到的结果可能是不一样的。浮点数表示只是近似的。 P20


虚拟地址空间是一个展现给机器级程序的概念性映像。实际的实现是将随机访问存储器(RAM)、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。程序中指针的值就是一个虚拟地址空间的地址!而不是物理内存中存储单元的地址!P22


C程序所得的实际机器级程序并不包含关于数据类型的信息!数据类型是由编译器解析并翻译成相应的机器代码。 P22


C中的十六进制直接量中,字母大小写可以同时出现在一个值中。即不区分大小写! P23


每台计算机都都有一个字长(word size),指明整数和指针数据的标称大小(nominal size)。字长决定的最重要的系统参数是虚拟地址空间的最大大小。 P25


处理器读写存储器(主存,内存)的存储方式:小端法,大端法,双端法(可设置成使用小端或大端)。P27
端的起源:鸡蛋


$ man ascii // 生成ascii表 P31


UTF-8: 一个字节到四个字节不等。 P31


ascii字符集,Unicode字符集(具体实现:UTF-8,UTF-16等) P31


Java默认用Unicode字符集来表示字符串,也即在解码(在运行时操作系统中还原成字符串)也要用Unicode字符集相对应的解码方式才能正确还原字符串,否则乱码。 P31


布尔代数:Bool algebra P32


掩码 P35


C和C++都支持无符号数,但java只支持有符号数。


下面的内容来自:http://blog.csdn.net/zdl1016/article/details/2507743
首先进行一个实验,分别定义一个signed int型数据和unsigned int型数据,然后进行大小比较:
    unsigned int a=20;
    signed int b=-130;
a>b?还是b>a?实验证明b>a,也就是说-130>20,为什么会出现这样的结果呢?
这是因为在C语言操作中,如果遇到无符号数与有符号数之间的操作,编译器会自动转化为无符号数来进行处理,因此a=20,b=4294967166,这样比较下去当然b>a了。
再举一个例子:
 unsigned int a=20;
 signed int b=-130;
 std::cout<<a+b<<std::endl;
结果输出为4294967186,同样的道理,在运算之前,a=20,b被转化为4294967166,所以a+b=4294967186


减法和乘法的运算结果类似。


如果作为signed int型数据的b=-130,b与立即数之间操作时不影响b的类型,运算结果仍然为signed int型:
signed int b=-130;
std::cout<<b+30<<std::endl;
输出为-100。


而对于浮点数来说,浮点数(float,double)实际上都是有符号数,unsigned 和signed前缀不能加在float和double之上,当然就不存在有符号数根无符号数之间转化的问题了。
(end)


C标准并没有要求用补码形式来表示有符号整数,但几乎所有的机器都是这么做的。许多书籍也一般假定是这样做的。Java标准明确要求用补码表示所有整数(它没有无符号整数) P42


确定大小的整数类型:为了可移植性。 P42
int32_t 32位有符号整型 
uint16_t 16位无符号整型


原码(最高位表示符号,其余位表示值),反码,补码:
0000001 就是+1
1000001 就是-1 
-3的反码(只是值位取反,符号位不取反)即10000011的反码为:11111100 
负数的补码是将其原码除符号位之外的各位求反之后在末位再加1。 
-3的补码即10000011的补码:=11111101  


对大多数C语言的实现而言,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会改变,但是位模式不变。也即数的机器表示在转换前后不会发生变化。 P45


C中的无符号常量(直接量)的表示方法:必须在数值后加上“U”或“u”。如: 12345U 或者 0x1A2Bu P47
C允许无符号数和有符号数之间的转换,原则是底层的位表示保持不变。 P47


将一个补码数字转换为一个更大的数据类型可以执行符号扩展(sign extension),规则是在表示中添加最高有效位的值的副本。P49


如果要截断数字:减少表示一个数据的位数(如从int到short),则直接去除所有多余的高位,这样转换后可能将正数变成一个负数。 P51


我们已经看到了由于许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不用无符号数。实际上,除了C/C++外,很少有语言支持无符号整数。 P54


当我们想要把字仅仅看做是位的集合,并且没有任何数字意义时,无符号数是非常有用的。 P55


舍入:因为表示限制了浮点数的范围和精度,浮点运算只能近似地表示实数运算。因此,对于值x,我们一般想用一种系统的方法,能够找到“最接近的”匹配值y,它可以用期望的浮点形式表示出来,这就是舍入(rounding)运算的任务。 P74


从int转换成float,数字不会溢出,但是可能被舍入。
从int或float转换成double,能保留精确的数值。
从double转换成float:可能溢出正无穷或负无穷。另外,由于精确度较小,它还可能被舍入。
从float或者double转换成int,值将会向0舍入。如:1.999将被转换成安,而-1.999将被转换成-1.进一步来说,值可能会溢出。(int) + 1e10会得到一个巨大的负数。 P78


p39-40 
把二进制数转为无符号十进制数 B2U(w)=∑xi*2^i (0<=i<=w-1)
B2U(4)([1011])=1*2^3+0*2^2+1*2^1+1*2^0=11


转换为补码形式 B2T(w)=-x(w-1)*2^(w-1)+∑xi*2^i (0<=i<=w-2)
B2T(4)([1011])=-1*2^3+0*2^2+1*2^1+1*2^0=-5


p45-46
无符号与补码之间转换
T2U(w)=x+2^w (x<0)  
=x (x>=0)


U2T(w)=u (u<2^(w-1))
=u-2^w (u>=2^(w-1))


p52
无符号数的截断结果 B2U([x(k-1),x(k-2)...x0])=B2U([x(w-1)...x0])mod 2^k
补码数字截断结果 B2T([x(k-1),x(k-2)...x0])=U2T(B2U([x(w-1)...x0])mod 2^k)


p55
无符号加法 x+(u,w)y= x+y (x+y<2^w) 
= x+y-2^w (2^w<=x+y<=2^(w+1))


p58
补码加法  x+(t,w)y= x+y-2^w (2^(w-1)<=x+y) 正溢出
=x+y (-2^(w-1)<=x+y<2^(w-1)) 正常
=x+y+2^w (x+y<-2^(w-1)) 负溢出








C语言被设计成包容多种不同字长和数字编码的实现。虽然高端机器逐渐开始使用64位字长,但是目前大多数机器仍使用32位字长。大多数机器对整数使用二进制补码编码,而对浮点数使用IEEE编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于编写能在全部数值范围上正确计算的程序来说,是很重要的。 


C语言的标准规定在无符号和有符合整数之间进行强制类型转换时,基本的位模式不应该改变。在二进制补码机器上,对于一个w位的值,这种行为是由函数T2Uw和U2Tw来描述的。C语言隐式的强制转换会得到许多程序员无法预计的结果,常常导致程序错误。 


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


和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比有一些特殊的属性。例如,由于溢出,表达式x*x能够得出负数。但是无符号数和二进制补码的运算都满足环的属性。这就允许编译器做很多的优化。例如,用(x<<3)-x取代表达式7*x时,我们就利用了结合性、交换性和分配性,还利用了移位和乘以2的幂之间的关系。 


我们已经看到了几种使用位级运算和算术运算组合的聪明方法。例如,我们看到,使用二进制补码运算,~x+1是等价于-x的。另外一个例子,假设我们想要一个形如[0,...,0,1,...,1]的位模式,由w-k个0后面紧跟着k个1组成。这些位模式对于掩码运算是很有用的。这种模式能够通过C表达式(1<<k)-1生成,利用的是这样一个属性,即我们想要的位模式的数值为2k-1。例如,表达式(1<<8)-1将产生位模式0xFF。 


浮点表示通过将数字编码为x*2y的形式来近似地表示实数。最常见的浮点表示方式是由IEEE标准754定义的。它提供了几种不同的精度,最常见的单精度(32位)和双精度(64位)。IEEE浮点也能够表示特殊值无穷大8和NaN。 


必须非常小心地使用浮点运算,因为浮点运算的范围和精度有限,而且浮点运算并不遵守普遍的算术属性,比如结合性。




3,float一般为4字节,double一般为8字节,指针一般用的是全字长,32位机上是4字节,64位机上是8字长。


4,在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节序列中最小的地址。
5,二进制代码很少能在不同机器和操作系统组合之间移植。
6,表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器同样的掩码可以写出0xFFFFFFF,但是这样的代码是不可移植的。
7,几乎所有的编译器/机器组合都对有符号数据使用算术右移,即在左边补充符号位。
8,C和C++都支持符号和无符号数,但JAVA只支持有符号数。
9,C库中的文件<limits.h>定义了一组常量,用来限定运行编译器的这台机器的不同整形数据类型的范围,如INT_MAX,INT_MIN,UINT_MAX。
10,强制类型转换并没有改变参数的位表示,只是改变了如何将这些位解释为一个数字:
 int x = -1;
 unsigned ux = (unsigned)x;
 这里的ux = 0xffff ffff
11,规则1:当将一个有符号数映射为它相应的无符号数时,负数就被转换成了大的正数,而非负数会保持不变。
规则2:对于小的数(<2^(w-1)),从无符号到有符号的转换将保留数字的原值,对于大的数,数字将被转换为一个负数值。
12,无符号整数加法: 


   x+y = x+y ,            x+y<2^w
   x+y = x+y-2^w     x+y>=2^w
13,有符号整数加法: 
x+y = x+y-2^w,            x+y >= 2^(w-1) 正溢出
x+y = x+y,                     正常
x+y = x+y+2^w,           x+y < -2^(w-1) 负溢出
14,二进制补码的非
-x = -2^(w-1),     x = -2^(w-1)
-x = -x,                 x > 2^(w-1)
15,在单精度浮点格式(C中的float)中,s,exp和frac分别为1位,8位,23位,产生一个32位的表示。在双精度格式(C中的double)中,s,exp和frac分别为1,11位,52为,产生一个64位的表示。
16,浮点数的舍入规则是向偶数舍入(round-to-even),或者向最接近的值舍入(round-to-nearest)。
17,浮点加法不具有结合性,浮点加法满足下面的单调性属性:如果a>=b,那么对任何a和b的值,除了x不等于NaN,都有x+a>=x+b。浮点乘法也满足相应的单调性属性。
18,浮点数取非就是简单的对它的符号位去反。float f; f == -(-f)是正确的。


19,看下面这段代码


[cpp] view plaincopy
#include <iostream>  
#include <iomanip>  
using namespace std;  
  
void main()  
{  
    double x = 1.3;  
        double y = 0.4;  
        if (x + y != 1.7)   
            cout << "addition failed?" << endl;  
}  
   


运行结果将是addition failed?" 。也就是x+y != 1.7原因就是double中保存的是近似值,而不是精确值1.7.


正确的写法应该如下:


[cpp] view plaincopy
#include <iostream>  
#include <iomanip>  
using namespace std;  
  
const double epsilon = 0.000001;  
  
bool about_equal(double x, double y)  
{  
    return (x < y + epsilon) &&  
           (x > y - epsilon);  
}  
  
void main()  
{  
    cout << "1.3 + 0.4 == 1.7: " <<   
            (1.3 + 0.4 == 1.7) << endl;  
    cout << "about_equal(1.3 + 0.4, 1.7): " <<  
            about_equal(1.3 + 0.4, 1.7) << endl;  
}  


第3章 程序的机器级表示


1. 通过阅读和分析汇编代码,我们能理解编著译器的优化能力,并分析代码中隐含的低效率。 P102


2. 32位机器(IA32)只能使用大概4GB(2的32次方字节)的内存地址空间(在实际的操作系统中,一般指的是虚拟地址空间)。而64位机器上能使用多达256TB(2的48次方字节)的内存地址空间。P103


3. IA32(Intel Architecture 32-bit)
Intel64:IA32的64位扩展,也称为x86-64
一般我们用x86和i386表示IA32,当提及64位时,经常会明确指出。 P104


4. AMD在技术上紧跟Intel,其市场策略是:生产的处理器性能稍低但价格更便宜。AMD和Intel的架构是一样的。 P105


5. Linux使用了平面寻址方式(flat addressing),在这种寻址方式中,程序员将整个存储空间看作一个大的字节数组。 P105


6. gcc -O1 -o p file1.c file2.c // -O表示优化级别,级别越高,优化程序越高,运行速度越快,但编译时间更长,更难于调试 P106


7. cisop  .c(文本文件),.i(文本文件),.s(文本文件),.o(二进制文件,机器码),可执行文件p P106


8. ISA:指令集体系结构(Instuction set architecture),它定义了机器级程序(处理器状态指令)的格式和行为。ISA的实现,如IA32,x86-64. P106


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


10. PC
整数寄存器文件(8个,32位):这些寄存器可以存储地址(对应C中的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其它的寄存器则用来保存临时数据,例如过程的避部变量和函数的返回值。
条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如用来实现if和while语句。
一组浮点寄存器存放浮点数据。 P106


11. 每个进程都有一个4G的虚拟地址空间,还是虚拟地址空间只有4G供所有进程共享(一个进度只占用一部分)? P107


12. 反汇编器: objdump: $ objdump -d abc.o // abc.o 由gcc -c生成,objdump会生成汇编源码并打印到控制台 P108


13. 关于“字”与“字长”,本书前后矛盾!!P111,Intel中一个字指二个字节(故short在汇编中是w(word)),因为32位系统是从16位扩展而来的。因此,四字节的int是双字长(故int在汇编中是l(long))。 P111


14. 汇编代码用“l”表示4字节整数和8字节双精度浮点,这不会产生歧义,因为浮点数用的是一组完全不同的指令和寄存器。 P111


15. 寄存器图P112: 32位:acd bsd sb; 16位: acdbsdsb; 8位:acdb(h(high),l(low))
e: extention


16: 15是存储器,是用来放操作数的,计算机中有寄存器和内存用来放运行时数据,那么操作数类型有:立即数(常数),寄存器,存储器(主存或内存)。 P113
Imm(Eb,Ei,s)
Imm:Immediate
E: extented register,指寄存器本身,如:%eax(把“%eax”看成是寄存器的名字,寄存器可以看是是变量,它的是值可用%eax引用)
b: base
i: 偏移,如数组的下标
s: step,如数组中无素的长度,也即指针的跨度,必须是2的次方(包括0次方即1)
有效地址被计算为:Imm + R[Eb] + R[Ei]*s  // R: Register


17. 传送指令集表 P114
基本格式:mov S,D  // S:source, D: Destination
mov: movb,movw,movl  // 传送
movs:  movsbw,movsbl,movswl // 传送符号扩展的字节,s: signed
movz: ...  // 传送零扩展的字节, z: zero
pushl S // S:source,将S数据压入程序栈中
popl D // D: Destination,从程序栈弹出数据
上面的符号扩展:目的位置的所有高位用源值的最高位数值进行填充; 零扩展,所有高位都用零填充。
将一个双字值压入栈中,首先要将栈指针减4,然后将值写到新的栈顶地址。故pushl %ebp的行为等价于以下两条指令:
subl $4,%esp  // Decrement stack pointer
movl %ebp,(%esp) // store %ebp on stack
18. 条件码寄存器中的几个位标志:CF,ZF,SF,OF, C进位,Z(ero),S(ign),O(verflow),F(lag) P124
19. 注意机器代码如何区分有符号和无符号值是很重要的。同C程序不同,机器代码不会将每个程序值都和一个数据类型联系起来。相反,大多数情况下,机器代码对于有符号和元符号的情况都使用一样的指令,这是因为许多算术运算对无符号和补码算术都有一样的位级行为。有些情况需要用不同的指令来处理有符号和元符号操作,例如,使用不同版本的右移、除法和乘法指令,以及不同的条件码组合。 P126
20. 无条件跳转,有条件跳转 P127
21. 对C中的循环,汇编没有相应的指令存在,可以用条件测试和跳转组合起来实现循环。对大多数汇编器而言,循环都是先转换成do-while形式然后再编译成机器代码。 P132
22. 理解产生的汇编代码与源代码之间的关系,关键是找到程序值和寄存器之间的映射关系。 P133
23. 源代码中的操作和数据,对应汇编中的操作指令和寄存器。 forever note 
24. IA32程序用程序栈来支持过程调用。 P149
25. 寄存器adc称为调用者保存寄存器(可被被调用过程直接覆盖)。bsd称为被调用者保存寄存器(被调用过程覆盖它们之前须保存它们中的内容)。 P151
26. GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。采用这个规则是为了保证访问数据的严格对齐。 P154
27. 对齐(26中提到的)简化了开成处理器和存储器系统之间接口的硬件设计。 P170
28. 无论数据是否对齐,IA32硬件都能正确工作。不过,Intel还是建议要对齐数据以提高存储器系统的性能。Linux沿用的对齐策略是,2字节数据类型(如short)的地址必须是2的倍数,而较大的数据类型(如int ,int*, float,double)的地址必须是4的倍数。注意这个要求就意味着一个short类型对象的地址最低位必须等于0。类似地,任何int类型的对象或指针的地址的最低两位必须都是0。 P170
29. 使用GDB调试器。 P174
30. 使用-O2的gcc:过程调用常常是内联的。常用循环来潜代递归。控制结构变得更纠结。 P174
31. 符号和符号表:每个可重定位目标模块m都有一个符号表,它包令m所定义和引用的符号的信息。在链接器的上下广开言路上,有三种不同的符号:由m定义并能被其他模块引用的全局符号。由其它模块定我并被模块m引用的全局符号。只被模块m定义和引用的本地符号。 P452
32. 典型的可执行目标文件的格式:ELF头部,段头部表,.init,.text,.rodata,.data,.bss,.symtab,.debug,.line,.strtab,节头表 P465
33. 静态链接:符号解析,重定义(为符号生成虚拟存储器的地址) P450
34. 几种二进制文件:可重定位目标文件(汇编器产生的机器代码),可执行目标文件(链接器生成),共享目标文件(模块,可在运行时加载到内存、链接到主程序并运行) P450
35. ELF可重定位目标文件格式:ELF头,.text(函数代码),.rodata(程序中出现的常量字串),.data(全局C变量),.bss(未初始化的全局C变量),.symtab,.rel.text(一个.text节中位置的列表,当链接器把这个目标文件和其它文件结合时,需要修改这些位置),.rel.data,.debug,.line,.strtab,节头部表 P451
36. 存储器:寄存器,sram(内存的高速缓存,只要有电,sram中的存储单元就会永远地保持它的值,不需要刷新),dram(内存,在有电的情况下,仍需要定时刷新,以保持数据值,这时因为电容易漏电,很快就会失去其应有的状态,所以需要在其失却状态之前,定时纠正状态(刷新,这个刷新的时间相对于漏电的过程要短得多,所以在不断刷新的情况下,数据得以保持)),磁(硬)盘(机械臂读取盘片),固态存储技术(SSD,U盘和固态硬盘结构原理是一样的,主控芯片+FLASH存储芯片)的实现:固态硬盘(DDRRAM Base SSD,需要电源保持数据,断电情况下,数据只能保存几年,较U盘更不易磨损)和Flash(U盘,SD卡,无需电源即可持久保存数据,较硬盘更易磨损),CD/DVD光盘,磁带 P383
37. 总线接口(CPU),系统总线,I/O桥(南,北),存储器总线,主存 P388
movl A,%eax // 读虚拟地址为A的数据(保存在特理主存中)到%eax: 总线接口发出读请求...
movl %eax,A // 将%eax的内容写入到虚拟地址为A对应的主存中
38. 磁盘:RPM:revolution per minite,转/分钟 P389
任何时刻,所有的读/写头都位于同一个柱面上。 P392
39. 寻道时间,旋转时间(最大为旋转一周的时间<刚好错过目标位置>),传送时间(读/写,速度约为sram的4万倍,内存(dram)的2500倍) P393
40. USB:Universal Serial Bus, SCSI(读音:scuzzy,比SATA贵!),SATA P395
41. 缓存与程序的良好局部性(locality)。 P401
42. 局部性:时间局部性,空间局部性,局部性好的程序运得得更快 P402
43. 重复引用同一个变量的程序有良好的时间局部性。
对于具有步长为k的引用模式的程序,步长越小(如1),空间局部性越好。 P403
对于取指令来说,循环环有好的时间和空间局部性,循环体越小,循环迭代次数越多,局部性越好。 P404
44. 缓存命中,不命中(冷缓存(强制性不命中,冷不命中),暖身,放置策略,冲突不命中)  P407
45. 各类缓存对比表,详细,页一般为4KB  P408
46. 程序性能,最小二乘方拟合
性能优化:消除循环的低效率,减少过程调用,消除不必要的存储器引用 P332
47. C:变量、表达式(操作符与数据)、控制语句、函数
汇编:寄存器,存储器,常量,指令:(传送(move,push,pop,lea<load effective address,获取地址,而不是内容>),运算<加减负补加减乘除移位与或异6四>,控制(条件码寄存器,比测设跳传<带条件>),调用(call,leave,ret)) P114
48. 在取一条指令时,执行它前面一条指令的算术运算。要做到这一点,要求能够事先确定要执行指令的序列,这样才能保持流水线中充满了待执行的指令。当机器遇到条年跳车(也称为“分支”)时,它常常还不能确定是否会进行跳出转,于是只能进行猜测(现代处理器的成功率可达到90%左右),这样命名得指令流水线中充满着指令了。但一旦猜错,也就导致程序性能的严重下降。 P141
49. 条件数据传送及相关指令。如cmovne. P143
50. GNU(编译器:gcc, 汇编器:as, 链接器:ld, 反汇编:objdump, 反编译:gnu套件中没有!), libdwarf,libbfd(as,ld,objdump),libelf
java的编译器:javac,反编译器:jdk带的javap、或jad(不在jdk中)
有一些反编译工具,如Boomerang(http://boomerang.sourceforge.net/,好像页面中显示在06年就停止更新了??在http://stackoverflow.com/questions/193896/whats-a-good-c-decompiler中说:I tested it with some windows binaries, and Boomerang just decompiled a small one. The others crashed this decompiler. )
在“http://stackoverflow.com/questions/205059/is-there-a-c-decompiler”(Is there a C++ decompiler?)中说:
回答1. You can use IDA Pro by Hex-Rays. You will usually not get good C++ out of a binary unless you compiled in debugging information. Prepare to spend a lot of manual labor reversing the code.
If you didn't strip the binaries there is some hope as IDA Pro can produce C-alike code for you to work with. Usually it is very rough though, at least when I used it a couple of years ago.
回答2. Yes, but none of them will manage to produce readable enough code to worth the effort. You will spend more time trying to read the decompiled source with assembler blocks inside, than rewriting your old app from scratch.
51. C++编译器无法检测出的8个常见编程错误:
1)变量未初始化
2)整数除法
3)=  vs  ==
4)混用有符号和无符号数
5)  delete  vs  delete []
6)  复合表达式或函数调用的副作用
7)不带break的switch语句
8)在构造函数中调用虚函数
52. 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
深度学习对于计算机架构师而言非常重要。深度学习是一种人工智能的实现方法,其运用了人工神经网络实现对数据的处理和学习,支持对数字图像、语音和文本等不同类型的数据进行分析和处理。因此,深度学习需要大量的计算和存储资源。计算机架构师需要设计和开发硬件体系结构来支持这些计算需求,并选择合适的存储方案来支持大规模的数据处理。 在计算机架构设计方面,深度学习的计算过程需要大量的浮点数计算,这通常需要高度并行的计算能力和高速的内存和存储访问速度。因此,计算机架构师需要设计高效的处理器、内存和存储系统,并考虑如何最大限度地利用每个资源。此外,深度学习通常需要大量的数据传输和存储。因此,计算机架构师需要设法提高访问速度、减少延迟和增加存储容量,以便为深度学习模型提供足够的计算和存储资源。 在存储方面,计算机架构师需要选择合适的存储设备来支持大规模的数据集。传统的硬盘驱动器最适合存储海量数据,而固态硬盘则能够提供更高的读写速度和更低的延迟。在设计存储系统时,计算机架构师还需要考虑数据访问的频率和数据的类型,并为不同的存储需求选择不同的存储技术。 总之,深度学习对计算机架构师的硬件设计和架构非常重要,需要者加强学习研究。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值