程序是怎么跑起来的–学习摘要

[TOC]

程序是怎样跑起来的。

第一章 对程序员来说CPU是什么

弄清楚了负责保存指令和数据的寄存器的机制,也就理解了程序的运行机制。

CPU的内部结构解析

CPU所负责的就是解释和运行最终转换为机器语言的程序内容。

CPU和内存是由许多晶体管组成的电子部件,通常称为IC(Integrated Circuit, 集成电路)。

从功能方面看,CPU的内部由寄存器控制器运算器时钟4个部分构成,各部分之间由电流信号互相连通。

寄存器可用来暂存指令、数据等处理请求,可以将其看做是内存中的一种。一个CPU会有20~200个寄存器。

控制器负责把内存上的指令、数据等读入寄存器,并根据指令的执行结果来控制整个计算机。所谓的控制是指数据运算以外的处理(主要是数据输出输出的时机控制)。

运算器负责运算从内存读入寄存器的数据。如:加法、乘法、位移、位操作等。

时钟负责发出CPU开始计时的时钟信号。不过有些计算机的时钟位于CPU的外部。

CPU是寄存器的集合体

CPU的四个构成部分中,程序员只需要了解寄存器即可,因为程序是把寄存器作为对象来描述的。

根据功能的不同,我们可以将寄存器大致划分为八类。可以看出,寄存器中存储的内容既可以是指令也可以是数据,数据又分为”用于运算的数值“和”表示内存地址的数值“两种。

种类 功能
累加寄存器(accumulator register) 存储执行运算的数据和运算后的数据
标志寄存器(flag register) 存储运算处理后的CPU的状态
程序计数器(program counter) 存储下一条指令所在内存的地址
基址寄存器(base register) 存储数据内存的起始地址
变址寄存器(index register) 存储基址寄存器的相对地址
通用寄存器(general purpose register) 存储任意数据
指令寄存器(instruction register) 存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。
栈寄存器(stack register) 存储栈区域的起始地址。

其中:程序计数器累加寄存器标志寄存器指令寄存器栈寄存器都只有一个,其他的一般有多个。

决定程序流程的程序计数器

条件分支和循环机制

无论当前累加寄存器的运算结果是负数、零还是正数,标志寄存器都将会将其保存(也负责存放溢出和奇偶校验的结果)。

CPU在进行运算时,标志寄存器的数据会根据运算结果自动设定。

条件分支在跳转指令前会进行比较运算。至于是否执行跳转指令,则由CPU在参考标志寄存器的数值后进行判断。

IMG_4966

函数的调用机制

函数调用使用的是call指令,而不是跳转指令。

在将函数的入口地址设定到程序计数器前,call指令会把调用函数后的要执行的指令地址存储到名为栈的主存内。

函数处理完毕后,再通过函数的出口来执行return命令。

return命令的功能是把保存在栈中的地址设定到程序计数器中。

WechatIMG15

WechatIMG16

通过地址和索引实现数组

实际地址 = 基址寄存器的值 + 变址寄存器的值

CPU的处理其实很简单

机器语言指令的主要类型和功能

类型 功能
数据转送指令 寄存器和内存、内存和内存、寄存器和外围设备之间的数据读写操作
运算指令 用累计寄存器执行算术运算、逻辑运算、比较运算和移位运算
跳转指令 实现条件分支、循环、强制跳转等
call/return指令 函数的调用/返回调用前的地址

第2章 数据是用二进制数表示的

用二进制数表示计算机信息的原因

IC的所有引脚,只有直流电压0V或5V两个状态,只能表示两个状态。

位是最小单位,字节是基本单位。

什么是二进制数

移位运算和乘除运算的关系

便于计算机处理的”补数“

可以参考原码、反码、补码

  1. 将2进制数的值取反加1的结果,和原来的值相加,结果为0
  2. 补码的补码=原码

逻辑右移和算数右移的区别

逻辑右移适合非数值计算,直接补0

算数右移补符号位

符号扩展也是直接补符号位。

掌握逻辑运算的窍门

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

可以参考浮点数

指数部分用的EXCESS系统表现。

EXCESS系统表现

