1.对程序员来说 CPU 是什么

  1. 程序是指计算机每一步动作的一组指令,程序是由指令和数据组成的
  2. 硬盘和磁盘等媒介上保存的程序被拷贝到内存后才能运行
  3. 内存地址是在内存中保存指令和数据存储位置的数值,是一个整数值
  4. 机器语言是 CPU 可以直接识别并使用的语言,可以理解为一个个电路状态的矩阵转换,高级编程语言必须编译为机器语言才能被CPU 处理
  5. CPU 范围更大表示计算机的中央处理单元,包含了微处理器芯片,但很多时候两者表达的意思作等同处理
  6. 程序运行流程:
    1. 高级语言编写程序
    2. 将程序编译后转换成机器语言的 EXE 文件
    3. 程序运行时,在内存中生成 EXE 文件的副本
    4. CPU 解释并运行程序内容
  7. 时钟信号(clock puzzle)的频率越高,CPU 的运行速度越快
  8. CPU 和内存是由许多晶体管组成的电子部件,通常称为 IC(Integrated Circuit,集成电路)
  9. CPU 构成:
    1. 寄存器:暂存指令、数据等处理对象,可理解为内存的一种;CPU 内部会有 20-100 个不同种类的寄存器
    2. 控制器:负责吧内存上的指令、数据读入寄存器,并根据指令的执行结果控制整个计算机
    3. 运算器:负责运算从内存读入寄存器的数据
    4. 时钟:负责发出 CPU 时钟信号(有些计算机的时钟信号位于 CPU 的外部)
      CPU的 4 个构成部分
  10. 在汇编语言中,是把寄存器作为对象来描述的,所以需要了解 CPU 中的寄存器部分,寄存器中的存储的内容既可以是指令也可以是数据,其中数据分为“用于运算的数值”和“表示内存地址的数值”(内存地址是一个整数值)。数据种类不同,存储改数值的寄存器也不同。下面是寄存器的主要种类和功能:
    1. 累加寄存器(accumulator register):存储执行运算的数据和运算后的数据
    2. 标志寄存器(flag register):存储运算处理后的 CPU 的状态,比如标志运算后的结果是负数、零还是正数,以及是否溢出和奇偶校验的结果,标志寄存器的数值会根据运算结果自动设定
    3. 程序计数器(program counter):存储下一条指令所在内存的地址
    4. 基址寄存器(base register):存储数据内存中的起始地址
    5. 变址寄存器(index register):存储基址寄存器的相对地址
    6. 通用寄存器(general purpose register):存储任意数据
    7. 指令寄存器(instruction register):存储指令,CPU 内部使用,程序员无法进行读写
    8. 栈寄存器(stack register):存储栈区域的起始地址
  11. 对程序员来说,CPU 是具有各种功能的寄存器的集合体,其中程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器可有多个:CPU 是寄存器的几何体
  12. 只有一行的有用程序是很少见的,机器语言的程序也是如此,操作系统将程序的机器语言加载到内存后,把程序第一个指令的地址设置到程序计数器中,并且程序计数器的值会自动加1,即 CPU 执行完程序的第一条指令后,会自动加载执行下一条指令。程序计数器决定着程序的执行流程。
  13. 函数的调用地址也是通过把程序计数器的值设定为函数的存储地址来实现的(但是函数执行完之后还需要跳转回来),机器语言的 call 指令是调用函数的,它在将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内,当函数处理完毕后,再通过函数的出口来执行 return 命令,return 命令的功能是把保存在栈中的地址设定到程序计数器中。
  14. CPU 会把基址寄存器+变址寄存器的值解释为实际查看的内存地址,变址寄存器的值就相当于高级编程语言程序中数组的索引功能
  15. CPU 的处理其实很简单,机器语言指令的主要类型和功能:
    1. 数据传送指令:寄存器和内存、内存和内存、寄存器和外围设备之间的数据读写操作
    2. 运算指令:用累加器执行算术运算、逻辑运算、比较运算和移位运算
    3. 跳转指令:实现条件分支、循环、强制跳转等
    4. call/return 指令:函数的指令/返回调用钱的地址

