计算机系统的本质核心概念

总结贴:理解并记录 CSAPP 提及的计算机系统核心概念

Posted by ZHR on March 22, 2018

本文随着刷书过程更新。

本文大多数内容来自于《深入理解计算机系统 第三版》。

计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。虽然系统的具体实现方式随着时间不断变化,但是系统内在的概念却没有改变。(P1)

信息的表示

  • 系统中所有的信息都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。(P2)
    • 信息 = 位 + 上下文。上下文决定了对一串比特的解释方式。因此,在不同的上下文下,同一个字节序列可能表示一个整数、浮点数、字符串或者机器指令。
  • 作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不同的。它们是对真值的有限近似值,有时候会有意想不到的行为表现。(P2)

C

  • C 语言标准不仅定义了 C 语法,还定义了一系列函数库,即所谓的 C 标准库。
  • C 语言小而简单。
  • C 语言的指针是造成程序员困惑和程序错误的一个常见原因。
  • C 语言缺乏对非常有用的抽象的显式支持,例如类、对象和异常。(P3)
  • C 语言是一种很小的语言,如果不使用外部库,它几乎什么也干不了。为了告诉编译器要使用哪些外部代码,需要包含include相关库的头文件。stdio.h是最常见的头文件,stdio 库包含了那些能在终端读写数据的代码。

main

  • 当计算机在运行程序时,需要一些方法来判断程序是否运行成功,计算机通过检查main函数的返回值来做到这一点。如果main返回0,那么表示程序运行成功;如果返回其他值,则表示程序在运行时出现了问题。
  • 在早期 ANCI C 标准中,main函数的返回值类型可以为void。但在 C99 标准中,main的返回值类型必须为int
  • 可以在终端输入echo $?命令来查看程序运行的状态。

变量

  • 每当声明一个变量,计算机就会为它在存储器中创建空间。
  • 在函数中声明的变量保存在栈中。
  • 在函数外声明的变量保存在全局量区。
  • 使用“取地址运算符” &可以获得变量的地址。
    • C 语言按值传递参数:调用函数时,作为参数传递的不是变量,而是变量的值。

字符串

  • C 语言比其他大多数语言的抽象层次更低,因此它不提供字符串,而是用了相似的东西来代替:以字符位元素的数组。
  • C 语言不知道数组的大小,因此判断是否已经到达数组末尾时,C 语言是通过 '\0'进行判断的。
  • 数组元素的索引值是一个偏移量,它表示当前引用的元素距离第一个元素存在多少个单位长度。

指针和数组

  • 在 C 语言中,我们能用数组表示法来引用指针,同时我们也能用指针表示法来引用数组元素。
  • 使用指针,可以不用传递整份数据。
  • 使用指针,可以让另个程序处理同一条数据。
  • 指针是一种间接形式的地址。
  • *& 运算符刚好相反。&接收一个数据,然后得出该数据的地址。*接收一个地址,然后得出该地址的值。
  • 当创建了一个数组,数组变量就可以当做指针使用,它指向数组在存储器中的起始地址。
  • 当将一个字符串常量赋值给一个字符数组,计算机会为字符串的每一个字符以及结束字符\0在栈上分配空间,并把首字符的地址和数组变量关联起来,代码中只要出现这个数组变量,计算机就会把它换成字符串首字符的地址。
    • 看编译后的代码来验证:数组变量的本质是一个指针。但在一些操作上,对它们的处理还是不同,即数组变量与指针不完全相同j。
    • 当创建指针变量时,计算机会为它分配4或8字节的存储空间。当创建数组时,计算机会为数组分配存储空间,但不会为数组变量分配任何空间,编译器仅仅在出现它的地方把它替换成数组的起始地址。
    • 正是由于计算机不会为数组变量分配存储空间,也就不能把它指向其他地方。
    • 对于数组变量 s 而言,有:&s==s
  • 指针变量只不过是一个保存数字的变量。
    • 可以使用 & 运算符找到指针变量的地址。
    • 可以将指针变量的值保存在普通的变量中。

逻辑运算

  • 在 C 语言中,布尔值使用数字表示的。0 表示假,非 0 表示真。
  • ANSI C 标准中没有用来表示真和假的值,C 程序把 0 当作假处理,把非 0 当作真处理。C99 标准则允许在程序中使用 true 和 false 关键字,但编译器还是会把它们当做 1 和 0 来处理。
  • |&不仅可以进行逻辑运算,还可以进行布尔运算。

函数

  • 编译器会把运算符编译成一串指令;而当程序调用函数时,会调到一段独立的代码中执行。
  • 编译器可以在编译时确定存储空间的大小:sizeof 运算符。

其他

  • 在类 Unix 操作系统中,运行程序必须指定程序所在的目录,除非程序的目录已经列在 Path 环境变量中。
  • 最初,创造 C++ 和 Objective-C 的目的都是为了用 C 语言写面向对象的程序。
    • 面向对象是一种对抗软件复杂性的技术。

关于虚拟地址空间的理解:计算机的存储层次架构中包含了主存,硬盘等,那么作为程序员如何通过编程来使用这些存储设备呢?操作系统对存储层次结构进行了抽象得到虚拟地址空间。进程对于存储设备的操作,其实是对操作系统提供的虚拟地址空间进行的。比如,C 语言中一个指针的值其实是某个存储块的第一个字节的虚拟地址。

这个概念其实是从操作系统和进程的角度进行理解的。

虚拟地址是以一个字长进行编码的。

因此,在一个字长为 w 的机器上,其虚拟地址的范围为 也就是说,进程最多能访问的字节数为


疑问:64 位和 32 位,分别从硬件和软件的角度如何理解。


