程序是怎样跑起来的
时间开销:
共计11h,273页。
阅读+随手记
181112: 2h
181113: 2h
181114: 2.5h
181121: 1.5h
181126: 1.5h
181127: 0.5h
小计:10h
总结笔记
181127:1h
分章节笔记
00 序言
这本书原本2001年10月出版第1版,这是翻译的原版第二版的内容,不过是2015年4月的译版第1版。
整体来说,这本书更加偏向底层硬件,计算机组成原理+操作系统+汇编语言,这部分刚好更是自己不了解的地方,仅仅只是看目录就可以看到很多地方都不是很懂了,有很多专业词汇,虽然这本书(2001年)写在那本书之前(2003年)但是其实可能更难理解。
全文共计12章+附录,每章关键词:CPU、二进制表示、小数运算、内存使用、内存和磁盘、压缩、程序运行环境、代码编译、操作系统和应用、程序实际构成(汇编)、硬件控制、AI、C语言附录。
看完之后的整体感觉章节与章节之间的取名or关联性有点体现不出来,讲解的都是一些比较小的点,如果要讲解程序是怎么跑起来的,可能可以考虑先从计算机的底层硬件开始讲起,然后讲解操作系统是怎么屏蔽程序直接调用底层硬件控制的结构,最后上升到上层应用程序调用操作系统的接口,怎么进行编译成本地代码然后运行的。——这么一想,可能这本书的确是这么考虑的,但是在章节名与章节名之间有点看不出来内部的关联性,对初学者有点难以理解吧。
01 对程序员来说CPU是什么
181112:1917:整体感觉这本书的理解程度比03年的计算机是如何跑起来的要难一些,专业性更强,很多地方的理解如果不是有一定基础还真的不太好理解,这章就更偏向计算机组成原理那门课的那本书了。
CPU就是寄存器的集合,一共有8种寄存器,其中有5种寄存器只有一个,而其他3个寄存器则有多个,CPU能够完成的功能其实很少,主要就是数据传送、运算、跳转、call/return这四种,实现了所有高级复杂的功能。
02 数据是用二进制数表示的
181112:2332:这章讲解二进制比较关键的点其实是补数相关的操作,其他都还是比较容易理解。因为计算机做减法运算时,内部实际上在做加法运算,所以当需要表示负数时,如1的负数即-1,就是所谓1的补数,使用1的二进制数取反加1得到。当计算结果是负数的时候,如3-5,即3+(-5)的补数,计算出来的结果再取反加一求补数,就能够得到绝对值的数。左移运算时直接低位补零即可,但是右移运算时,则需要把空白出来的高位补充成该数值原始最高位符号位的值,即-4右移之后,应该变成-1才对,11111111;同理在符号扩充也是,需要把符号位的数字,扩充到所有的高位中。剩下的就是进制与进制之间的转换公式,以及加减乘除四则算术运算和与或非异或四个逻辑运算的东西,比较简单了。
03 计算机进行小数运算时出错的原因
181113:1444:二进制表示小数比表示整数复杂很多。先不考虑计算机内部的小数表示,先从纸质手写上看二进制小数转换到十进制小数的方法,方法和整数转化类似,还是通过基数位权求和,小数点左边的数字按照位权从0,1,2递增,小数点右边的按照位权-1,-2,-3递减;十进制的小数转换为二进制,则是小数部分乘以2,取整数部分依次从左往右放在小数点后,直到小数部分为0或者位数已经够了。——其他进制转换成十进制,其实都是采用基数位权求和的方法,只是不同进制,基数不同而已;而十进制转换成其他进制,也是同理,除基数取余,然后倒序排列,高位补零;而如果是某种进制的小数的话,则是乘基数取整数,依次放到后面,直至小数部分为0或位数已够。
然后考虑计算机内部的小数表示(最终都是二进制表示),无论单精度还是双精度,都是用浮点数表示法,他俩的区别主要在于支持的位数不同,32(1+8+23)/64(1+11+52),分别是符号位+指数+尾数。浮点数表示有一个统一的规范,写成十进制小数时,要遵循十进制小数点前面为0,小数点后面第1位不能为0的规则,而写成二进制小数时,则要对原本的小数点为进行逻辑左移操作,直至小数点左边第一位为1,左移了多少位,就是所谓的指数,而小数点右边的数字则是尾数部分,不够23/52位直接补0即可。由于二进制存储的时候小数点左边的第一个1没有存进去,实际上二进制23/52位的尾数,可以表示十进制中小数点后24/53位的小数。指数部分采用了EXPRESS系统表现,即通过把能够表示的范围中间的那个数作为0,以消除符号位,比如说8位的指数部分,不考虑符号位,能够正常表示0255的数字,现在把11111111除以2,舍弃小数,得到1/2中间的数01111111,普通情况下其值表示127,在EXPRESS中认为其值为0,这样从00000000-01111111就是-1270的值了。
正因为二进制小数在计算机中如此保存,有一些在十进制中能够表示的小数,在二进制中就无法表示,如0.1等,在二进制表示中能够发现其是二进制中的无限循环小数,如1/3在10进制中也是无限循环小数一样。
避免这种计算机计算错误的方式:
- 回避,即无视这些错误(可能就直接按需要位数进行截断):在一些精度要求不高的场景下,只要得到近似值即可
- 把小数转化成整数进行计算,先乘10^x把小数变大,然后算完之后再除回来
- 使用BSD方法,这里没有详细介绍,但是说如果精度要求很高,则一定需要使用第二和第三种方法才行。
整体来说,进制之间的转换一直没有弄得很清楚,并且数字到底在计算机中如何存储的也不是很明白,这章算是把之前没有弄明白的地方做了很好的补充。
04 熟练使用有棱有角的内存
181113:1543:先简单介绍了物理上的内存表现形式,通过地址信号、数据信号、控制信号来进行物理上的内存操作,数据信号的引脚个数,决定了一次性能够读入内存的数据大小范围,地址信号引脚的个数,决定了内存中能够指向的内存块个数,两者相乘得到的就是该内存IC的大小,多数情况下,地址信号引脚的个数会更多,使得内存容量更大。所谓指针也是一种变量,表示其值为内存地址的变量,所以指针变量的大小和CPU的内存地址位数一致,而该指针指向的数据类型无论是什么,指针变量的大小都一样,只是该指针能够读取的内存长度受数据类型的限制。后面介绍了逻辑上的内存操作,主要是基于数组的各种数据结构的介绍,和前一本书差不多,多了一个环状缓冲区的内容,差别不大。
05 内存和磁盘的亲密关系
181114:2034:动态链接和静态链接的区别在于,一个是程序运行时才被调用加载到内存,一个是本身就编码在程序中,程序一运行就已经被加载到内存了;扇区是磁盘保存数据的最小物理单位,但是簇是保存文件的最小逻辑单位,无论文件有多小,只要没有超过1簇的大小,就必须要占有1簇的物理磁盘扇区位置,虽然看起来有点浪费,但是簇如果越小,存取大文件的时候就回需要更多的磁盘读写操作,降低运行速度,簇的大小是磁盘读写速度和磁盘存储容量的平衡;程序运行必须要先从磁盘中加载到内存中,然后才能够被CPU根据内存地址进行调用执行;磁盘缓存指把数据从磁盘中调用到内存中存储,改善磁盘获取数据的速度(预取),是假想的内存;虚拟内存则是把假想的磁盘,把磁盘的一部分当做内存来使用,实际运行中其实是在虚拟内存(磁盘)和真实内存之间做了数据的置换,把程序运行暂时没有用到的放在虚拟内存中,真正运行的放在真实内存中;置换分为分页式和分段式,windows和linux貌似都是分页式;无论是磁盘缓存还是虚拟内存都是为了解决真实内存容量小的问题,但都是治标不治本,如果要从根本上解决还是只能增加内存,不过反过来也可以减少程序内存开销;有两种在编程时节约内存的方法(虽然现在磁盘、内存都越来越大),一个是善于使用动态链接库,把公共使用的函数尽可能的抽象出来成dll;一个是使用C中_stdcall的思路,默认的栈调用后的清理程序由发起调用的函数执行,因为清理程序必须要知道函数的参数个数和类型,但如果是在函数前加这个符号,即可告诉被调用程序,该函数的参数和类型是不变的,即可让被调用函数自己执行栈调用后的清理程序,从汇编语言上看,能够节约3个字节的内存,积少成多还是不错的。
06 亲自尝试压缩数据
181114:2115:虽然感觉有点莫名其妙在这里加了一章讲解压缩文件的东西,不过算是复习了下,之前学习信息论的时候就了解过信息的编码及压缩算法,图像处理的时候也接触过一些,还算是比较简单;主要复习了下RLE行程长度编码和哈夫曼编码;以及可逆压缩和非可逆压缩,对于文本文件及一些EXE执行文件来说,只能使用可逆编码,或者说,大多数情况下大家都是要求可逆编码的,而对于一些图像处理、图像文件保存不同格式来说,即使非可逆压缩损失了一些信息,只要肉眼分辨不出来也都是OK的;此外需要强调的一点是,所有的文件其实都是二进制数字保存在文件中。
07 程序是在何种环境中运行的
181114:2141:这一章简单介绍了硬件(CPU)、(BIOS)、操作系统、应用之间的关系;不同的CPU,具有其独有的机器语言,只能解释运行它自己的那一种机器语言,所以不同的硬件需要不同的编译器,把相同的源代码编译成不同的硬件能够理解的(早期就存在基于不同硬件的专用应用)本地代码;操作系统非常棒的地方在于,它屏蔽了各种不同硬件带来的区别,虽然不同硬件也需要装上同一种操作系统的不同专用版本,但是对于上层应用来说,它都是在同一种操作系统中进行运行的,已经省了很大一笔工夫,原本直接调用硬件接口的东西,现在都通过调用操作系统的API接口,由操作系统来间接调用了;同时,如果是类似FreeBSD这种提供ports移植源代码机制的,从源码仓库中下载源码,本地重新编译得到本地代码,可以说是很方便了,但是windows不是;同时,考虑到不同的硬件又可以安装不同种类的操作系统,如Windows、Linux、MacOS等,同一个应用想要在不同种类的操作系统中进行运行,就还必须要针对每种操作系统开发不同的版本,因为每种操作系统提供的API不同(为什么不能只存在一种操作系统呢?);这里提供了两种解决方案,一个是在操作系统A中安装支持操作系统B的虚拟机,在虚拟机中直接安装一个操作系统B,然后在操作系统B中运行特定版本的应用(多数使用Mac的都会装一个支持Windows的虚拟机实例),一个是使用Java虚拟机环境;Java虚拟机环境和前面直接装个虚拟机的区别在于,前面是直接虚拟了一个操作系统B,但是如果要在操作系统C中运行,还得再装一个操作系统C的虚拟机实例,而Java不同,它提供的是一个程序运行环境的平台(当然不同的操作系统需要安装特定版本的Java虚拟机),它其实也是一个应用,运行在操作系统之上,屏蔽了前面不同种类操作系统带来的API接口的不同,这样如果一个应用的源码想在不同的操作系统中都能够运行的话,就在Java虚拟环境中进行编译,它每次运行的时候把Java源码先编译成在Java虚拟环境能够处理的字节代码,然后JavaVM再把字节代码处理成本地代码,JavaVM虽然屏蔽了操作系统的区别,但是也有它的不足之处,首先是有些应用调用的API可能不能支持在所有的JavaVM中都能够运行,其次由于每次都要生成字节代码,运行速度受到了一定影响;不过也有一些对JavaVM的优化方式,比如第一次生成的本地代码保存起来,以后直接运行,或者改善生成的本地代码的质量优化耗时较长的部分;除此之外需要了解到BIOS这个程序,它是内置在计算机主机内部的程序,一开机就会启动,它的作用是启动“引导程序”,而引导程序的作用则是把操作系统加载到内存运行,然后操作系统才能够开机运行支持其他应用的运行。
08 从原文件到可执行文件
181121:2307:这章虽然讲得比较简单入门,但是还是有一些东西是第一次才看到。之前知道的是,源码编译之后得到目标文件, 然后再经过链接得到最终的可执行文件,整个过程是没有问题的,但是很多细节的地方没有深入了解到。首先,源码编译之后得到的目标文件,其实已经是机器语言,即本地代码,能够被CPU解释看懂的了;其次,仅仅只是经过编译,得到的文件虽然CPU能够看懂,但是却不能加载到内存中运行,因为其中调用了其他的库文件函数,需要把那些库文件中的对应的函数本体包含在一起,才能够运行;这里有个问题是,如果任何外部函数都没有调用,是不是只用编译即可?这里以一个编译器举例说,即使没有调用任何库函数,仍然需要链接,因为需要和编译器本身提供的一个目标文件进行结合,即程序的启动,它是同所有程序起始位置相结合的处理内容(这句话说实话没看懂),gcc编译的时候并没有看到gcc有需要一个什么启动,或者已经被隐藏了没有看到;然后链接时,之前只知道有静态链接库.lib和动态链接库.dll/.so,这里还讲了一种库文件,导入库,后缀也还是.lib库文件的后缀,但是它内部存储的其实并不是函数本体,而是存储着动态链接库的索引位置,包括具体的这个函数需要的那一个库文件以及整个动态链接库在主机中存储的位置,相当于“指针”的作用,编译器根据它的索引,找到对应真正存储调用函数本体的动态链接库。所谓DUMP,就是把文件中的内容按照每个字节2位十六进制的数表示。决定不同编译器的有3个关键点:编程语言、CPU型号、操作系统环境,这里了解到一个比较新的概念是交叉编译器,即能够生成和运行环境CPU不同的CPU型号的本地代码。编译器通过对源代码进行语法解析、句法解析、语义解析等,得到本地代码,理解就是翻译器。这里想到一个问题:是不是可以通过变成同一种机器语言,来做到多种编程语言之间的转换呢?用Java编写的源码编译之后得到的本地代码,通过某种反编译C编译器是否能够自动生成C语言的代码呢?后续可以调研一下。链接形成的EXE文件,是由再配置信息、变量组、函数组构成的,再配置信息保存的是转换内存地址所必需的信息,因为EXE给变量和函数分配了虚拟的内存地址,但是实际上最终CPU分配的真实地址每次程序运行时都是不一样的。再配置信息就是变量和函数的相对地址,其实就是相对于组基点的偏移量。简单的说,链接后,调整了源码中变量和函数的顺序,把它们编成了变量组和函数组,然后在再配置信息中存储着调整后的变量组和函数组的偏移量。EXE运行时则还会在内存中添加2种组,栈和堆,栈保存着函数调用时的局部变量,当函数调用完毕,其后续的内存回收机制则是由编译器自动完成;而堆则是需要自己申请自己释放,如果只申请不释放就会导致内存泄漏,直至最终无内存可用,程序可能就会出现万恶的segementation fault。分割编译将多个源码分别编译,便于程序管理。编译器和解释器的区别在于,编译器是在程序运行前对源码整个一次性进行解释处理,而解释器则是在运行时对源码一行一行的解释处理。不链接导入库也可以在程序运行调用dll文件中的函数,只要通过使用LoadLibrary()等API(之前用过Python加载C编译完成的.so,就是使用的Python的一个加载函数接口API,而不是使用Python解释器对.so进行的解释,因为语言不通根本解释不了)。叠加链接指把不会同时执行的函数交替加载到同一个地址中运行,需要使用叠加编译器才行,这是为了节约内存,不知道当前是否还存在这种功能。C/C++都需要自己申请自己释放的垃圾回收机制,而Java、C#等,程序运行环境会自动进行垃圾回收。
09 操作系统和应用的关系
181126:1958:这章比较短也比较简单,主要强调了操作系统、应用、网络数据库等中间件之间的关系,大多数程序员编写的都是应用,并不直接与计算机硬件进行交互,造作计算机硬件接口,而是由操作系统把底层与硬件的交互屏蔽,提供其系统调用的API,供高级编程语言进行调用和实现其自身应用的功能。WYSIWYG表示所见即所得。所谓监控程序可以说是操作系统的原型,OS是由它慢慢演化而来。设备驱动是新的设备连接到计算机时自动安装的能够控制设备的程序,也可以说是该设备提供给想要调用它的应用,可调用的API接口。后面对Windows操作系统的一些功能进行了介绍,如GUI/多任务等。这里需要强调一点的是,GUI编程的逻辑和传统的命令行程序不同,传统的程序是由程序员决定程序的执行逻辑,用户根据默认已经制定好的程序逻辑往下进行即可,而GUI的执行逻辑由用户决定,用户想要如何点击按钮,这个顺序是不可控的,GUI编程则需要考虑这一点。
10 通过汇编语言了解程序的实际构成
181126:2033:整章用了个例子,详细讲解了程序过程中汇编语言的运行机制。本地代码和汇编语言是一一对应的。汇编语言的语法是操作码+操作数,即指令动作+指令对象。CPU中各种名字的寄存器的作用是不一样的。函数的参数通过栈来传递,函数的返回值通过寄存器来返回。编译器具有表示段定义的伪指令,伪指令负责把程序的构造及汇编的方法告诉汇编器(转换程序)。后面对全局变量和局部变量简单说明了一下,局部变量只在函数运行的时候保存在寄存器和栈上,CPU更倾向使用寄存器,因为它的处理速度更快。另外只对局部变量进行定义是不行的,一定要对局部变量进行赋值之后才会给局部变量分配寄存器。最后指出虽然C语言等建议不使用goto语句进行跳转,但是在汇编语言这一层级,它必须使用类似goto语句的跳转指令,才能够实现循环和条件分支的功能。最最后说了下了解程序底层运行机制的必要性,知其然,更知其所以然。
11 硬件控制方法
181126:2336:汇编语言中利用IN/OUT指令对硬件的输出输出进行控制;而外围设备通过中断请求IRQ来中断CPU的操作,外围设备的中断端口与其他I/O设备不同,叫中断编号;利用DMA功能,磁盘等存储大量数据的外围设备能够不经过CPU直接同主内存进行数据传输,省去了CPU切换处理的时间;显示器中显示的信息一直存储在VRAM内存中,以前VRAM是作为主存的一部分,而现在,显卡中一般配备与主存独立的VRAM和GPU专门用来做图形图像处理。感觉这章内容还挺新鲜,但是也没讲特别深的东西,挺容易理解。
12 让计算机“思考”
181127:0916:这一章有点神奇,循序渐进的通过实现猜拳游戏的例子,完成了程序实现人类思考方式的模拟。先是把人类思考的习惯添加进去,使得程序习惯性的喜欢出某一种拳;然后采用随机数,让计算机随机出拳(这里简单介绍了随机数和伪随机数,以及随机数种子,举例线性同余法作为生成伪随机数的算法),把直觉嵌入;以及活用计算机的记忆功能,把经验嵌入进去,提高出拳胜率;最后还把思考方式,作为思考方法的节奏给嵌入进去,当连输两局的时候就换一种思考方式提高胜率。整体来说猜拳游戏比较简单,涉及了一点点AI的皮毛,跟AlphaGo那种比不了,不过还是有点点启发,直觉、习惯、经验、思考方式,当前的计算机有思考功能吗?感觉是有的,只是还没有自我意志。
附录:让我们开始C语言之旅
这里只是简单的介绍了下C语言的基础知识,如果要入门的话,还是得看一本系统一点专门讲解C语言入门的书比较好。
全书总结:
181127:1004:一共花了10h看完这本书,但是时间上跨度有两周。整体来看这本书一些章节构造不是很能理解为何要这样写。比如第2章和第三章,主要讲解了二进制数以及其涉及到的二进制计算的问题,但是和整体的程序运行机制貌似没有很直接的关联,或许只要提一句计算机中的任何文件都是由二进制表示的即可?感觉缺了这两章也没有太大影响。另外比如第6章讲解压缩的部分,完全不明白为什么要突然转头说到压缩,感觉和程序怎么跑起来也没有什么太大关系,压缩和内存or数据保存有一定关联但是好像删掉这一张也没有太大影响。剩下的第1/4/5/7/8/9/10/11中,7章之前的内容算是计算机基础环境,有助于后面的理解,第7章之后的内容就直接和标题相关了,程序是怎么运行的,是怎么生成可执行文件的,怎么在操作系统的屏蔽下调用硬件的,第10章程序的实际构成感觉应该放在可执行文件之后or之前讲解比较顺畅,anyway。最后第12章算是一个程序例子,说明程序是怎么用的,怎么表示人类的思考方式的。
这本书封面说它自己是“计算机组成原理”图解趣味版,不过也有涉及到其他方面的知识。对个人来说,第2和第3章中对二进制的介绍又深入理解了一下,第5章的动静态链接,第7章的计算机从底层硬件到上层应用的关系,第8章编译的过程等还是挺有收获的。下一本是这个系列的最后一本了,网络是怎么连接的,这本应该看起来更快一些。
暂定阅读计划顺序:
2. 网络是怎样连接的(这三本书是同一个系列的,总是成套卖,还是比较经典的,想一次性看完)
3. 程序员的自我修养——链接、装载与库(很想把编译这块没有了解的弄明白)
4. 编译原理(这本书感觉会比上面这本难,所以先看上面这本打个底)
5. 程序员的数学(看完编译原理看这个放松一下)
6. 操作系统概念(算是复习一下)
7. 七周七语言:理解多种编程范型(接触过的编程语言也算是有一些了,可能现在看这本书会比较有感觉了)
8. 多核编程入门(之前一直不太了解多核、并行、分布式这堆东西,想看看了解下)