2.数据是用二进制数表示的

  1. 集成电路(IC)的引脚,只有直流电压 0V 和 5V 两个状态,也就说 IC 的一个引脚智能表示两个状态,IC 的这个特性决定了计算机的信息数据只能用二进制数来处理,二进制数不少专门为 IC 设计的,但是和 IC 的特性非常吻合。
  2. 计算机所处理的信息的基本单位是 8 位二进制数,被称为一个字节(byte);字节是基本单位,内存和磁盘都使用字节来存储和读写数据,位是最小单位。
  3. 计算机只存储信息,如何区分它是数值、文字还是某种图片的格式,取决于程序的编写方式。
  4. 二进制数表示负数值时,一般会把最高位作为符号位来使用
  5. 正数符号位为 0,而其他部分正常表示,负数使用补数来表示,负数使用补数来表示只会计算机的减法就可以变为加法,只需要加法器即可无需减法器了,在计算机运算时位溢出部分会被忽略
  6. 补数是在正数的基础上取反再+1 得到的,为了理解为什么补数这么定义,只需要使用 0-正数=负数 的二进制运算便明了了,由于 0 不够减便像更高位借位,减去后得到的结果和被减数比较,便会发现 负数=正数的取反+1。
  7. 算术右移对于正数用 0 补位,对于用补码表示的负数用 1 补位
  8. 符号扩充:8 位的二进制数扩充到 16 位和 32 位时,用符号位扩充到高位即可,如果是正数扩充 0,如果是补码表示的负数扩展 1,如 1111 1111 扩充到 16 位是:11111111 11111111
  9. 算术运算:将二进制表示的信息进行算术四则运算
  10. 逻辑运算:将数值处理为单纯的 0 和 1 的罗列,逻辑运算有 NOT/AND/OR/XOR 四种

3.计算机进行小数运算时出错的原因

  1. 浮点数是指把小数用“符号 尾数 * 基数的指数次幂”这种形式来表示
  2. 计算机可以准确的用二进制数来表示正式,但无法准确的用二进制数来表示小数,这是因为整数不是偶数就是奇数,偶数部分用2的幂次方相加总能表示出来,而2的0次幂是1也解决了奇数的问题,但是2的-1次幂、2的-2次幂等却不能表示所有连续的小数
  3. 双进度浮点型(double)用 64 位,单精度浮点型(float)用 32 位来表示全体小时
  4. 浮点数是指用符号、尾数(小数点后面的数字)、基数和指数这四部分来表示的小数:+-m*n^e,因为计算机内部使用的是二进制数,所以基数自然就是 2,所以实际的数据中往往缺省了基数 2,只用符号、尾数、指数这三部分即可表示浮点数,所以 double 的64位和 float 的32位的数据,会被分为 3 部分来使用,分别表示符号位、尾数部分和指数部分。
  5. 如十进制的 91.25 用二进制浮点数是 1001.001,将其表示成科学计数方式为 1.001012^3,在计算机中,任何一个数都可以表示成 1.xxxxx2^n 这样的形式,其中 xxxxx 部分就是尾数部分,n 表示指数部分,而其中因为最高位的 1 在二进制浮点数的科学计数法里面最高位总是 1,所以在存储的时候实际上并不保存这一位,这使得 float 的 23bit 的尾数部分可以表示 24bit 的精度,double 中的 52bit 的尾数可以表达 53bit 的精度:
    浮点数的内部构造(IEEE 的规定)
  6. Excess 系统:计算机内可以同时存储正数和负数而不用符号位的一种方法,在 Excess 系统中存在一个幻数,是寻址空间的中位值,比中位值大的表示正数,比中位值小的表示负数。在计算机的浮点数表示中,指数部分的负数就是使用 Excess 系统来表示的。对于 1-256 个数来说,幻数取 2^7-1即 127,所以 126 表示-1,128 表示+1。
  7. 二进制数很方便,但是位数表多后在程序里看起来比较麻烦,这个时候就有人想到了用 16 进制来表示,在数值的开头加上 0x 表示 16 进制数。