通过将指数部分表示范围的中间值设为0,使得负数不需要用符号来表示。

处理小数出错的方案:

  1. 无视。在大部分场景下,近似值就够了
  2. 把小数处理为整数。譬如价格。价格只有2位小数,x100就可以了。
  3. 使用BCD表示法。会计系统常用
  4. 特定领域。如mysql用decimal表示定点小数,定点小数没有误差。

第4章 熟练使用有棱有角的内存

WechatIMG17

内存IC 中有电源、地址信号、数据信号、控制信号等用于输入输出的大量引脚,通过为其指定地址,来进行数据的读写。

RD是读,WR是写,内部由大量可以存储数据的地方,通过地址指定这些场所,之后即可进行数据的读写。

第5章 内存与磁盘的亲密关系

节约内存的编程方法:

  1. 通过DLL方式实现函数共享。
  2. 通过调用_stdcall来减小程序文件的大小(C语言的高级技巧)。默认情况下,清理栈代码自动嵌入在调用方(几次调用就有几份)。加上_stdcall声明后,清理栈代码会被嵌入到被调用方(从而只有一份)。

第5章 磁盘与内存的亲密关系

磁盘物理结构

扇区是对磁盘进行物理读写的最小单位。不过程序中的最小单位是簇,1簇是整数倍的扇区。windows扇区为512字节。

不管是硬盘还是软盘,不同的文件是不能存储在同一簇中的,否则就会导致另一个文件不能被删除。因此不管是多么小的文件,都会占用一簇的空间。

已簇为单位进行读写时,1簇中没有被填满的区域会保持不被使用的状态,

第6章 亲自尝试压缩数据

文件以字节为单位保存

在任何情况下,文件中的字节数据都是连续存储的。

需要注意空洞现象

在UNIX文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为 0。
如果 offset 比文件的当前长度更大,下一个写操作就会把文件“撑大(extend)”。这就是所谓的在文件里创造“空洞(hole)”。没有被实际写入文件的所有字节由重复的 0 表示。空洞是否占用硬盘空间是由文件系统(file system)决定的。

RLE算法的机制

像这样,把文件内容用”数据 x 重复次数”的形式来表示的压缩方法称为RLE(Run Length Encoding, 行程长度编码)算法。RLE算法常用于压缩传真的图像等。不适合文本。

哈夫曼算法

参考哈夫曼算法

可逆压缩与非可逆压缩

能还原到压缩状态前的压缩为可逆压缩,无法还原到压缩状态前的为非可逆压缩

一般图像用非可逆压缩即可,文本文件和Exe需要用可逆压缩。

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

运行环境 = 操作系统 + 硬件

首先,操作系统屏蔽了 硬件的差异。

其次,不同的操作系统,对外提供的api不同。

解决方案:

  1. 虚拟机
  2. java, java通过java虚拟机屏蔽了操作系统的差异。其实不同操作系统上的java虚拟机本就是针对操作系统的。

BIOS和引导

BIOS存储在ROM中,是预先内置在计算机主机内部的程序。

BIOS除了键盘、磁盘、显卡等基本控制程序外,还有启动”引导程序”的功能。

引导程序是存储在启动驱动器起始区域的小程序。启动驱动器一般是硬盘,也有CD-ROM和软盘,USB。

开机后,BIOS会确认硬件是否正常运行,没有问题的话就会启动引导程序。

引导程序的功能是把在硬盘的记录的OS加载到内存中去。

第8章 从源文件到可执行文件

计算机只能运行本地代码

也就是机器代码。不管什么编程语言,最后运行时都是运行的机器代码。

机器代码是和硬件强相关的,譬如CPU。因为CPU都有特定的指令集。

本地代码的内容

0和1的二进制代码

编译器负责转换源代码

读入的源代码经过语法解析、句法解析、语义解析等,才能生成本地代码。

编译器不仅和编程语言相关,和CPU的类型也是相关。

交叉编译器可以生成多种CPU类型下的本地代码。

仅靠编译是无法得到可执行文件的

编译后生成的目标文件还需要通过链接才能生成可执行文件。

因为源代码中除了用户写的代码,还会引用到标准库代码,第三方库代码。需要把对应的目标文件也链接进来才能运行。

