csapp-深入理解计算机系统学习记录

csapp 学习记录一

第1章:计算机系统漫游

信息就是位+上下文

系统中的所有信息,都是一串比特组成的。区分不同数据对象的唯一方法是联系他们的上下文。

从一个c文件,到可执行目标文件整个翻译过程分为4个阶段

csapp-深入理解计算机系统学习记录

  • 预处理阶段 预处理器 cpp 根据字符# 开头的命令,修改原始的C程序。

    比如include<stdio.h> 命令告诉预处理器,读取系统头文件 stdio.h的内容。并把它插入到程序中。结果得到了另一个c程序。通常为.i作为文件拓展。

  • 编译阶段。 编译器(ccl)将hello.i 翻译成文本文件 hello.s 翻译是将.i 转为汇编。

  • 汇编阶段, 汇编器(as)将hello.s 翻译成机器语言指令,将这些指令打包成可重定位目标程序的格式。并将结果保存至目标 hello.o中(改文件为二进制文件,它包含的17个字节是main的执行编码)。如果文本编辑器打开,则为一堆乱码

  • 链接阶段 : 比如说hello程序调用了printf函数,是每个C编译器都提供的标准C库中的一个函数,printf 函数保存在一个名为printf.o 的单独预编译完成的目标文件中。 这个文件必须以某种方式合并到我们程序中。链接器(ld)就负责这种合并,于是得到hello文件,它是一个可执行文件,可以直接加载到内存中执行。

程序执行的过程:

shell程序执行指令,等待我们的命令,输入命令并回车之后, shell将字符都逐一读进寄存器。再把它放到内存中。

(利用DMA:直接存储器读取)技术可以数据不经过处理器而直接从磁盘到达主存。

一旦目标文件hello的代码和数据被加载到内存,处理器就开始执行。hello程序中main程序中的机器语言指令。再从寄存器文件中复制到显示设备,最终显示在屏幕上。

csapp-深入理解计算机系统学习记录

摩尔定律:

csapp-深入理解计算机系统学习记录

HELLO WORLD 可执行程序的产生

hello.c 文本文件的创建:

#include<stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

对源代码进行编译,生成可执行文件hello

理解编译过程及原理的意义何在

整体来看有三个方面的原因:

1、优化程序性能

2、理解程序链接时的错误,有助于我们解决各种奇奇怪怪的错误

3、避免安全漏洞,比如缓冲区溢出漏洞等

可执行程序hello在计算机上执行的过程

此过程用几副抽象出来图片来说明一下

csapp-深入理解计算机系统学习记录 图中“hello”由usb键盘输入,通过I/O总线传递给cpu,其处理后将获得的数据存至内存中

csapp-深入理解计算机系统学习记录 图中磁盘与内存之间通过DMA(Direct Memory Access)直接存储器存取技术将需要的hello程序数据,从磁盘中读入内存中

csapp-深入理解计算机系统学习记录 图中CPU读取内存中的程序指令及数据,处理后将计算后的数据交给显示设备,显示设备收到数据后进行显示

程序执行过程中的几点启示

1、程序执行时数据需要进行多点交换,整个过程需要消耗“大量”时间。

2、按照目前大部分计算机来说,计算机数据存储单元按照读写速度由大到小比较:寄存器>高速缓存L1>高速缓存L2>高速缓存L3>内存>磁盘,而按照数据存储大小来看正好相反。

3、本例中,hello程序的执行并非是自己将自己交给处理器进行执行,而是通过shell程序将各种参数交给操作系统,操作系统再对计算机资源进行调度,然后将hello程序交给计算机组件进行处理输出。

4、计算机在进行资源调度时,会为新的程序创建一个进程,而每个进程看到的内存都是一样的,因为程序在链接的时候会对内存地址进行分配,所以操作系统为每个进程统一内存,还原一个程序所需要的内存环境,我们称之为虚拟地址空间。如下图(虚拟地址空间)的分布。

系统的硬件组成

csapp-深入理解计算机系统学习记录