4.熟练使用有棱有角的内存

  1. 高级编程语言中的数据类型表示的是占据内存区域的大小和存储在该内存区域的数据类型
  2. 物理内存是以字节为单位进行数据存储的
  3. 内存实际上是一种名为内存 IC 的电子元件,内存 IC 中有电源(VCC +5V,GND 0V)、地址信号(A0~A9)、数据信号(D0~D7)、控制信号(RD(read), WR(write))等用于输入输出的大量引脚(IC 的引脚),通过为其指定地址,来进行数据的读写
    内存 IC
  4. 内存 IC 的物理机制实质上很简单,总体来讲其内部有大量可以存储 8 位数据(1 byte)的地方,通过地址来指定这些场所之后即可进行数据的读写。内存 IC 读写
  5. 将多字节数据的低位字节存储在内存低位地址的方式称为低字节序,于此相反,把数据的高位字节存储在内存低位的方式称为高字节序。
  6. 理解指针的关键点就是要弄清楚数据类型这个概念,指针也是一种变量,它所表示的不是数据类型的值,而是存储着数据的内存的地址,通过使用指针,就可以对任意指定地址的数据进行读写。
  7. C 语言中定义指针:char *d; 这里的 * 表示的是指针,而用 char 来指定,其实表示的就是从指针 d 存储的地址中一次能够读写的数据字节数(换成自定义类型也一样,类型在内存中占有的内存在编译时是可知的)。内存指针
  8. 数组是高效使用内存的基础,数组所有和内存地址的变换工作是由编译器自动实现的,CPU 是通过利用基址寄存器(保存数组的起始位置)和变址寄存器(数组的索引)来指定内存地址的。数组和物理内存的构造是一样的,所以数组被广泛的应用。
  9. 队列一般是以环状缓冲区(ring buffer)的方式来实现的:queue
  10. 链表使元素的追击和删除更容易,在数组的各个元素中,除了数据的值之外,通过为其附带上下各元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。
  11. 二叉查找树使数据搜索更有效,二叉查找树是在链表的基础上往数组追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。binar tree为了处理二叉查找树,其实数组的每个元素中只要有数据的值和两个索引信息就可以了。使用二叉查找树的便利之处在于可以使数据的搜索更有效率。
  12. 只要在程序开发中多花些心思,我们就可以熟练使用内存、实现栈处理、链表处理、二叉查找树处理等,为什么要进行这些处理?就是为了使用计算机的物理内存来对现实世界中的存储模型进行建模,而数组是进行这些处理的基础。

5.内存和磁盘的亲密关系

  1. 内存的基本单位是 1 byte,磁盘的一个扇区是 512 byte,扇区是磁盘保存数据的物理单位。内存利用电为存储信息,断电后内容消失,磁盘利用磁来存储信息,断电后信息不丢失
  2. 虚拟内存:把磁盘中的一部分作为假想内存来使用。借助虚拟内存,内存容量不足的计算也可以运行很大的程序
  3. 存储程序方式(程序内置方式):程序保存在存储设备中,通过有序地被读到内存以实现运行。程序要加载到内存后才开始运行:程序加载
  4. 磁盘缓存(把磁盘中读出的数据存储到内存空间中)加快了磁盘访问速度。缓存在两种访问介质速度不一样的地方经常存在,如 低速的web 请求缓存在相对高速的磁盘中。可以理解为磁盘缓存是假想的磁盘,正如虚拟内存是假想的内存一样。虚拟内存 把运行的程序分成一定大小的页(Page,比如4K大小),并以页为单位在内存和磁盘间进行置换,从磁盘装载到内存叫 Page In,从内存写回到磁盘叫 Page Out
  5. 虚拟内存能解决内存不够的问题,但伴随着频繁的 Page In/Out 会让程序运行变得缓慢。把应用程序变小可以避免频繁的换页操作:通过 DLL(Dynamic Link Library) 文件实现函数共有、通过调用 _stdcall(标准调用) 来减少程序文件的大小、
  6. 在 C 语言中,在调用函数后由调用方负责执行栈清理处理指令,将不需要的函数参数从栈上清理出去,而这是通过设置栈的写入位置前进多少位来清理的,相当于重置写入指针的位置。另外,C 语言函数的返回值是通过寄存器而非栈来返回的。栈清理处理,比起在程序调用方进行,在反复被调用的函数一方进行时,程序整体要小一些,这就是 _stdcall。在函数前加上 _stdcall 就可以把栈清理处理变为在被调用函数一方进行。int myFunc(int a, int b) => int _stdcall MyFunc(int a, int b),具体是可以节省在汇编语言层面视角的清理栈指令的内存占用。
  7. 磁盘、磁道、扇区。扇区是磁盘进行物理读写的最小单位,在 windows 中一般 1 个扇区是 512 字节,不过在软件方面对磁盘进行读写的单位是扇区整数倍,称为蔟。不管是硬盘还是软盘,不同的文件是不能存储在同一个蔟中的(否则会导致只有一方的文件不能被删除),因此不管是多么小的文件都会占用一簇的空间,而且文件占用内存的大小始终都是蔟的整数倍。
  8. 一个优秀的程序,不仅要运行速度快,还要内存占用小。(Rust!)

