本篇文章是《程序是怎么跑起来的》读书笔记。
在学习的过程中,会发现: 知道整个程序运行的过程,尤其是流程图和内存图是很重要的,所以找此书来读。
下面的文章,会以章节为顺序对一些内容进行摘录,并有一些自己语言的总结。同时,会在文章末尾记录一些名词的扎英文对照。下面开始:
CPU
程序是由指令和数据构成的。
CPU所负责的就是解释和运行最终转换成机器语言的程序内容。
从功能方面来看, CPU 的内部由寄存器、控制器、运算器和时钟四个部分构成,各部分之间由电流信号相互连通。
寄存器 可用来暂存指令、数据等处理对象,可以将其看作是内存的一种。
控制器 负责把内存上的指令、数据等读入寄存器,并根据指令的执行结果来控制整个计算机。
运算器 负责运算从内存读入寄存器的数据。
时钟 负责发出 CPU 开始计时的时钟信号。不过,也有些计算机的时钟位于 CPU 的外部。
汇编语言 (assembly) 采用助记符 (memonic) 来编写程序,每一个原本是电气信号的机器语言指令都会有一个与其相应的助记符,助记符通常为指令功能的英语单词的简写。
通常我们将汇编语言编写的程序转化成机器语言的过程称为 汇编 ;反之,机器语言程序转化成汇编语言程序的过程则称为 反汇编 。
CPU是具有各种功能的寄存器的集合体。其中,程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器 (基址,变址,通用) 一般有多个。
CPU每执行一个指令,程序计数器的值就会自动加1。也就是说,程序计数器 决定着程序的流程。
标志寄存器 实现了对程序的流程控制。
函数调用处理是通过把程序计数器的值设定成函数的存储地址来实现的。
函数调用使用的是call指令,而不是跳转指令。在将函数的入口地址设定到程序计数器之前,call指令会把 调用函数后要执行的指令地址 存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行return
命令。return
命令的功能是把保存在栈中的地址设定到程序计数器中。
第一张结束了,通过第一章。我们要知道几个基本的知识:
- 程序就是数据和指令
高级编程语言
->汇编语言
->机器语言
二进制
因为 IC 的所有引脚,只有直流电压 0V 或 5V 两个状态。所以,决定了计算机的信息数据只能用二进制数来处理。
计算机处理信息的最小单位——位,就相当于二进制中的一位。位的英文 bit
是二进制数位(binary digit)
的缩写。
8位二进制数被称为一个 字节 。字节是最基本的信息计量单位。位是最小单位,字节是基本单位。
二进制数的值转换成十进制数的值,只需将二进制数的各数位的值和 位权 相乘,然后将相乘的结果相加即可。
移位运算 指的是将二进制数值的各数位进行左右移位 (shift=移位) 的运算。移位有左移 << (向高位方向) 和右移 >> (向低位方向) 两种。
为了获得补数,我们需要将二进制数的各数位的数值全部取反,然后再将结果加1。将二进制数的值取反后加1的结果,和原来的值相加,结果为 0
当二进制数的值表示图形模式而非数值时,移位后需要在最高位补0。类似于霓虹灯往右滚动的效果。这就称为逻辑右移。
将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值 (0或1) 。这就称为算术右移。如果数值是用补数表示的负数值,那么右移后在空出来的最高位补1,就可以正确地实现1/2、1/4、1/8等的数值运算。如果是正数,只需在最高位补0即可。
只有在右移时才必须区分逻辑位移和算术位移。左移时,无论是图形模式 (逻辑左移) 还是相乘运算 (算术左移) ,都只需在空出来的低位补 0 即可。
符号扩充就是指在保持值不变的前提下将其转换成16位和32位的二进制数。也就是说,不管是正数还是用补数表示的负数,都只需用符号位的值 (0或者1) 填充高位即可。
计算机能处理的运算,大体可分为算术运算和逻辑运算。算术运算是指加减乘除四则运算。逻辑运算是指对二进制数各数字位的0和1分别进行处理的运算,包括逻辑非(NOT运算)、逻辑与(AND运算)、逻辑或(OR运算)和逻辑异或(XOR运算)四种。
计算机中进行小数计算时出错的原因
经典面试题目: 0.1 + 0.2 = ?
是因为 “有一些十进制数的小数无法转换成二进制数”
双精度浮点数 类型用64位、单精度浮点数 类型用32位来表示全体小数
浮点数 是指用符号、尾数、基数和指数这四部分来表示的小数
64: 1位符号,11位指数,52位尾数
32: 1位符号,8位指数,23位尾数
如何避免计算机计算出错
首先是回避策略,即无视这些错误。根据程序目的的不同,有时一些微小的偏差并不会造成什么问题。
把小数转换成整数来计算。计算机在进行小数计算时可能会出错,但进行整数计算(只要不超过可处理的数值范围)时一定不会出现问题。
在涉及财务计算等不允许出现误差的情况下,一定要将小数转换成整数或者采用BCD方法,以确保最终得到准确的数值。BCD (Binary Coded Decimal) 是一种使用二进制表示十进制的方法。
在以位为单位表示数据时,使用二进制数很方便,但如果位数太多,看起来就比较麻烦。因此,在实际程序中,也经常会用十六进制数来代替二进制数。
内存
内存 实际上是一种名为内存IC的电子元件。内存IC中有电源
、地址信号
、数据信号
、控制信号
等用于输入输出的大量引脚(IC的引脚),通过为其指定地址(address),来进行数据的读写。
编程语言中的数据类型表示存储的是何种类型的数据。
从内存来看,就是占用的内存大小的意思。
即使是物理上以1个字节为单位来逐一读写数据的内存,在程序中,通过指定其类型(变量的数据类型等),也能实现以特定字节数为单位来进行读写。
指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址。通过使用指针,就可以对任意指定地址的数据进行读写。
数组是高效实用内存的基础。
数组是指多个同样数据类型的数据在内存中连续排列的形式。
作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引(index)
。
指定索引后,就可以对该索引所对应地址的内存进行读写操作。而索引和内存地址的变换工作则是由编译器自动实现的。
数组的定义中所指定的数据类型,也表示一次能够读写的内存大小。
栈用的是LIFO
(Last Input First Out,后入先出)方式,而队列用的则是FIFO
(First Input First Out,先入先出)方式。
队列一般是以环状缓冲区
(ring buffer)的方式来实现的
在数组的各个元素中,除了数据的值之外,通过为其附带上下一个元素的索引,即可实现链表。使用链表来追加或删除数据则毫不费事。
内存和磁盘
计算机中主要的存储部件是内存和磁盘。磁盘中存储的程序,必须要加载到内存后才能运行。在磁盘中保存的原始程序是无法直接运行的。这是因为,负责解析和运行程序内容的CPU,需要通过内部程序计数器来指定内存地址,然后才能读出程序。即使CPU可以直接读出并运行磁盘中保存的程序,由于磁盘读取速度慢,程序的运行速度还是会降低。总之,存储在磁盘中的程序需要读入到内存后才能运行。
磁盘缓存 (disk cache) 指的是把从磁盘中读出的数据存储到内存空间中的方式。这样一来,当接下来需要读取同一数据时,就不用通过实际的磁盘,而是从磁盘缓存中把内容读出。使用磁盘缓存可以大大改善磁盘数据的访问速度
虚拟内存 (virtual memory) 是指把磁盘的一部分作为假想的内存来使用。
虚拟内存虽说是把磁盘作为内存的一部分来使用,但实际上正在运行的程序部分,在这个时间点上是必须存在在内存中的。也就是说,为了实现虚拟内存,就必须把实际内存(也可称为物理内存)的内容,和磁盘上的虚拟内存的内容进行部分置换(swap),并同时运行程序。
节约内存的编程方法
(1) 通过DLL文件实现函数共有
DLL(Dynamic Link Library)文件,顾名思义,是在程序运行时可以动态加载Library(函数和数据的集合)的文件。此外,还有一个需要大家注意的地方,那就是多个应用可以共有同一个DLL文件。而通过共有同一个DLL文件则可以达到节约内存的效果。
Windows的操作系统本身也是多个DLL文件的集合体。有时在安装新应用时,DLL文件也会被追加。应用则会通过利用这些DLL文件的功能来运行。
(2) 通过调用_stdcall来减小程序文件的大小
栈清理处理是指,把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域中清理出去。
该命令不是程序记述的,而是在程序编译时由编译器自动附加到程序中的。编译器默认将该处理附加在函数调用方。
栈清理处理,比起在函数调用方进行,在反复被调用的函数一方进行时,程序整体要小一些。这时所使用的就是_stdcall。
在函数前加上_stdcall,就可以把栈清理处理变为在被调用函数一方进行。
磁盘的物理结构
扇区是对磁盘进行物理读写的最小单位。
不管是硬盘还是软盘,不同的文件是不能存储在同一个簇中的,否则就会导致只有一方的文件不能被删除。因此,不管是多么小的文件,都会占用1簇的空间。这样一来,所有的文件都会占用1簇的整数倍的磁盘空间。
以簇为单位进行读写时,1簇中没有填满的区域会保持不被使用的状态。虽然这看起来是有点浪费,不过该机制就是如此规定的,所以我们也没有什么好办法。另外,如果减少簇的容量,磁盘访问次数就会增加,就会导致读写文件的时间变长。由于在磁盘表面上,表示扇区区分的领域是必要的,因此,如果簇的容量过小,磁盘的整体容量也会减少。扇区和簇的大小,是由处理速度和存储容量的平衡来决定的。
压缩数据
RLE算法: 把文件内容用数据×重复次数
的形式来表示的压缩方法称为RLE (Run Length Encoding,行程长度编码) 算法。
在实际的文本文件中,同样字符多次重复出现的情况并不多见。虽然针对相同数据经常连续出现的图像、文件等,RLE算法可以发挥不错的效果,但它并不适合文本文件的压缩。
莫尔斯编码是根据日常文本中各字符的出现频率来决定表示各字符的编码的数据长度的。
使用哈夫曼树后,出现频率越高的数据所占用的数据位数就越少,而且数据的区分也可以很清晰地实现。
程序是在何种环境中运行的
应用的运行环境,是指操作系统
和计算机本身(硬件)
的种类
机器语言的程序称为本地代码 (native code)
程序员用C语言等编写的程序,在编写阶段仅仅是文本文件。文本文件(排除文字编码的问题)在任何环境下都能显示和编辑。我们称之为源代码。
应用程序向操作系统传递指令的途径称为API (ApplicationProgramming Interface)
BIOS (BasicInput/Output System)。BIOS存储在ROM中,是预先内置在计算机主机内部的程序。BIOS除了键盘、磁盘、显卡等基本控制程序外,还有启动“引导程序”的功能。
引导程序是存储在启动驱动器起始区域的小程序。操作系统的启动驱动器一般是硬盘,不过有时也可以是CD-ROM或软盘。
从源文件到可执行文件
源代码完成后,就可以编译生成可执行文件了。负责实现该功能的是编译器。Dump
是指把文件的内容,每个字节用2位十六进制数来表示的方式。
能够把C语言等高级编程语言编写的源代码转换成本地代码的程序称为编译器。
但实际上,仅仅靠对应表是无法生成本地代码的。读入的源代码还要经过语法解析、句法解析、语义解析等,才能生成本地代码。
编译器转换源代码后,就会生成本地文件。不过,本地文件是无法直接运行的。为了得到可以运行的EXE文件,编译之后还需要进行“链接”处理。
把多个目标文件结合,生成1个EXE文件的处理就是链接,运行连接的程序就称为链接器(linkage editor或连结器)
1 | ilink32 -Tpe -c -x -aa c0w32.obj Sample1.obj, Sample1.exe, , |
c0w32.obj这个目标文件记述的是同所有程序起始位置相结合的处理内容,称为程序的启动。
因而,即使程序不调用其他目标文件的函数,也必须要进行链接,并和启动结合起来。
像import32.lib及cw32.lib这样的文件称为库文件。
库文件指的是把多个目标文件集成保存到一个文件中的形式。
链接器指定库文件后,就会从中把需要的目标文件抽取出来,并同其他目标文件结合生成EXE文件。
sprintf()
等函数,不是通过源代码形式而是通过库文件形式和编译器一起提供的。这样的函数称为标准函数
。
之所以使用库文件,是为了简化为链接器的参数指定多个目标文件这一过程。
Windows中,API的目标文件,并不是存储在通常的库文件中,而是存储在名为DLL(Dynamic Link Library)文件的特殊库文件中。
就如Dynamic这一名称所表示的那样,DLL文件是程序运行时动态结合的文件。
与此相反,存储着目标文件的实体,并直接和EXE文件结合的库文件形式称为静态链接库。
静态(static=静态的)同动态(dynamic=动态的)是相反的意思。存储着sprintf()
的目标文件的cw32lib就是静态链接库。sprintf()
提供了通过指定格式把数值转换成字符串的功能。
那就是EXE文件中给变量及函数分配了虚拟的内存地址。在程序运行时,虚拟的内存地址会转换成实际的内存地址。链接器会在EXE文件的开头,追加转换内存地址所需的必要信息。这个信息称为再配置信息。
EXE文件的再配置信息,就成为了变量和函数的相对地址。相对地址表示的是相对于基点地址的偏移量,也就是相对距离。
EXE文件的内容分为再配置信息、变量组和函数组
当程序加载到内存后,除此之外还会额外生成两个组,那就是栈和堆。
栈是用来存储函数内部临时使用的变量(局部变量),以及函数调用时所用的参数的内存区域。
堆是用来存储程序运行时的任意数据及对象的内存领域
EXE文件中并不存在栈及堆的组。栈和堆需要的内存空间是在EXE文件加载到内存后开始运行时得到分配的。
因而,内存中的程序,就是由用于变量的内存空间
、用于函数的内存空间
、用于栈的内存空间
、用于堆的内存空间
这4部分构成的。
栈及堆的相似之处在于,他们的内存空间都是在程序运行时得到申请分配的。不过,在内存的使用方法上,二者存在些许不同。栈中对数据进行存储和舍弃(清理处理)的代码,是由编译器自动生成的,因此不需要程序员的参与。使用栈的数据的内存空间,每当函数被调用时都会得到申请分配,并在函数处理完毕后自动释放。与此相对,堆的内存空间,则要根据程序员编写的程序,来明确进行申请分配或释放。
栈及堆的相似之处在于,他们的内存空间都是在程序运行时得到申请分配的。
栈中对数据进行存储和舍弃(清理处理)的代码,是由编译器自动生成的,因此不需要程序员的参与。使用栈的数据的内存空间,每当函数被调用时都会得到申请分配,并在函数处理完毕后自动释放。
与此相对,堆的内存空间,则要根据程序员编写的程序,来明确进行申请分配或释放。
如果没有在程序中明确释放堆的内存空间,那么即使在处理完毕后,该内存空间仍会一直残留。这个现象称为内存泄露(memory leak)
Q:编译器和解释器有什么不同?
A:编译器是在运行前对所有源代码进行解释处理的。而解释器则是在运行时对源代码的内容一行一行地进行解释处理的。
Q:“分割编译”指的是什么?
A:将整个程序分为多个源代码来编写,然后分别进行编译,最后链接成一个EXE文件。这样每个源代码都相对变短,便于程序管理。
Q:“Build”指的是什么?
A:根据开发工具种类的不同,有的编译器可以通过选择“Build”菜单来生成EXE文件。这种情况下,Build
指的是连续执行编译和链接。
Q:使用DLL文件的好处是什么?
A: DLL文件中的函数可以被多个程序共用。因此,借助该功能可以节约内存和磁盘。此外,在对函数的内容进行修正时,还不需要重新链接(静态链接)使用这个函数的程序。
Q:不链接导入库的话就无法调用DLL文件中的函数吗?
A:通过使用LoadLibrary()及GetProcAddress()这些API,即使不链接导入库,也可以在程序运行时调用DLL文件中的函数。不过使用导入库更简单一些。
Q:“叠加链接”这个术语指的是什么?
A:将不会同时执行的函数,交替加载到同一个地址中运行。通过使用“叠加链接器”这一特殊的链接器即可实现。在计算机中配置的内存容量不多的MS-DOS时代,经常使用叠加链接。
IDE: Integrated Development Environment,即继承了变成所需的各种工具的开发软件。
CPU: Central Processing Unit (中央处理器)
IC: Integrated Circuit (集成电路)
DLL: Dynamic Link Library
- 本文作者: Jambo
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!