典型系统的硬件构成

  • 总线

    贯穿整个系统的是一组电子管道,称为总线 ,它携带信息字节,并负责在各个部件之间传递,通常总线被设计成传送定长的字节块,就是字(word),字中的字节数(字长)是一个基本的系统参数,现在大多数机器字长有4个字节(32位)或8字节(64位)

  • I/O设备

    每个I/O设备都通过一个控制器或者适配器和I/O总线相连,控制器是I/O设备本身或者系统的主印制电路板(主板)上的芯片组,而适配器则是一块插在主板插槽上的卡,无论如何,它们的功能都是为了在I/O总线和I/O设备之间传输数据

  • 主存

    主存是一个临时存储设备,在处理器执行程序的时候,用来存放程序和程序处理的数据,从物理上说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上说,存储器是一个线型的字节数组,每个字节都有其唯一的索引(数组索引),这些地址是从零开始的,一般来说,组成程序的每条机器指令都由不同数量的字节构成,比如在运行Linux的x86-64机器上,short类型的数据需要2个字节,int和float类型需要4个字节,而long和double需要8个字节

  • 处理器

    *处理单元(CPU),简称处理器,是解释(或运行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中某条机器语言指令(即含有该条指令的地址)

    处理器看上去是一个非常简单的指令执行模型来操作的,这个模型是由指令集架构决定的,在这个模型中,指令按照严格的顺序执行,而执行一条指令包含一系列的步骤,处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,而这条指令并不一定和在内存中刚刚执行的指令相邻

    这样简单的操作并不多,它们围绕着主存、寄存器文件和算数/逻辑单元(ALU)进行。寄存器文件是一个小的存储设备,由一些单个字长的寄存器组成,每个寄存器都有唯一的名字, ALU计算新的数据和地址值。下面是一些简单操作的例子,CPU在指令的要求下可能会执行这些操作

    • 加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来内容
    • 存储:从寄存器赋值一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容
    • 操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容
    • 跳转:从之灵本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值

高速缓存

系统化了大把时间把信息从一个地方挪动到另一个地方,hello程序从最初的在磁盘上,当程序加载时,它们被复制到主存,当处理器运行程序的时候,指令又从主存复制到了处理器上,这些复制就是开销,减慢了程序真正的工作

根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备,类似的,一个典型的寄存器文件只存储几百字节的信息,而主存中可存放几十亿字节,然而,处理器从寄存器文件中读数据比从主存中读取数据快乐至少100倍

高速缓存存储器:作为暂时的集结区域,存放处理器近期可能会需要的信息

位于处理器芯片上的L1高速缓存的容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快,一个容量为数十万的到数百万的L2高速缓存通过一条特殊的总线连接到处理器,进程访问L2高速缓存的时间要比访问L1高速缓存的时间长5倍,但是这仍比访问主存的时间快5~10倍,L1和L2高速缓存是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的,这些高速缓存的速度快是因为利用了高速缓存的局部性原理,即程序具有访问局部区域里的代码和数据的趋势,通过让高速缓存里存放尽可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成

存储设备形成层次结构

在处理器和一个较大较慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法已经成为了一个普遍的观念。

csapp-深入理解计算机系统学习记录

存储层次结构

在这个层次结构中,从上至下,设备的访问速度越来越慢,但是容量越来越大,并且每字节的造价也越来越便宜,寄存器文件在层次结构中位于最顶部,也就是第0级或者记为L0

存储器层次结构的主要思想是上一层的存储器作为第一层存储器的高级缓存,比如寄存器文件就是L1的高速缓存,L1就是L2的高速缓存以此类推

操作系统管理硬件

当shell加载和运行hello的时候,以及hello程序输出自己的信息的时候,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存,取而代之的是,它们依靠操作系统提供的服务,我们可以把操作系统看成是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试,都必须经过操作系统

操作系统的两个基本功能

  • 防止硬件被失控的应用程序滥用
  • 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备

操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能,文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示

csapp-深入理解计算机系统学习记录

操作系统的抽象表示

进程

像hello这样的程序在现代系统上运行的时候,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和I/O设备,处理器看上去就像在不间断地处理一条接一条地执行程序中的指令,即改程序的代码和数据是系统内存中唯一的对象

进程是操作系统对一个正在进行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件,而并发运行,则是说一个进程的指令和另一个进程的指令是相互交替执行的,在大多数系统中,需要运行的进程数量是多于可以运行它们的CPU个数的,传统系统在一个时刻只能执行一个程序,而现今的多核处理器同时可以执行多个程序,无论是在单核还是多核的系统中,一个CPU看上去都像是在并发地执行多个经常,这是通过处理器在进程间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换

操作系统保持跟踪进程运行所需的所有状态信息,这种状态就是上下文,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码,当操作系统决定要把控制权从当前进程转移到某个新进程的时候,就会进行上下文切换,即保存当前的进程上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始

举一个shell和hello两个进程并发的例子,最开始,只有shell进程在运行,即等待命令行上的输入,当我们让他运行hello程序时,shell通过调用一个专门的函数,即系统调用

系统调用会将控制权传递给操作系统,操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传递给hello进程,hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入

从一个进程到另一个进程的转换是由操作系统内核管理的。内核是操作系统代码常驻主存的部分,当应用程序需要操作系统的某些操作的时候,比如读写文件,它就执行一条特殊的系统调用(system call)命令,将控制权传递给内核,然后内核执行被请求的操作并返回应用程序,内核不是一个独立的进程。相反它是系统管理全部进程所用代码和数据结构的集合

csapp-深入理解计算机系统学习记录

进程的上下文切换

线程

一个进程实际可以由多个称为线程的执行单元构成,每个线程运行在进程的上下文中,并共享同样的代码和全局数据,县城成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效,当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法

虚拟内存

虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占的使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间

csapp-深入理解计算机系统学习记录

进程的虚拟地址空间

在Linux系统中,地址空间最上面的区域是保留给操作系统中代码和数据的,这对所有进程来说都是一样,地址空间底部区域存放用户进程定义的代码和数据,上图中地址是从下向上增大的

  • 程序代码和数据
    对所有进程来说,代码是从同一固定的地址开始,紧接着的适合C区安居变量相对应的数据位置,代码和数据区是直接是直接按照可执行目标文件的内容初始化的

  • 代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准函数的时候,堆可以在运动时动态的拓展和收缩
  • 共享库
    大约在地址空间的中间部分是一块用来存放C标准库和数学库这样的共享库的代码和数据的区域

  • 位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用,和堆一样,栈在程序执行期间可以动态扩展和收缩,当调用一个函数的时候栈就会被扩展,一个函数返回的时候栈就会收缩
  • 内核虚拟内存
    地址空间顶部是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数,它们必须通过内核来执行这些操作,虚拟内存的运作基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存

并发和并行

我们用的术语并发是一个通用的概念,指一个同时具有多个活动的系统;而术语并行指的是用并发来使一个系统运行得更快,并行可以在计算机系统的多个抽象层次上运用

线程级并发

传统意义上线程级别上的并发只是模拟出来的,是通过是一台计算机在它正在执行的进程间快速切换来实现的,就好像一个杂耍艺人保持多颗杂技球在空中飞舞一样

指令级并行

在较低的抽象层次上,现代处理器可以同时执行多条指令的属性叫做指令级并行,如果处理器可以达到比一个周期一个指令更快的执行速率就称之为超标量处理器

第2章:信息表示和处理

2.1 信息存储

前言:大致的数的表示,数的运算在机组课上已经有老师带领全部学习了一遍,这里主要以复习提升为主。

  1. 重要概念

1)字节:大多数计算机使用8位的块(字节),作为最小的可寻址的内存单位,而不是访问内存中单独的位。
2)虚拟内存:机器级程序将内存视作一个很大的字节数组,称作虚拟内存。
\3) 地址:内存的每一个字节都有一个唯一的数字来标识,称为它的地址。
4)虚拟地址空间:所有可能的地址的集合称为虚拟地址空间。

  • 这个虚拟地址空间只是一个展示给机器级程序的概念性映像。
  • 实际的实现(见第9章)是将动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为应用程序提供一个看上去统一的字节数组。

