《程序是怎样跑起来的》矢泽久雄[日] 读书笔记,详细建议阅读原版图书学习
1 程序由指令与数据组成,是指示计算机每一步动作的一组指令,机器语言指的是CPU可以直接识别并使用的语言。
2 程序运行流程示例图
3 IC: 集成电路
4 CPU的内部由寄存器、控制器、运算器和时钟四个部分构成,各部分之间由电流信号相互连通。寄存器可用来暂存指令、数据等处理对象,可以将其看作是内存的一种。根据种类的不同,一个CPU内部会有20~100个寄存器。控制器负责把内存上的指令、数据等读入寄存器,并根据指令的执行结果来控制整个计算机。运算器负责运算从内存读入寄存器的数据。时钟负责发出CPU开始计时的时钟信号,其实所谓的控制就是指数据运算以外的处理(主要是数据输入输出的时机控制),需要着重了解的是寄存器。
5 程序是把寄存器作为对象来描述的。机器语言级别的程序是通过寄存器来处理的。也就是说,在程序员看来“CPU是寄存器的集合体”。
程序计数器决定着程序的流程。“跳转到0104地址”这个指令间接执行了“将程序计数器设定成0104地址”这个操作。
是否执行跳转指令,则由CPU在参考标志寄存器的数值后进行判断。
基址寄存器和变址寄存器。通过这两个寄存器,我们可以对主内存上特定的内存区域进行划分,从而实现类似于数组的操作。
6 汇编语言和机器语言基本上是一一对应的。通常我们将汇编语言编写的程序转化成机器语言的过程称为汇编;反之,机器语言程序转化成汇编语言程序的过程则称为反汇编。
eax和ebp是CPU内部的寄存器的名称。内存的存储场所通过地址编号来区分,而寄存器的种类则通过名字来区分。
7 CPU执行比较的机制很有意思,因此请大家务必牢记。例如,假设要比较累加寄存器中存储的XXX值和通用寄存器中存储的YYY值,执行比较的指令后,CPU的运算装置就会在内部(暗中)进行XXX-YYY的减法运算
8 函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(函数调用指令的下一个地址)。call指令会把调用函数后要执行的指令地址存储在名为栈[插图]的主存内。函数处理完毕后,再通过函数的出口来执行return命令。return命令的功能是把保存在栈中的地址设定到程序计数器中。在编译高级编程语言的程序后,函数调用的处理会转换成call指令,函数结束的处理则会转换成return指令。
9 CPU可以进行的处理非常少。虽然高级编程语言编写的程序看起来非常复杂,但CPU实际处理的事情就是这么简单。
1 IC的所有引脚,只有直流电压0V或5V[插图]两个状态。也就是说,IC的一个引脚,只能表示两个状态。
2 计算机所处理的信息的基本单位是8位二进制数。8位二进制数被称为一个字节。
3 二进制数所特有的运算,也是计算机所特有的运算,因此可以说是了解程序运行原理的关键。移位运算指的是将二进制数值的各数位进行左右移位(shift=移位)的运算。移位有左移(向高位方向)和右移(向低位方向)两种。>运算符。>运算符的左侧是被移位的值,右侧表示要移位的位数。
对程序员来说,掌握位运算和逻辑运算的机制是一项基本技能,所以一定要掌握。十进制数左移后会变成原来的10倍、100倍、1000倍……同样,二进制数左移后就会变成原来的2倍、4倍、8倍……反之,二进制数右移后则会变成原来的1/2、1/4、1/8
4 二进制数中表示负数值时,一般会把最高位作为符号来使用,因此我们把这个最高位称为符号位。计算机在做减法运算时,实际上内部是在做加法运算。
为此,在表示负数时就需要使用“二进制的补数”。补数就是用正数来表示负数,为了获得补数,我们需要将二进制数的各数位的数值全部取反[插图],然后再将结果加1。例如,用8位二进制数表示- 1时,只需求得1,也就是00000001的补数即可。
补数求解的变换方法就是“取反+ 1”,将二进制数的值取反后加1的结果,和原来的值相加,结果为0
5 当运算结果为负数时,计算结果的值也是以补数的形式来表示的。通过求解补数的补数,就可知该值的绝对值(因为和原来的值相加,结果为0)。
6 类似于霓虹灯往右滚动的效果。这就称为逻辑右移.
算数右移:将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值(0或1)。这就称为算术右移。如果数值是用补数表示的负数值,那么右移后在空出来的最高位补1
不管正数还是用补数表示的负数,都只需用符号位的值(0或者1)填充高位即可。这就是符号扩充的方法。
7 二进制数表示的信息作为四则运算的数值来处理就是算术。而像图形模式那样,将数值处理为单纯的0和1的罗列就是逻辑。
算术运算是指加减乘除四则运算。逻辑运算是指对二进制数各数字位的0和1分别进行处理的运算,包括逻辑非(NOT运算)、逻辑与(AND运算)、逻辑或(OR运算)和逻辑异或(XOR运算[插图])四种。逻辑异或指的是排斥相同数值的运算。“两个数值不同”,也就是说,当“其中一方是1,另一方是0”时运算结果是1,其他情况下结果都是0。
1 浮点数是指把小数用“符号 尾数×基数的指数次幂”这种形式来表示
2 计算机之所以会出现运算错误,是因为“有一些十进制数的小数无法转换成二进制数”。
3 不管是使用单精度浮点数还是双精度浮点数,都存在计算出错的可能性。接下来将介绍两种避免该问题的方法。首先是回避策略,即无视这些错误。另一个策略是把小数转换成整数来计算。除此之外,BCD(Binary Coded Decimal)也是一种使用二进制表示十进制的方法。简单来讲,BCD就是用4位来表示0~9的1位数字的处理方法,这里不再做详细说明。
4 二进制数的4位,正好相当于十六进制数的1位。
1 内存引脚
数据信号引脚有D0~D7共八个,表示一次可以输入输出8位(=1字节)的数据。此外,地址信号引脚有A0~A9共十个,表示可以指定0000000000~1111111111共1024个地址。而地址用来表示数据的存储场所,因此我们可以得出这个内存IC中可以存储1024个1字节的数据。因为1024=1K[插图],所以该内存IC的容量就是1KB。像WR和RD这样可以让IC运行的信号称为控制信号。其中,当WR和RD同时为0时,写入和读出的操作都无法进行。
2 编程语言中的数据类型表示存储的是何种类型的数据。从内存来看,数据类型就是占用的内存大小的意思。根据程序中所指定的变量的数据类型的不同,读写的物理内存大小也会随之发生变化
3 指针前的数据类型表示的是从指针存储的地址中一次能够读写的数据字节数。
4 数组是指多个同样数据类型的数据在内存中连续排列的形式。作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引(index)。
5 栈和队列的区别在于数据出入的顺序是不同的。在对内存数据进行读写时,栈用的是LIFO(Last Input First Out,后入先出)方式,而队列用的则是FIFO(FirstInput First Out,先入先出)方式。队列一般是以环状缓冲区(ring buffer)的方式来实现的。
使用链表来追加或删除数据则毫不费事。
使用二叉查找树的便利之处在于可以使数据的搜索等更有效率。
1 磁盘缓存是指,把从磁盘中读出的数据存储在内存中,当该数据再次被读取时,不是从磁盘而是直接从内存中高速读出。这种思想其他地方也用的到。
2 函数的加载方式有静态链接和动态链接两种。
3 总之,存储在磁盘中的程序需要读入到内存后才能运行。在考虑内存和磁盘的关系之前,大家一定要了解这个前提
4 由于使用虚拟内存时发生的Page In和Page Out往往伴随着低速的磁盘访问,因此在这个过程中应用的运行会变得迟钝起来。
5 DLL(Dynamic Link Library)文件,顾名思义,是在程序运行时可以动态加载Library(函数和数据的集合)的文件。栈清理处理,比起在函数调用方进行,在反复被调用的函数一方进行时,程序整体要小一些。这时所使用的就是_stdcall。
6 扇区方式中,把磁盘表面分成若干个同心圆的空间就是磁道,把磁道按照固定大小(能存储的数据长度相同)划分而成的空间就是扇区。扇区是对磁盘进行物理读写的最小单位。Windows中使用的磁盘,一般1个扇区是512字节。1簇=1扇区。所有的文件都会占用1簇的整数倍的磁盘空间。
1 在任何情况下,文件中的字节数据都是连续存储的
2 把文件内容用“数据×重复次数”的形式来表示的压缩方法称为RLE(Run Length Encoding,行程长度编码)算法
3 哈夫曼算法是哈夫曼(D. A. Huffman)于1952年提出来的压缩算法。哈夫曼算法的关键就在于“多次出现的数据用小于8位的字节数来表示,不常用的数据则可以用超过8位的字节数来表示”。哈夫曼算法中,通过借助哈夫曼树构造编码体系。用枝条连接数据时,我们是从出现频率较低的数据开始的,这就意味着出现频率越低的数据到达根部的枝条数就越多。而枝条数越多,编码的位数也就随之增多了。
4 由于显示器及打印机输出的bit(点)是可以直接映射(mapping)的,因此便有了BMP=bitmap这一名称。JPEG格式[插图]的文件是非可逆压缩,因此还原后的图像信息有一部分是模糊的。而GIF格式的文件虽然是可逆压缩,但因为有色数不能超过256色的限制,所以还原后颜色信息会有一些缺失,进而导致了图像模糊。TIFF格式的文件中带有各种标签信息,是可以选择压缩格式的。
1 应用的运行环境通常是用类似于Windows(OS)和AT兼容机(硬件)这样的OS和硬件的种类来表示的。
常用操作系统与CPU有(来自火绒官网):
2 CPU只能解释其自身固有的机器语言。不同的CPU能解释的机器语言的种类也是不同的。例如,CPU有x86、MIPS、SPARC、PowerPC,Alpha等几种类型,它们各自的机器语言是完全不同的。
3 机器语言的程序称为本地代码(native code)。文本文件(排除文字编码的问题)在任何环境下都能显示和编辑。我们称之为源代码。通过对源代码进行编译,就可以得到本地代码。
4 MS-DOS应用大多都是不经过操作系统而直接控制硬件的,而Windows应用则基本上都由Windows来完成对硬件的控制
5 应用程序向操作系统传递指令的途径称为API(Application ProgrammingInterface)。
6 Java,有两个层面的意思。一个是作为编程语言的Java,另一个是作为程序运行环境的Java。
编译后生成的并不是特定CPU使用的本地代码,而是名为字节代码的程序。字节代码的运行环境就称为Java虚拟机(JavaVM, Java VirtualMachine)。Java虚拟机是一边把Java字节代码逐一转换成本地代码一边运行的。编译器会将程序员编写的源代码(sample.java)转换成字节代码(sample.class)。而Java虚拟机(java.exe)则会把字节代码变换成x86系列CPU适用的本地代码,然后由x86系列CPU负责实际的处理。从操作系统方面来看,Java虚拟机是一个应用,而从Java应用方面来看,Java虚拟机就是运行环境。
7 BIOS存储在ROM中,是预先内置在计算机主机内部的程序。BIOS除了键盘、磁盘、显卡等基本控制程序外,还有启动“引导程序”的功能。引导程序是存储在启动驱动器起始区域的小程序。
1 即使是用不同编程语言编写的代码,转换成本地代码后,也都变成用同一种语言(机器语言)来表示了。
2 Dump是指把文件的内容,每个字节用2位十六进制数来表示的方式。
3 能够把C语言等高级编程语言编写的源代码转换成本地代码的程序称为编译器。交叉编译器,它生成的是和运行环境中的CPU不同的CPU所使用的本地代码。
4 为了得到可以运行的EXE文件,编译之后还需要进行“链接”处理。编译后生成的不是EXE文件,而是扩展名为“.obj”的目标文件。把多个目标文件结合,生成1个EXE文件的处理就是链接,运行连接的程序就称为链接器(linkage editor或连结器)。即使程序不调用其他目标文件的函数,也必须要进行链接,并和启动结合起来。
5 Windows中,API的目标文件,并不是存储在通常的库文件中,而是存储在名为DLL(Dynamic Link Library)文件的特殊库文件中。与此相反,存储着目标文件的实体,并直接和EXE文件结合的库文件形式称为静态链接库。
6 Windows中的编译及链接机制
7 那就是EXE文件中给变量及函数分配了虚拟的内存地址。在程序运行时,虚拟的内存地址会转换成实际的内存地址。
8 EXE文件的内容分为再配置信息、变量组和函数组,除此之外还有堆与栈。栈是用来存储函数内部临时使用的变量(局部变量[插图]),以及函数调用时所用的参数的内存区域。堆是用来存储程序运行时的任意数据及对象的内存领域。
9 栈及堆的相似之处在于,他们的内存空间都是在程序运行时得到申请分配的。不过,在内存的使用方法上,二者存在些许不同。栈中对数据进行存储和舍弃(清理处理)的代码,是由编译器自动生成的,因此不需要程序员的参与。使用栈的数据的内存空间,每当函数被调用时都会得到申请分配,并在函数处理完毕后自动释放。与此相对,堆的内存空间,则要根据程序员编写的程序,来明确进行申请分配或释放。
10 如果没有在程序中明确释放堆的内存空间,那么即使在处理完毕后,该内存空间仍会一直残留。这个现象称为内存泄露(memory leak)
11 根据开发工具种类的不同,有的编译器可以通过选择“Build”菜单来生成EXE文件。这种情况下,Build指的是连续执行编译和链接。
1 GUI:Graphical User Interface(图形用户界面)
2 应用通过系统调用(system call)间接控制硬件,操作系统和高级编程语言能够使硬件抽象化。这是个非常了不起的处理。
3 操作系统本身并不是单独的程序,而是多个程序的集合体
4 多任务指的是同时运行多个程序的功能。Windows是通过时钟分割技术来实现多任务功能的。Windows中还具有以程序中的函数为单位来进行时钟分割的多线程功能。
5 中间件而不是应用。意思是处于操作系统和应用的中间(middle)。
1 .asm是assembler(汇编器)的略写。高级编程语言的源代码中,即使指令和数据在编写时是分散的,编译后也会在段定义中集合汇总起来。
2 汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操作码)和针对汇编器的伪指令构成的。伪指令负责把程序的构造及汇编的方法指示给汇编器(转换程序)。由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为段定义
3 _TEXT是指令的段定义,_DATA是被初始化(有初始值)的数据的段定义,_BSS是尚未初始化的数据的段定义。
4 汇编语言指令的语法结构是操作码+操作数(也存在只有操作码没有操作数的指令)。
5 程序运行时,CPU会从内存中把指令和数据读出,然后再将其存储在CPU内部的寄存器中进行处理。内存中的存储区域是用地址编号来区分的。CPU内的寄存器是用eax及ebx这些名称来区分的。
6 指令中最常使用的是对寄存器和内存进行数据存储的mov指令。mov指令的两个操作数,分别用来指定数据的存储地和读出源。push指令和pop指令中只有一个操作数。该操作数表示的是“push的是什么及pop的是什么”。
7 在汇编语言中,函数名表示的是函数所在的内存地址。
8 最优化功能是编译器在本地代码上费尽功夫实现的,其目的是让编译后的程序运行速度更快、文件更小。
9 在函数内部利用的寄存器,要尽量返回到函数调用前的状态。为此,我们就需要将其暂时保存在栈中,然后再在函数处理完毕之前出栈,使其返回到原来的状态。
10 函数的参数是通过栈来传递,返回值是通过寄存器来返回的
11 局部变量是临时保存在寄存器和栈中的。寄存器空闲时就使用寄存器,寄存器空间不足的话就使用栈。
12 在汇编语言的源代码中,循环是通过比较指令(cmp)和跳转指令(jl)来实现的。比较结果小或相等时跳转的jle(jump on less or equal)、大或相等时跳转的jge(jump on greater orequal)、不管结果怎样都无条件跳转的jmp。
13 对了解计算机和程序的实际运行方式来说,体验汇编语言是最有效的。
1 在x86系列CPU用的汇编语言中,通过IN指令来实现I/O输入,OUT指令来实现I/O输出。用来实现计算机主机和外围设备输入输出交互的IC称为I/O控制器或简称为I/O。
2 IRQ指的是用来执行硬件中断请求的编号。
3 DMA指的是,不经过CPU中介处理,外围设备直接同计算机的主内存进行数据传输。
4 CPU内部的寄存器是用来进行数据运算处理的,而I/O寄存器则主要是用来临时存储数据的。
5 IN指令和OUT指令在端口号指定的端口和CPU之间进行数据的输入输出。这和通过内存地址来进行主内存的读写是一样的道理。
6 大部分C语言的处理(编译器的种类)中,只要使用_asm{和}括起来,就可以在其中记述助记符。也就是说,这样就可以编写C语言和汇编语言混合的源代码。
7 IRQ是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制称为中断处理。实施中断请求的是连接外围设备的I/O控制器,负责实施中断处理程序的是CPU。我们可以在I/O控制器和CPU中间加入名为中断控制器的IC来进行缓冲。
8 在中断请求完毕后,各寄存器的数值必须要还原到中断前的状态。只要寄存器的值保持不变,主程序就可以像没有发生任何事情一样继续处理。
9 DMA是指在不通过CPU的情况下,外围设备直接和主内存进行数据传送。磁盘等都用到了这个DMA机制。
10 I/O端口号、IRQ、DMA通道可以说是识别外围设备的3点组合。
11 显示器中显示的信息一直存储在某内存中。该内存称为VRAM(Video RAM)。
1 由于借助公式产生的随机数具有一定的规律性,因此并不是真正的随机数,通常称为伪随机数。不过,虽然是伪随机数,仍然十分有用。
1 C语言虽是高级编程语言,但它也具备了能够和汇编语言相媲美的低层处理(内存操作及位操作)功能。作为Unix系列操作系统之一的Linux也是用C语言来编写的。
2 C语言的程序就是由变量和函数构成的
3 函数
4 函数的处理内容是用{}围起来的部分。{}围起来的部分称为模块。
5 了解语法结构但不会编写程序”和“知道英文语法却不会说英语”是同样的。不管是C语言还是英语,都是从实践中得来的。