编译系统

  • 编译系统由预处理器、编译器、汇编器和链接器组成,它们对应着整个编译过程的四个阶段:预处理阶段、编译阶段、汇编阶段和链接阶段。(P3)从下图可以看出,源程序被其他程序翻译成不同的格式。

compilation system

  • 了解编译系统如何工作是大有益处的。(P4:这一部分属于 CSAPP 的脉络)
    • 优化程序性能
    • 理解链接时出现的错误
    • 避免安全漏洞

汇编语言

  • 汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。(P3)

Shell

  • Shell 是一个命令行解释器。(P5)

系统的硬件组成

  • 总线(P5)
    • 总线是一组贯穿整个系统的电子管道,它携带信息字节并负责在各个部件间传递。
    • 通常总线被设计成传送定长的字节块,也就是字(word)。
    • 字中的字节数(即字长)是一个基本的系统参数,各个系统都不尽相同。
  • I/O 设备
    • I/O 设备是系统与外部世界的联系通道。
    • 每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连。
  • 主存
    • 主存是由一组动态随机存取存储器芯片组成的。
    • 从逻辑上说,存储器是一个线性的字节数组,每个字节都有其唯一的地址。
  • 处理器
    • 处理器的核心是一个大小为一个字的存储设备。

高速缓存

出现高速缓存和存储器层次结构的原因:在计算机各个部件之间复制带来了开销,这种开销与处理速度越来越快的处理器的矛盾造成了计算机的速度瓶颈。上层的高速缓存利用局部性原理减少了复制的时间。

数的表示

进制

进制产生是为了解决如何表示无限个数值的表示问题,进制解决这个问题的做法就是用有限个元素来表示无限个元素。

进制之间的转换见 C 语言入门Page192

  • 整数和浮点数的表示
  • 整数运算和浮点数运算的属性
  • 编译器如何对算术运算进行优化

关于 C 语言可移植性的理解:

  • Unix 系统的最早版本是使用汇编语言编写的,汇编语言是机器级语言。为了让 Unix 系统在不同的机器上也可以运行,必须使用非机器级语言来编写。于是 C 语言就被开发出来了。从 C 语言实现的角度来看,C 语言是对机器级语言的抽象,或者说是对机器的抽象。
  • 更近一步,C 语言是对指令集体系结构进行封装(一条高级语言语句对应多条指令)。只要采用相同指令集体系结构的机器,C 语言编写的程序都是可移植的。

关于编译的思考:

  • 什么是编译?从编译器的角度看,编译的本质是什么?编译器视角理解的概念与机器、人类理解的有什么不同?要解决这些问题,可能要学习编译原理。
  • 从机器的角度看,没有数据类型的概念,只有程序对象的概念。数据类型的概念是编译器中的的概念。编译器作用是将数据类型的概念转换成机器中程序对象的概念。比如整型值转换为机器码。
  • C 语言中有 char short int long 等数据类型,但从机器的眼中只有补码,这些不同的整型对于机器而已不过是不同字节数表示的补码。也就是说,机器有处理表示为 2 字节、4 字节或者 8 字节整数的指令。(Page 27 倒数第二段)
  • 为了避免不同编译器和机器设置而导致的数据类型字节数的不同,尽量使用 C99 引入的一类数据类型,该数据类型的大小是固定的,不随编译器和机器设置而变化,如 int32_t int64_t
  • 尽管有些语言不是编译型语言,但它们中的一些,像 JavaScript 和 Python,为了提供速度,通常会在幕后使用一些编译技术。

关于大端和小端的思考:

  • 机器?还是操作系统?决定大端或小端。
  • 应该是机器决定大小端的问题。考虑一条处理 4 字节整数加法的指令:指令体系结构只是决定了该指令的功能,但具体如何实现是由微体系结构决定的。
  • 但是可能由于某些原因,比如战略原因,有些特定的操作系统只支持小端,比如 iOS 和 Android。

gcc:

  • gcc 拥有前端和后端。
  • 不同的前端用于将不同语言的源代码转换为一种中间代码。
  • 后端则是一个将中间代码转换为多种平台的机器代码的系统。

整数的表示

  • 从数学出发,掌握用于精确定义和描述计算机如何编码和操作整数的数学术语,掌握不同编码形式的数学属性,掌握机器级的实现。

  • 记住书中图2-8。
  • 分清楚两个词:整数和整型数据类型
    • 整数:数学领域的术语。
    • 整型数据类型:编程语言中的术语,简单地说,用于表示数学领域中有限范围的整数。
    • 整数是数学上的概念,编码(用位来编码整数)也可以认为是数学上的概念,整型数据类型则是计算机编程领域的概念。

无符号编码

  • 数学定义
  • 权重的形象表示:权重条
  • 最大值和最小值
  • 双射

补码编码

  • 数学定义:$B2T_w(\vec{x}) \dot= -x_{w-1} 2^{w-1} + \sum_{i=0}^{w-2} x_{i} 2^{i}$
  • 最高有效位权重:$-2^{w-1}$
  • 负数的特点:最高有效位的值为 1
  • 非负数的特点:最高有效位的值为 0
  • 最小值:$-2^{w-1}$
  • 最大值:$\sum_{i=0}^{w-2} x_i 2^i = 2^{w-1} - 1$
  • 双射
  • $\vert{TMin_w}\vert = \vert{TMax_w}\vert + 1$
  • $UMax_w = 2TMax_w + 1$
  • 求一个负数的补码表示,可以通过对其绝对值的补码表示取反加一得到。
  • j

Java VS C/C++

  • C/C++ 都支持有符号和无符号数,但 Java 只支持有符号数。
  • C 语言标准并没有要求要用补码形式来表示有符号整数。但是,Java 标准非常明确,它要求采用补码表示。