5)程序对象:程序数据、指令和控制信息。

2.1.1 十六进制表示法

  1. 为什么选择十六进制

1)一个字节由8位组成,在二进制表示法中,它的值域是 0000000 0 2 ∼ 1111111 1 2 00000000_2\sim 11111111_2 000000002∼111111112,如果看成十进制就是 0 10 ∼ 25 5 10 0_{10}\sim 255_{10} 010∼25510.
2)两种表示法对于描述位模式都十分不方便。二进制表示法太冗长,十进制表示法与位模式的转化十分麻烦。

  1. 十六进制表示法
  • 1)十六进制数:

    十六进制使用0~9,以及字符A ~ F来表示16个可能的值,
    一个字节的值域为 0 0 16 ∼ F F 16 00_{16}\sim FF_{16} 0016∼FF16
    在C语言中,以0x或者0X开头的数字常量被认为是十六进制的数。字符‘A’ ~ ‘F’既可以是大写也可以是小写,例如我们可以将 F A 1 D 37 B 16 FA1D37B_{16} FA1D37B16写作 0 x F A 1 D 37 B 0xFA1D37B 0xFA1D37B,或者 0 x f a 1 d 37 b 0xfa1d37b 0xfa1d37b
    

2)十六进制和二进制之间的转换

  • csapp-深入理解计算机系统学习记录

  • 注意:将二进制数字转化为十六进制的时候,要把二进制数字分割为每四个一组,如果总数不是四的倍数,最左边一组可以少于四位,前面用零补足。然后将每个四位组转化为相应的十六进制数字。
    当值x是2的幂时,也就是,对于某个n,x= 2 n 2^n 2n,我们可以很容易地将x写成十六进制形式。只要记住x的二进制表示就是1后面跟n个零。十六进制数字О代表四个二进制0。所以,对于被写成i+4j形式的n来说,其中0≤i≤3,我们可以把x写成开头的十六进制数字为1(i=0)、2(=1)、4 ( i=2)或者8(i=3),后面跟随着j个十六进制的0。比如,x=2048= 2 11 2^{11} 211,我们有n=11 =3+4·2,从而得到十六进制表示0x800。
    

    3)十六进制和十进制之间的转换

    十进制和十六进制表示之间的转换需要使用乘法或者除法来处理一般情况。将一个十进制数字x转换为十六进制,我们可以反复地用16除x,得到一个商q和一个余数r,也就是x=q· 16+r。然后,我们用十六进制数字表示的r作为最低位数字,并且通过对q反复进行这个过程得到剩下的数字。例如,考虑十进制314156的转换:
    314156 = 19634 ⋅ 16 + 12 ( C ) 19634 = 1227 ⋅ 16 + 2 ( 2 ) 1227 = 76 ⋅ 16 + 11 ( B ) 76 = 4 ⋅ 16 + 12 ( C ) 4 = 0 ⋅ 16 + 4 ( 4 )
    314156196341227764=19634⋅16+12(C)=1227⋅16+2(2)=76⋅16+11(B)=4⋅16+12(C)=0⋅16+4(4)
    

    314156196341227764=19634⋅16+12©=1227⋅16+2(2)=76⋅16+11(B)=4⋅16+12©=0⋅16+4(4)
    从这里,我们能读出十六进制表示为0x4CB2C.
    反过来,将一个十六进制数字转换为十进制数字,我们可以用相应的16的幂乘以每个十六进制数字。比如,给定数字Ox7AF,我们计算它对应的十进制值为716+ 1016+ 15=7256+1016+ 15= 1792+ 160+ 15= 1967。

