大学学习C语言时,书上面有一句话,程序就是数据和算法。面对着整型、字符串、浮点数等数据,还有冒泡、选择、堆等算法,好像程序确实如此。后来入门程序开发工作,变成了面对各种开发库的使用,UI的实现,业务的沟通和理解,好像一个程序不仅仅是数据和算法那么简单。
《程序员的自我修养》这本书,引导我从程序的最终运行方式(计算机的CPU不断读取物理内存中的指令和数据)角度,重新认识和理解开发的最终程序,再一次回归到程序就是数据和算法的论断。记得一句哲人说,认识有3个阶段。第一阶段是看山是山,看水是水;第二阶段是看山不是山,看水不是水;第三阶段,看山还是山,看水还是水。感谢这本书的作者,让我重新回到对程序的认识上。
1 CPU执行时只有指令和数据
无论什么样的程序,最终运行时,就是CPU在不断的工作。它工作的方式就是不断地从内存中读取指令和数据进行运算。这些指令和数据,都是以二进制的0和1进行存储。使用工具分析程序打包后的可执行文件时,能看到各种.text.data.rel.bss.got等不同的段分类,在调试程序时,能够看到程序运行的每一步,所对应的具体代码。这些都是开发工具对二进制指令和数据内容的相应翻译,将用于机器理解的二进制的内容,翻译为对应的高级(人类)语言。
从程序最终运行的角度去看,优化程序的方式可以有:减少执行的步骤,比如使用更优的算法、减少上下文的切换、合并指令等;加快读取的时间,比如使用缓存。当然还有一种釜底抽薪的解决方式,更换为更好的硬件产品,😄。
2 虚拟内存和物理内存
启动一个程序时,操作系统会开辟一个进程,建立虚拟内存到物理内存的映射,加载程序的运行库,最后跳转到程序的执行入口。CPU执行时,都是从物理内存中读取内容。通过计算机中的内存计算单元(MMU),可以将物理内存标记的地址,转换为程序进程的虚拟地址,进而装载相对应的可执行文件。这个过程就像将可执行文件的内容通过镜面(虚拟内存)映像到物理内存中一样,所以可执行文件也被称为映像文件。
虚拟内存是真实存在的吗?从名称就可以看出,它是不存在的。虚拟内存只是建立了一种读取可执行文件的方式,是用于物理内存读取可执行文件内容的中转站。就像在计算器中输入2X4,看到得到的结果是8。这是计算器通过内置的乘法公式计算出来的,我们能说乘法公式是在计算器中真实存在的吗?显然不能。虚拟内存就相当于计算器中的乘法公式,用来对可执行文件的内容进行转换。这种转换后的结果就像建立了一个专属于程序进程的内存,我们称它为虚拟内存。
3 目标文件和可执行文件
目标文件是编译器将源代码文件翻译成计算机可以理解的二进制文件,可执行文件是链接器将目标文件(单个或多个)和依赖库(一般都有)一起链接后,形成的计算机可以运行的二进制文件。以C语言为例,初始代码都在.c的文件中,通过编译,获得.o的目标文件。将目标文件和C运行库进行链接,获得最终的可执行文件.out。
本质上,目标文件和可执行文件都是文件,而且都是二进制文件。使用工具对两者进行分析,会看到都包含.bss.data.text.rodata等内容。这是两者相同的地方。相对于目标文件,为什么可执行文件就可以执行呢?寻找这个问题答案的过程,就是理解链接器作用的过程。简单来说,就是链接器将不同的目标文件的相同段进行合并,按照内存分页和地址对齐的原则对不同段重新分配虚拟空间,然后将指令涉及的符号按照虚拟空间的地址重新定位,最后指定程序的入口函数。这就形成了最终的可执行文件。
4 运行库
现在编程时,使用的基本都是高级语言。使用高级语言编写源代码,不仅仅在于它更容易使人理解,而且在于高级语言大都提供了便捷的运行库,让开发者可以不用太关心底层实现,专注于上层功能的实现。比如使用C语言开发时,使用scanf/printf函数就可以实现计算机的IO调用,使用malloc/free就可以实现内存的申请和释放,使用fopen/fwrite就可以实现文件的读写等等。这些都是因为C语言在标准库(stdlib)中进行了实现,开发时只要引用就可以了。这些高级语言编写的程序运行时,所有相关库的集合,就是运行库。
运行库分为两种存在静态和动态两种存在形式。使用静态运行库,在链接器生成可执行文件时,会将相关的静态运行库一起拷贝到执行文件中。使用动态运行库,会将可执行文件和相关运行库的链接过程,推迟到可执行文件启动时完成,这是一种牺牲部分时间,获得更大磁盘空间的选择。
总结
计算机的技术更新日新月异。像各种出现不久的开发语言go\ruby\flutter等,不同语言下出现的SDK更是数不胜数。甚至以为相对稳定的操作系统,就在近5年内,出现了鸿蒙和VisionOS。表面上,这些变化应接不暇,就像进入了一个卖各种美食的食宝街,转来转去,不知选择哪一家下口。面对众多的商家,心中若有想要都通吃一遍的想法,甚至会心生一丝心有余而力不足的恐惧。
作为程序开发者,抽丝剥茧,或者快刀斩乱麻,回到程序运行的最终形态去看,就是编译、链接和装载。回到上面的计算器的例子,表面上有珠算、速算、乘法口诀等等各种便捷计算方式,可底层终归是围绕加减乘除的数学规则。
计算机的大厦,就是由不同层次的积木累积而成。底层的硬件,像CPU\内存\主板\显示器\键盘等。对不同硬件的使用进行封装,就形成了操作系统。操作系统提供封装完善的API,方便不同语言的运行库使用。开发者使用运行库,顺利开发出新程序,在对应的操作系统运行。不同层次的内容不断更新,可是形成的这个整体框架,确是稳定不变的。
多彩纷繁的语言如同漫天繁星,与它们的距离遥远的仿佛深不可测。只专注于一个星球——地球的了解,却让我们获得了万有引力和相对论的真理。读这本《程序员的自我修养》的过程,就相当于去探索程序运行的本质。这也是我认为之所以命名为修养
的原因。