6.亲自尝试压缩数据

  1. 文件存储的基本单位是 1 个字节,文件是字节数据的集合体,文件是将数据存储在磁盘等存储媒介中的一种形式,在任何情况下,文件中的字节数据都是连续存储的。
  2. RLE(Run Length Encoding) 行程长度压缩算法,是把数据按线性序列分成 2 种,一种是连续的重复数据块,另一种是连续的不重复数据块,然后表示成 数据块*出现 次数的压缩方式。RLE 是一种可逆的压缩,AAABBC 压缩后是 A3B2C1 或者 A3B2C。RLE 算法经常被用于传值 FAX 等,因为传真机是把文字和图形都作为黑白图像来发送的。
  3. 非8位数据的读写
  4. 摩尔斯编码:把一般文本中出现频率高的字符用短编码来表示。这里的出现频率是根据印刷行业的印刷活字数目来确定的
  5. 哈夫曼编码:比摩尔斯编码更进一步,根据要压缩文本中字符出现的频率来决定短编码。在哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据huffman
  6. 哈夫曼树的编码顺序:huffman-encoding-tree
  7. windows的标准图像数据形式为 BMP(Bitmap),是完全未压缩的。

7.程序是在何种环境中运行的

  1. 应用的运行环境,指的是操作系统和计算机本身(硬件)的种类。不同的硬件种类需要不同的操作系统,应用是为了在特定操作系统上运行而作成的。
  2. 运行环境 = 操作系统 + 硬件
  3. 程序 = 指令 + 数据 = 算法 + 数据结构
  4. CPU 只能解释其固有的机器语言,不同的 CPU 能解释的机器语言种类是不同的
  5. 从操作系统方面来看,Java 虚拟机是一个应用,而从 Java 应用方面来看,Java 虚拟机就是运行环境。
  6. 程序的运行环境中,存在名为 BIOS(Basic Input/Output System) 的系统。BIOS 存储在 ROM(Read Only Memory) 中,是预先内置在计算机主机内部的程序。BIOS 除了键盘、磁盘、显卡等基本控制程序外,还有启动“引导程序”的功能。引导程序是存储在启动驱动器起始区域的小程序。操作系统的启动驱动器一般是硬盘,不过有时也可以是 CD-ROM 或软盘。(磁盘分为硬盘和软盘,硬盘一般是装在主机内的不可移动的,容量较大;而软盘是可以移动的,比如 U 盘)开机后,BIOS 会确认硬件是否正常运行,没有问题的话就会启动引导程序(启动驱动器(一般是硬盘)起始区域的小程序)。引导程序的功能是把在硬盘等记录的 OS 加载到内存中运行。虽然启动应用是 OS 的功能,但 OS 并不能自己启动自己,而是通过引导程序来启动。
  7. Bootstrap的原意是指靴子上不的“拔靴带”,BIOS 这样小的程序(拔靴带)可以带动(启动)操作系统(靴子)这样的大程序,所以由此得名。操作系统运行以后,程序员就不用关注 BIOS 及引导程序了。