2.1.2 字

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

2.1.3 数据大小

  • csapp-深入理解计算机系统学习记录

2.1.4 寻址和字节顺序

1.地址

  • 在几乎所有的机器上,多字节对象被存储为连续的字节序列,对象的地址为所使用的的字节序列的最小地址。
  • 例如,假设一个类型为int 的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么,x的四字节将被存储在存储器的0x100、0x101、0x102和0x103位置。

2.字节排序的两个通用规则:小端法&大端法

  • 对表示一个对象的字节序列排序,有两个通用的规则。考虑一个w位的整数,有位表示 [ x w − 1 , x w − 2 , ⋯   , x 1 , x 0 ] [\left.x_{w-1},x_{w-2}, \cdots, x_{1}, x_{0}\right] [xw−1,xw−2,⋯,x1,x0],其中 x w − 1 x_{w-1} xw−1是最高有效位,而 x 0 x_{0} x0是最低有效位。假设w是8的倍数,这些位就能被分组成为字节,其中最高有效字节包含位 [ x w − 1 , x w − 2 , ⋯   , x w − 8 ] \left[x_{w-1}, x_{w-2}, \cdots, x_{w-8}\right] [xw−1,xw−2,⋯,xw−8],而最低有效字节包含位 [ x 7 , x 6 , ⋯ x 0 ] \left[x_{7}, x_{6}, \cdots x_0\right] [x7,x6,⋯x0],其他字节包含中间的位。某些机器选择在存储器中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则—–最低有效字节在最前面的方式被称为小端法(little endian)。大多数源自以前的Digital Equipment 公司(现在是Compaq公司的一部分)的机器,以及 Intel的机器都采用这种规则。后一种规则(最高有效字节在最前面的方式)被称为大端法(big endian)。IBM、Motorola和Sun Microsystems 的大多数机器都采用这种规则。注意我们说的是“大多数”。这些规则并没有严格按照企业界限来划分。比如,IBM制造的个人计算机使用的是Intel兼容的处理器,因此就是小端法。许多微处理器芯片,包括Alpha和Motorola的 PowerPC,能够运行在任一种模式中,其取决于芯片加电启动时确定的字节顺序规则。
  • 继续我们前面的示例,假设变量x类型为int,位于地址0x100 处,有一个十六进制值为0x01234567。地址范围0x100~0x103的字节顺序依赖于机器的类型:
    csapp-深入理解计算机系统学习记录
    注意,在字0x01234567中,高位字节的十六进制值为0x01,而低位字节值为0x67。