把多个目标文件结合,生成1个EXE文件的处理就是链接, 运行连接的程序称为链接器

启动及库文件

库文件指的是把多个目标文件集成保存到一个文件中的形式。链接器指定库文件后,就会从中把需要的目标文件抽取出来,并同其他目标文件结合生成EXE文件。 (优点:指定一个库文件就好,不用指定多个目标文件来链接)

通过以目标文件的形式或集合多个目标文件的库文件形式来提供函数,就可以不用公开标准函数的源代码内容。

DLL文件和导入库

DLL文件是程序运行时动态结合的文件。

像import32.lib文件,没有实际的目标文件,而是存储函数和DLL的对应关系,以及DLL文件信息的 库文件称为导入库

与此相反,存储着目标文件的实体,并直接和EXE文件结合的库文件形式称为静态链接库

WechatIMG19

可执行文件运行时的必要条件

链接器会在EXE文件的开头,追加转换内存地址所需的必要信息。这个信息称为再配置信息(Relocation)

EXE文件的再配置信息,就成为了变量和函数的相对地址。

在EXE文件中,变量和函数会变成连续排列的组。

image-20210302183158470

程序加载时会生成栈和堆

EXE文件的内容分为再配置信息、变量组和函数组。

在EXE加载到内存中运行后,会额外生成两个组:栈和堆。

WechatIMG21

第9章 操作系统和应用的关系

操作系统功能的历史

最早期的是监控程序: 仅具有加载和运行功能。

要意识到操作系统的存在

WechatIMG22

系统调用和高级编程语言的移植性

高级编程语言的机制就是,使用独自的函数名,然后再在编译时将其转换为相应操作系统的系统调用(有可能是多个的组合)。

操作系统和高级编程语言使硬件抽象化

文件是操作系统对磁盘媒介空间的抽象化。

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

汇编语言和本地代码是一一对应的

汇编语言->本地代码 汇编

本地代码->汇编语言 反汇编

本地代码-> 高级语言 逆向工程

通过编译器输出汇编语言的源代码

不会转换成本地代码的伪指令

汇编语言的源代码,是由转换成本地代码的指令和针对汇编器的伪指令构成的。

伪指令:

伪指令负责把程序的构造及汇编的方法指令给汇编器。

_TEXT是指令的段定义。

_DATA是被初始化的数据的段定义。

_BSS是尚未初始化的数据的段定义。

汇编语言的语法是“操作码+操作数”

操作码示例

操作码 操作数 功能
mov A, B 把B的值赋给A
add A,B A = A+B
push A 入栈
pop A 出栈,值赋给A
call A 调动函数A
ret 将处理返回到 函数的调用源
cmp A, B 比较A,B, 结果自动存入标志寄存器
inc A A++
jge 标签名 和cmp组合使用。A>=B跳转
jl 标签名 和cmp组合使用。A<B跳转
jle 标签名 和cmp组合使用。A<=B跳转
jmp 标签名 无条件跳转
xor A,B A = A xor B

x86系列CPU的主要寄存器

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

最常用的mov指令

move eax dword ptr [ebp+8] # 把ebp指向的内存地址+8, 然后读取4个字节内容,赋给eax寄存器 dword ptr = doube word pointer

如果指定了没有用方括号围起来的内容,就表示对该值进行处理;

如果指定了用方括号围起来的内容,方括号中的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。

对栈进行push和pop操作

push和pop指令运行后,esp寄存器的值会自动进行更新(push指令是-4, pop指令是+4)。

WechatIMG23

函数调用机制

call指令运行后,call指令的下一行的内存地址 会自动push入栈。然后该值在被调用函数调用ret时pop出栈,从而把程序流程回到 call指令的下一行。

add esp, 8 #通过简单地修改栈指针,从而达到清理栈的目的

函数内部的处理

1. push ebp # 1 和 4 结合是为了暂存ebp的值,执行完函数之后恢复
2. move ebp, esp # mov指令中方括号内的参数,是不允许指定esp寄存器的。。估计是因为esp的值会自动修改,call,push,pop,ret都会自动修改
3. mov eax, dword ptr [ebp+8] #这里+8是因为存了函数的返回地址和ebp
3. add eax, dword ptr [ebp+12]
4. pop ebp
5. ret