8.从源文件到可执行文件

  1. source code vs native code(本地代码,即机器语言,native 有“母语的”意思,对 CPU 来说其母语就是机器语言)
  2. CPU 只能解释native code,即使是不同编程语言的 source code,最后都被转换成同一种语言了(机器语言)。而机器语言对应的是 0/1 的组合构成的电平变化指令,这是数字硬件电路能处理的,通过读取和译码过程产生相应的状态跳转和基本计算
  3. 编译器负责将 source code 转换成 native code,词法解析、语义解析等
  4. 编译器也是程序的一种,也需要运行环境。
  5. 仅靠编译是无法得到可执行文件的,编译器生成 native code 后,会生成目标文件“*.obj”,还需要进行“链接”处理(使程序使用到的系统类型填充进来,即把多个目标文件结合并生成 1 个 EXE 文件,其实包括了给 native code 添加“启动”的部分,可以理解为引导头),这就是链接器。windows-compile-link
  6. 在 native code 的程序中,给变量及函数分配的地址是虚拟地址。在程序运行时,虚拟地址会转换成实际的内存地址,其中链接器会在 native code 的头部,追加转换内存所需的必要信息,这个信息称为 再配置信息,这个再配置信息,就称为了变量和函数的相对地址,表示的是相对于基点地址的偏移量。在进行链接后的 native code 里面,虽然变量和函数是在不同位置分散记述的,但是在链接后的可执行文件中,变量和函数就会变成一个连续排列的组。这样各个变量的内存地址就可以用相对于变量组起始位置这一基点的偏移量来表示。而各组基点的内存地址则是在程序运行时被分配的。link-exe
  7. 程序加载时会生成栈和堆。可执行文件的内容分为再配置信息、变量组合函数组,除此之外当程序加载到内存后,还会额外生成 2 个组:栈和堆。栈是用来存储函数内部的局部变量,以及函数调用时所用的参数的内存区域;而堆是用来存储程序运行时的任意数据及对象的内存区域。
  8. 内存中的程序,就是由用于变量的内存空间、用于函数的内存空间、用于栈的内存空间、用于对的内存空间这 4 部分构成的。program-memory
  9. 栈和堆:两者的内存空间都是程序在运行得到分配的,不同在于用法,栈中对数据的存储和舍弃的代码是由编译器自动生成的,因此程序员不需要参与,需要时会自动申请,不需要时会自动释放;而堆中的内存空间则要程序员来明确进行申请分配或释放。
  10. 栈和堆的大小,可以由程序任意指定。在高级编程语言中,编译器会自动生成指定栈和堆大小的代码,并将其附加到程序中。
  11. 垃圾回收:对堆内存空间不再需要的数据和对象进行清理

9.操作系统和应用的关系

  1. 操作系统的硬件控制功能,通常是通过一些小的函数集合体的形式来提供的,这些函数及调用函数的行为统称为 system call,即系统调用,也就是应用对操作系统(system)的功能进行调用(call)的意思。
  2. GUI 全称是 Graphical User Interface(图形用户界面)
  3. What You See Is What You Get(所见即所得)
  4. 利用计算机运行程序大部分是为了提供处理效率,程序员的工作就是编写各种各样的应用来提高业务效率
  5. 操作系统(Operating System)也称为基础软件,操作系统是计算机运行时不可或缺的控制程序,以及在控制程序下运转的位其他软件运行提供操作环境的软件的统称。在操作系统上运行的程序也叫“应用程序”
  6. 操作系统的原型是监控程序,监控程序的主要功能是程序的加载和运行。通过事先启动监控程序,程序员就可以根据需要将各种程序加载到内存中运行(本质也是加载 native code,不过这比在不存在操作系统的时候需要手动输入 native code方便)os-prototype
  7. 随着时代的发展,人们在利用监控程序编加载和运行程序的过程中,发现很多程序都有共通的部分,例如通过键盘输入文字数据、往显示器输出文字数据等,这些处理在任何程序下都是一样的,而如果每个程序都记述了相同的处理的话,那真的是太浪费时间了。因此,基本的输入输出部分的程序都被追加到了监控程序中,初期的操作系统就这样诞生了。再之后,随着时代的发现,开始有更多的功能被追加到监控程序中,比如为了方便程序员的硬件控制程序、编程语言处理器(汇编、编译、解析)以及各种实用程序等,结果就形成了和现在相差不多的操作系统。因此,操作系统本身并不是单独的程序,而是多个程序的集合体。os-construct
  8. 在操作系统这个运行环境下,应用并不是直接控制硬件,而是通过操作系统来简介控制硬件的。操作系统收到应用程序发出的指令后,首先会对该指令进行解释,然后会对时钟 IC(实时时钟,Real-time clock,计算机中都会安装的用来保存日期和时间的)和显示器的 I/O 进行控制。os-app-io
  9. 操作系统和高级编程语言使硬件抽象化。文件是操作系统对磁盘媒介空间的抽象化。
  10. Windows 是通过名为 API 的函数集来提供系统调用的,API 通过多个 DLL 文件来提供,各 API 的实体都是用 C 语言编写的函数。
  11. 多任务指的是同时执行多个程序的功能,Windows 是通过时钟分割技术来实现多任务功能的
  12. 网络功能和数据库功能虽然不是操作系统的不可或缺的功能,但因为它们和操作系统很接近,所以被称为中间件而不是应用。操作系统和中间件组合在一起,也称为系统软件。system-software
  13. 即插即用(Plug-and-Play)指的是新的设备连接(Plug)后立刻就可以使用(Play)的机制。新的设备连接到计算机后,系统就会自动安装和设定用来控制设备的设备驱动程序。设备驱动是操作系统的一部分,提供了同硬件进行基本的输入输出的功能。