3.字节顺序变得重要的三种情况

1)小端法机器产生的数据被发送到大端法机器或者反之时,接收程序会发现,字里的字节变成了反序。为了避免这类问题,网络应用程序必须建立关于字节顺序的规则,以确保发送机器将它的内部表示转换为网络标准,而接收方机器则将网络标准转换为它的内部表示。
2)字节顺序变得重要的第二种情况是当阅读表示整数数据的字节序列时。这通常发生在检查机器级程序时。作为一个示例,从某个文件中摘出了下面这行代码,该文件给出了一个针对Intel 处理器的机器级代码的文本表示: 80483 b d : 01 05 64 94 04 08 add % eax, 0 × 8049464
80483bd:010564940408 add % eax, 0×8049464
80483bd:010564940408 add % eax, 0×8049464这一行是由反汇编器((disassembler)生成的,反汇编器是一种确定可执行程序文件所表示的指令序列的工具。我们将在下一章中学习有关这些工具的更多知识,以及怎样解释像这样的行。而现在,我们只是注意这行表述了十六进制字节串01 05 64 94 04 08是一条指令的字节级表示,这条指令是增加一个字宽的数据到存储在主存地址Ox8049464的值上。如果我们取出这个序列的最后四字节:64940408,并且按照相反的顺序写出,我们得到08049464。去掉开头的零,我们就得到值Ox8049464,就是右边写着的数值。当阅读像此例中一样的小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,但是这和书写数字时最高有效位在左边,最低有效位在右边的通常方式是相反的。
3)字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在C语言中,可以通过使用强制类型转换或者联合来允许以一种数据类型来引用一个对象,而这种数据类型与创建这个对象时的定义的数据类型不同。

2.1.5 表示字符串

  • C语言中的字符串被编码成一个以null(其值为零)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII编码。
  • 在使用ASCII码作为字符码的任何系统上运行show_bytes程序,都将得到相同的结果,与字节顺序和字的大小规则无关。
    -因而文本数据比二进制数据具有更强的平*立性。

2.1.6 表示代码

  • 指令编码是不同的。
    • 不同的机器类型使用不同的且不兼容的指令和编码类型。
    • 完全一样的进程,运行在不同的操作系统上也有不同的编码规则。因此二进制代码是不兼容的。