函数的参数是通过栈来传递,返回值是通过寄存器来返回的。

WechatIMG24

始终确保全局变量用的内存空间

_DATA segment dword public use32 'DATA'
_a1 label dword # 标签名,也就是变量名。内存地址是相对于段来的,这里偏移量是0
    dd 1 # define double word 4字节的空间,内容为1
_DATA ends

_BSS segment dword public use32 'DATA'
_b1 label dword 
    db 4 dup(?) # define byte 4, 4个1字节的空间,也是4字节,内容为?,表示未初始化
_BSS ends

临时变量确保局部变量用的内存空间

局部变量存到寄存器和栈中。

优先使用寄存器,寄存器不够再使用栈。具体使用哪个寄存器,由编译器决定。

循环处理的实现办法

就是利用ebx寄存器,然后inc指令累计ebx, 再和循环次数做cmp比较,之后就是跳转指令了。

ebx的初始化有技巧。直接xor ebx ebx就初始化为0了。

条件分支的实现方法

就是jmp家族的用法。

了解程序运行方式的必要性

第11章

应用与硬件无关

应用调用操作系统的api, 操作系统操作硬件。

支撑硬件输入输出的IN和OUT指令

IN指令通过指定端口号的端口输入数据,并将其存储在CPU内部的寄存器中。

OUT指令则是把CPU寄存器中存储的数据,输出到指定端口号的端口。

I/O控制器中有用于临时保存输入输出数据的内存。这个内存就是端口。I/O控制器内部的内存,也叫寄存器。

各端口之间通过端口号进行区分。端口号也称为I/O地址

WechatIMG25

编写测试用的输入输出程序

在C语言中,嵌入_asm{ } 可以混这编写汇编代码。示例如下。

_asm{
    IN eax, 61H # 把61h 端口的数据写入eax寄存器。 61h是小喇叭的端口号
    OR eax, 03H # 把eax的低2位置为1, 功能是使小喇叭鸣叫
    OUT 61H, eax # 把eax的值输出到 61h端口号
}

外围设备的中断请求

IRQ是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制称为中断处理。(可类比为玩游戏是接到电话,电话就中断了游戏的运行,打完电话接着玩游戏)

实施中断请求的是连接外围设备的I/O控制器,负责实施中断处理的是CPU。

为了进行区分,外围设备的中断请求会使用不同于I/O端口的其他编号,该编号称为中断编号

为了处理多个设备的中断请求,在I/O控制器和CPU中间加入了名为中断控制器的IC来进行缓存。这样每个发起中断的I/O控制器都会得到机会处理。

处理流程:

CPU接受来自中断控制器的中断请求后,会把当前正在运行的主程序中断,并切换到中断处理程序。

中断处理程序第一步就是把CPU所有寄存器的数值保存到内存的栈中。

在中断处理程序中完成外围设备的输入输出后,把栈中保存的数值还原到CPU寄存器中,然后在继续进行对主程序的处理。

用中断来实现实时处理

如果没有中断的话,就需要轮询外围设备。效率低下。

DMA可以实现短时间内传送大量数据

DMA(directory memory access)是指在不通过CPU的情况下,外围设备直接和主内存进行数据传递。磁盘等都用到了这个机制。

I/O端口号IRQDMA通道可以说是识别外围设备的3点组合。

假如多个外围设备都设定成同样的端口号、IRQ及DMA通道的话,计算机就无法正常工作了。这种情况下,就会出现“设备冲突”的提示。

文字及图片的显示机制

显示器中显示的信息一致存储在某内存中,该内存称为VRAM(video RAM)。

以前VRAM是主存的一部分,现在是显卡的一部分。

第12章 让计算机思考

伪随机数

借助公式产生的随机数具有一定的规律性,因此并不是真正的随机数,通常称为伪随机数

有几种方式:

  1. 线性同余法
  2. 乘同余法
  3. M系法
  4. Knuth减算法

线性同余法:

Ri+1 = (a x Ri + b) mod c