10.通过汇编语言了解程序的实际构成

  1. 在 native code(机器语言)的指令中,表示其功能的应用缩写称为 助记符,使用助记符的编程语言称为汇编语言,汇编语言中和 native code 是一一对应的,通过查看汇编语言编写的源代码,就可以了解程序的本质了。

  2. 汇编语言的源文件扩展名是 .asm (assembler,汇编器的意思,即使是汇编语言也需要转换成 native code 才能运行,这个过程称为汇编,将 native code 转换为汇编语言泽称为反汇编) assembly-native-code

  3. 汇编语言程序中的段定义指的是构成程序的命令和数据的集合组。在高级语言编程中,即使指令和数据在编写时是分散的,但是在编译后也会在段定义中集合汇总起来

  4. 汇编语言的源代码,是由转换成本地代码的指令和针对汇编器的伪指令构成的,由伪指令 segment 和 ends 围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为段定义,段定义的英文表达 segment 具有“区域”的意思。一个程序由多个段定义构成。

  5. 在汇编语言中,把相当于 C 语言函数的形式称为过程

  6. 在汇编语言中,1 行表示对 CPU 的一个智力,汇编语言指令的的语法是“操作码 + 操作数”(也存在只有操作码没有操作数的指令)。

  7. 操作码表示的是指令动作,操作数表示的是指令对象,动宾结构。能够使用何种形式的操作码,是由 CPU 的种类决定的。

    操作码 操作数 功能
    mov A, B 把 B 的值赋给 A
    and A, B 把 A 同 B 的值相加,并把结果赋给 A
    push A 把 A 的值存储在栈中
    pop A 从栈中读取出值,并将其赋给 A
    call A 调用函数 A
    ret 无 将处理返回到函数的调用源

cpu-and-memory

  1. 寄存器是 CPU 中的存储区域,除了具有存储指令和数据的功能外,还有运算功能

  2. x86 系列的 CPU 的主要寄存器:

    寄存器名    名称        主要功能
    eax         累加寄存器  运算
    ebx         基址寄存器  存储内存地址
    ecx         计数寄存器  计算循环次数
    edx         数据寄存器  存储数据
    esi         源基址寄存器    存储数据发送源的内存地址
    edi         目标基址寄存器  存储数据发送目标的内存地址
    edp         扩展基址指针寄存器  存储数据春初领域基点的内存地址
    esp         扩展栈指针寄存器    存储栈中最高位数据的内存地址
  3. 最常用的 mov

    mov ebp, esp    // esp 寄存器中的值被直接存储在了 ebp 寄存器中
    mov eax, dword ptr [esp+8]  // 将 esp+8 的内存地址所存储的值,复制到 eax 中,  
                                // dword ptr (double word poiter)表示的是从指定内存地址读出 4 字节的数据
  4. 对栈进行 push 和 pop

    程序运行时,会在内存上申请分配一段称为栈(stack,“干草堆积如山”)的数据空间,数据从内存的下层(高位的内存地址)逐渐往上层(低位内存地址)累计,如果都用完了则会产生栈溢出错误(stack overflow)