2.1.7 布尔代数简介

  1. 布尔代数:围绕数值0和1的数学知识体系。
  2. 0和1的运算
    csapp-深入理解计算机系统学习记录
  3. 位向量的运算:
    csapp-深入理解计算机系统学习记录
  • 用位向量表示有限集合:

    a = [ 01101001 ] 可 以 表 示 A = { 0 , 3 , 5 , 6 } a=[01101001]可以表示A=\{0,3,5,6\} a=[01101001]可以表示A={0,3,5,6}
    b = [ 01010101 ] 可 以 表 示 B = { 0 , 2 , 4 , 6 } b=[01010101]可以表示B=\{0,2,4,6\} b=[01010101]可以表示B={0,2,4,6}
    布尔运算 ∣ | ∣和 & \& &分别对应于集合的并和交,而 ∼ \sim ∼对应于集合的补
    

2.1.8 C语言中的位级运算

  1. 示例
    csapp-深入理解计算机系统学习记录
  2. 掩码运算:
  • 例如: x = 0 x 89 A B C D E F 做 掩 码 运 算 x & 0 x F F = 0 x 000000 E F x=0x89ABCDEF做掩码运算x& 0xFF=0x000000EF x=0x89ABCDEF做掩码运算x&0xFF=0x000000EF

2.1.9 C语言中的逻辑运算

  1. 逻辑运算容易与位级运算混淆,注意比较以下例子:
    csapp-深入理解计算机系统学习记录
  2. 位级运算与逻辑运算的区别:
  • 逻辑运算认为所有非零的参数都表示TRUE,参数零表示FALSE,返回值为1或者0.
  • 逻辑运算符如果对第一个参数求值就能确定表达式的值,那么逻辑运算符就不会对第二个参数求值。

2.1.10 C语言中的移位运算

  1. 示例csapp-深入理解计算机系统学习记录
  • 移位运算从左往右可结合
  • 右移运算包括逻辑右移算数右移

2.2 整数表示

2.2.1整数数据类型

csapp-深入理解计算机系统学习记录

2.2.2 无符号数编码

csapp-深入理解计算机系统学习记录

无符号编码属于相对较简单的格式,因为它符合我们的惯性思维,上述定义其实就是对二进制转化为十进制的公式而已,只不过在一向严格的数学领域来说,是要给予明确的含义的。

2.2.3 补码编码

最常见的有符号数的计算机表示方式就是补码形式。在这个定义中,将字的最高有效位解释为负权,我们用函数B2T来表示。java中使用的就是补码。

csapp-深入理解计算机系统学习记录

我们观察这个公式,不难看出,补码格式下,对于一个w位的二进制序列来说,当最高位为1,其余位全为0时,得到的就是补码格式的最小值,即
csapp-深入理解计算机系统学习记录
而当最高位为0,其余位全为1时,得到的就是补码格式的最大值,根据等比数列的求和公式,即
csapp-深入理解计算机系统学习记录

2.2.4 有符号数和无符号数之间的转换

在C语言当中,我们经常会使用强制类型转换,而在之前的章节中,也提到过强制类型转换。强制类型转换不会改变二进制序列,但是会改变数据类型的大小以及解释方式,那么考虑相同整数类型的无符号编码和补码编码,数据类型的大小是没有任何变化的,变化的就是它们的解释方式。比如1000这个二进制序列,如果用无符号编码解释的话就是表示8,而若采用补码编码解释的话,则是表示-8。

一、补码转换为无符号数:

csapp-深入理解计算机系统学习记录

二、无符号数转换为补码:

csapp-深入理解计算机系统学习记录

2.2.5 C语言中的有符号数和无符号数

有符号数和无符号数的本质区别其实就是采用的编码不同,前者采用补码编码,后者采用无符号编码。

在C语言中,有符号数和无符号数是可以隐式转换的,不需要手动实施强制类型转换。不过也正是因为如此,可能你不小心将一个无符号数赋给了有符号数,就会造成出乎意料的结果,就像下面这样。

#include <stdio.h>

int main(){
    short i = -12345;
    unsigned short u = i;
    printf("%d %d\n",i,u);
}
1234567

输出结果为-12345,53191。一个不小心,一个负数就变成正数了。

再看下面这个程序,它展示了在进行关系运算时,由于有符号数和无符号数的隐式转换所导致的违背常规的结果。

#include <stdio.h>

int main(){
    printf("%d\n",-1 < 0U);
    printf("%d\n",-12345 < 12345U);
}
123456

两个结果都为0,也就是false,这与我们直观的理解是违背的,由于C语言对同时包含有符号和无符号数表达式的这种处理方式,出现了一些奇特的行为。当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。就像我们将要看到的,这种方法对于标准的算术运算来说并无多大差异,但是对于像<和>这样的关系运算符来说,它会导致非直观的结果。

2.2.6 扩展一个数字的位表示

当我们将一个短整型的变量转换为整型变量时,就涉及到了位的扩展,此时由两个字节扩充为四个字节。

在进行位的扩展时,最容易想到的就是在高位全部补0,也就是将原来的二进制序列前面加入若干个0,也称为零扩展。还有一种方式比较特别,是符号扩展,也就是针对有符号数的方式,它是直接扩展符号位,也就是将二进制序列的前面加入若干个最高位。

  • 无符号数的零扩展:要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示的开头添加0。这种运算被称为零扩展。
  • 补码数的符号扩展:要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加最高有效位的值。

对于零扩展来说,很明显扩展之后的值与原来的值是相等的,而对于符号扩展来说,也是一样,只不过没有零扩展来的直观。我们在计算补码时有一个比较简单的办法,就是符号位若为0,则与无符号是类似的。若符号位为1,也就是负数时,可以将其余位取反最终再加1即可。因此当我们对一个有符号的负数进行符号扩展时,前面加入若干个1,在取反之后都为0,因此依旧会保持原有的数值。

总之,在对位进行扩展时,是不会改变原有数值的。

2.2.7 截断数字

截断与扩展相反,它是将一个多位二进制序列截断至较少的位数,也就是与扩展是相反的过程。截断可能会导致数据的失真。

一、对于无符号编码来说,截断后就是剩余位数的无符号编码数值

csapp-深入理解计算机系统学习记录

二、 对于补码编码来说,截断后的二进制序列与无符号编码是一样的,因此我们只需要多加一步,将无符号编码转换为补码编码就可以了。

csapp-深入理解计算机系统学习记录

不难看出,具有有符号和无符号数的语言,可能会因此引起一些不必要的麻烦,而且无符号数除了能表示的最大值更大以外,似乎并没有太大的好处。因此有很多语言是不支持无符号数的。如Java语言,就只有有符号数,这样省去了很多不必要的麻烦。无符号数很多时候只是为了表示一些无数值意义的标识,比如我们的内存地址,此时的无符号数就有点类似于数据库主键或者说键值对中的键值的概念,仅仅是一个标识而已。

lab1 Data Lab(未完待续)

快速开始请访问 CSAPP https://link.zhihu.com/?target=http%3A//csapp.cs.cmu.edu/3e/labs.html官网

开始做 CSAPP 的实验了,这次是第一次实验,内容是关于计算机信息的表示,主要是位操作、整数题和浮点数相关的题。

第一次实验流程还不是很熟练,跟着大佬的操作在一步步尝试ing~

题目列表

名称 描述 难度 指令数目
bitXor(x,y) 只使用 ~& 实现 ^ 1 14
tmin() 返回最小补码 1 4
isTmax(x) 判断是否是补码最大值 1 10
allOddBits(x) 判断补码所有奇数位是否都是 1 2 12
negate(x) 不使用负号 - 实现 -x 2 5
isAsciiDigit(x) 判断 x 是否是 ASCII 3 15
conditional(x, y, z) 类似于 C 语言中的 x?y:z 3 16
isLessOrEqual(x,y) x<=y 3 24
logicalNeg(x) 计算 !x 而不用 ! 运算符 4 12
howManyBits(x) 计算表达 x 所需的最少位数 4 90
floatScale2(uf) 计算 2.0*uf 4 30
floatFloat2Int(uf) 对于浮点参数 f,返回 (int) f 的位级等价数 4 30
floatPower2(x) 对于整数 x,返回 2.0^x 4 30

题解

图片待补充

bitXor(x,y)

只使用两种位运算实现异或操作。这个算是一个比较简单的问题了,难度系数 1。学数电和离散二布尔代数的时候了解过。