stack-push-pop

  1. 函数调用机制

    函数调用是栈发挥大作用的场合,函数的参数是通过栈来传递,返回值是通过寄存器来返回的。  
    C 语言中函数的返回值必须通过 eax 寄存器返回,这也是规定。  
    函数最后的ret 指令运行后,函数返回目的地的内存地址会自动出栈,据此,程序流程就会跳转返回到调用处的下一行  
    局部变量只进行定义是不够的,只有在局部变量赋值时,才会被分配到寄存器/栈的内存区域  

local-var-stack-request-alloc

  1. 循环处理的实现方法

    汇编语言中比较指令的结果,会存储在 CPU 的标志寄存器中,然后跳转指令根据标志寄存器的值来判断是否需要跳转  
    汇编语言是对 CPU 的实际运行进行直接描述的低级编程语言  
    C 语言是用于人类的感觉相近的表现来描述的高级语言

assembly-for

  1. 条件分支的实现方法

    同循环处理一样,条件分支使用的也是 cmp 指令和跳转指令
    C 语言中的 goto 相当于汇编语言的 jmp 指令
    汇编语言有 3 种跳转指令:  
    jle (jum on less or equal)  
    jpe (jump on greater or equal)  
    jmp (无论结果怎样都无条件跳转)

assembly-if-branch

C 语言中的一行代码(counter *= 2;),转换到汇编中可能有多行指令对应:  

    mov eax,dword ptr[_counter] ; 将 counter 的值读入 eax 寄存器  
    add eax,eax ; 将 eax 寄存器的值扩大至原来的 2 倍  
    mov dword ptr[_counter],eax ; 将 eax 寄存器的数值存入 counter 中

而在多线程处理中,汇编语言记述的指令代码没运行 1 行都有可能切换到其他线程中  
为了避免该 bug,我们可以采用以函数或 C 语言源代码的行为单位来禁止线程切换的锁定方法