代码

  • /* 
     * bitXor - x^y using only ~ and & 
     *   Example: bitXor(4, 5) = 1
     *   Legal ops: ~ &
     *   Max ops: 14
     *   Rating: 1
     */
    int bitXor(int x, int y) {
      return ~(~x&~y)&~(x&y);
    }
    
  • 思路

    根据布尔代数,可以通过 ~& ,即非和与操作实现异或操作。所谓异或就是当参与运算的两个二进制数不同时结果才为 1,其他情况为 0。C 语言中的位操作对基本类型变量进行运算就是对类型中的每一位进行位操作。所以结果可以使用 “非” 和 “与” 计算不是同时为 0 情况和不是同时为 1 的情况进行位与,即 ~(~x&~y)&~(x&y)

  • tmin()

    使用位运算获取对 2 补码的最小 int 值。这个题目也是比较简单。

    • 代码

    • /* 
       * tmin - return minimum two's complement integer 
       *   Legal ops: ! ~ & ^ | + << >>
       *   Max ops: 4
       *   Rating: 1
       */
      int tmin(void) {
        return 0x1<<31;
      }
      
    • 思路

      C 语言中 int 类型是 32 位,即 4 字节数。**补码最小值就是符号位为 1,其余全为 0。**所以只需要得到这个值就行了,我采用的是对数值 0x1 进行移位运算,得到结果。

    isTmax(x)

    通过位运算计算是否是补码最大值。

    • 代码
    /*
     * isTmax - returns 1 if x is the maximum, two's complement number,
     *     and 0 otherwise 
     *   Legal ops: ! ~ & ^ | +
     *   Max ops: 10
     *   Rating: 1
     */
    int isTmax(int x) {
      int i = x+1;//Tmin,1000...
      x=x+i;//-1,1111...
      x=~x;//0,0000...
      i=!i;//exclude x=0xffff...
      x=x+i;//exclude x=0xffff...
      return !x;
    }
    

    思路

    做这个题目的前提就是必须知道补码最大值是多少,这当然是针对 int 类型来说的,最大值当然是符号位为 0,其余全是 1,这是补码规则,不明其意则 Google。在此说一下个人理解,最终返回值为 0 或 1,要想判断给定数 x 是不是补码最大值(0x0111,1111,1111,1111),则需要将给定值 x 向全 0 值转换判断,因为非 0 布尔值就是 1,不管你是 1 还是 2。根据我标注的代码注释理解,如果 x 是最大值,将其转换为全 0 有很多方法,不过最终要排除转换过程中其他的数值,比如本例子中需要排除 0xffffffffffffffff 的情况:将 x 加 1 的值再和 x 相加,得到了全 1(函数第二行),然后取反得到全 0,因为补码 - 1 也有这个特点,所以要排除,假设 x 是 -1,则 +1 后为全 0,否则不为全 0,函数 4-5 行则是排除这种情况。
    isTmax(int x) {
    int i = x+1;//Tmin,1000…
    x=x+i;//-1,1111…
    x=~x;//0,0000…
    i=!i;//exclude x=0xffff…
    x=x+i;//exclude x=0xffff…
    return !x;
    }

思路

做这个题目的前提就是必须知道补码最大值是多少,这当然是针对 int 类型来说的,最大值当然是符号位为 0,其余全是 1,这是补码规则,不明其意则 Google。在此说一下个人理解,最终返回值为 0 或 1,要想判断给定数 x 是不是补码最大值(0x0111,1111,1111,1111),则需要将给定值 x 向全 0 值转换判断,因为非 0 布尔值就是 1,不管你是 1 还是 2。根据我标注的代码注释理解,如果 x 是最大值,将其转换为全 0 有很多方法,不过最终要排除转换过程中其他的数值,比如本例子中需要排除 0xffffffffffffffff 的情况:将 x 加 1 的值再和 x 相加,得到了全 1(函数第二行),然后取反得到全 0,因为补码 - 1 也有这个特点,所以要排除,假设 x 是 -1,则 +1 后为全 0,否则不为全 0,函数 4-5 行则是排除这种情况。

上一篇:k8s学习笔记,问题处理【二进制部署的node节点IP被占用,重新分配IP后如何加入集群】


下一篇:ZooKeeper部署