11.硬件控制方法

  1. 在汇编语言中,是分别用 IN 指令和 OUT 指令来同外围设备进行输入输出操作的,用来实现计算机主机和外围设备输入输出交互的 IC称为 I/O 控制器或简称为 I/O

  2. 用来标识外围设备的编号称为 I/O 地址或者 I/O 端口号

  3. 用来识别具有 DMA 功能的外围设备的编号称为 DMA 通道,比如磁盘就有 DMA 功能

  4. 控制 CPU 只需要把 native code 加载到主存然后运行就可以了,控制 CPU 和主存以外的设备就需要进行 I/O 操作,比如磁盘等
    system-io

  5. Windows 中的 显示字符串的 API 函数 TextOut,应该向 CPU 传递了某些指令,从而通过软件控制了硬件,其中具有代表性的输入输出指令就是 IN/OUT。

  6. IN 指令通过指定端口号的端口输入数据,并将其存储在 CPU 内部的寄存器中。
    OUT 指令则是把 CPU 寄存器中存储的数据,输出到指定的端口号的端口
    io

  7. 在计算机主机中,附带了用来连接显示器即键盘等外围设备的连接器。这些连接器的内部,都连接有用来交换计算机主机同外围设备之间电流特性的 IC,这些 IC 统称为 I/O 控制器。I/O 控制器可以解决主机同外围设备的电压/数模信号等的电流特性差异。

  8. 主机中分别有专门的 I/O 控制器 来对应显示器、键盘等外围设备,所以会有多个 I/O 控制器,一个 I/O 控制器也可以控制多个外围设备。

  9. I/O 控制器中有用于临时保存输入输出数据的内存,这个内存就是端口(port,字面意思是“港口”,由于端口就像是在主机和外围设备之间进行货物(数据)装卸的港口,所以因此得名)。各端口之间通过端口号进行区分,端口号也称为 I/O 地址。IN 指令和 OUT 指令在端口号指定的端口和 CPU 之间进行数据的输入输出,这和通过内存地址来进行主存的读写是一样的道理。
    cpu-port-io

    I/O 装置(I/O 控制器),有的直接附带在主机主板上,有的则是各自独立的扩展板卡。  
    键盘、鼠标、打印机等常用的 I/O  一般都是内嵌在主板上,而显示高速图形的显示器  
    及网卡等特殊的 I/O,通常是独立扩展板卡,即独立显卡/网卡。
  10. I/O 控制器内部的内存也称为寄存器,不过和 CPU 内部的寄存器有所不同,其主要用来临时存储数据。

  11. 主机内的蜂鸣器是外围设备的一种,虽然它在主机内部

  12. 中断请求(Interrupt Request),是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制,这称为中断处理。

  13. 实施中断请求的是连接外围设备的 I/O 控制器,负责实施中断处理的是 CPU。为了进行区分,外围设备的中断请求使用不同于 I/O 端口的其他编号,称为中断编号。假如同时有多个外围设备进行中断请求的话,CPU 也会为难,为此可以在 I/O 控制器和 CPU 中间加入名为中断控制器的 IC 来进行缓存。
    interrupt-request-handler

  14. CPU接收到来自中断控制器的中断请求后,会把当前正在运行的主程序中断,并切换到中断处理程序。中断处理程序的第一步处理,就是把 CPU 所有寄存器的数值保存在内存的栈中。在中断处理程序完成外围设备的输入输出后,把栈中保存的数据还原到 CPU 寄存器中,然后再继续进行对主程序的处理。
    interrupt-request-order

  15. 实际上,大部分外围设备都会频繁地发出中断请求,就是未了实时处理从外围设备输入的数据,虽然不利用中断也可以从外围设备输入数据,但那样就需要主程序需要不断的轮询检测是否有外设数据输入,这对计算机来说是不适合的,因为这会导致不能实时处理,而通过使用中断,就可以实现实时处理了。

  16. 打印机等输出的外设中,外设接收数据的状态,有时是需要用中断来通知的,这种情况下就不需要对打印机的状态进行多次调查,所以使用中断处理变得很方便。

  17. Direct Memory Access(DMA,直接内存存取), 指的是不经过 CPU 中断处理,外围设备直接同主内存进行数据传输。磁盘等都利用到了 DMA 机制,CPU 在其中作为中介的时间被节省了。

  18. 文字及图片的显示机制

    显示器显示的信息一直存储在某内存中,该内存称为 VRAM(Video RAM)。  
    在改程序中,只要往 VRAM 中写入数据,该数据就会在显示器中显示出来。  
    实现该功能的程序,是有操作系统或 BIOS 提供,并借助中断来进行处理的。

VRAM

  1. 用软件来控制硬件听起来好像很难,但实际上只是利用输入输出指令桐外围设备进行输入输出的处理而已。中断处理是根据需要来使用的选项功能,DMA 则直接交给对应的外围设备即可。

    虽然计算机领域的新技术在不断涌现,但计算机能处理的事情,始终只是对输入的数据进行计算,  
    并把结果输出。这一点是不会发生改变的。计算机是关于计算(Computing)的机器。

12.让计算机思考

  1. 控制就是指 CPU 和各种设备之间配合进行数据的输入输出处理
  2. 程序的使用目的:
    • 一类是用来作为工具,例如文字处理器
    • 一类是用来代替执行人类的思考过程,如电饭煲会根据米和水的分类来自动调整火的大小及加热时间
  3. 通常所说的随机数指的是统一随机数,是在一定范围内各数出现频率相同的随机数形式,C 语言的 rand() 函数的返回值就是统一随机数
  4. 伪随机数与随机数相比具有周期性,比如线性同余法求随机数的公式为:Ri+1 = (a * Ri + b) % c
    rand

附录:让我们开始 C 语言之旅

  1. C 语言具备能够和汇编语言相媲美的低层处理功能:内存操作及位操作

  2. C 语言的程序是由变量(存放数值的地方)和函数(含 f 函数来处理 x 这个变量)构成的

  3. 数据类型:

    char        1 byte  
    short       2 bytes  
    int/long    4 bytes  
    float       4 bytes  
    double      8 bytes