计算机底层的秘密读书笔记

计算机底层的秘密读书笔记

从编程语言到可执行程序


前言

在学习编程语言的过程中总会对应用层软件如何运行于真正的硬件系统中感到疑惑,遂查找资料找到了一本生动形象的技术书籍——计算机底层的秘密,开此篇内容用于跟随大神脚步学习知识,自用笔记;
系列笔记旨在自用总结计算机底层方面知识,提高个人能力水平,加深对编程应用的理解,如有看客还请指正笔记中错误及不足,另原书购买链接:https://item.jd.com/13944872.html
主要介绍软件在由高级编程语言编写完毕后,如何让只了解0 1的计算机运行起来;
高并发高性能服务器如何工作;
现代计算机的两种结构:

  1. 冯诺依曼结构:也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同;该结构指出计算机有五大部件组成:运算器(CU control unit)、控制器(ALU Arithmetic/Logic Unit)、存储器(主存储器、辅助存储器 MU memory Unit)、输入设备(Input device)、输出设备(Output device);前两项组成CPU、前三项(主存储器)组成主机、辅助存储器+后两项组成外设;![五大部件结构]

  2. 哈佛结构:哈佛结构是一种将程序指令存储和数据存储分开的存储器结构,如下图所示。中央处理器首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,并进行下一步的操作(通常是执行)。程序指令存储和数据存储分开,可以使指令和数据有不同的数据宽度。在这里插入图片描述


一、从编程语言到可执行文件

1. 汇编语言

CPU是基于简单的开关组合发展而来,这样说明CPU所能认识的字只有0和1的组合,在这一时代程序员只能站在CPU的角度编程,一大串的01二进制字符;
随着长时间的工作,程序员总结了CPU执行的指令,并进行映射抽象,出现了汇编语言,即把机器指令映射到人类理解的简单的单词:sub mov & call…

2. 底层的细节和高层的抽象

2.1 高级语言的诞生

汇编言中出现了对人类友好的单词,但汇编语言仍然属于低级语言,它只是对机器语言做了映射的工作,而没有进行抽象工作,即低级语言仍然需要人类将一个任务拆解到CPU能够理解的最小步骤去交给CPU执行,如计算一个变量的加法结果,需要通知CPU于哪个地址储存了哪些值,再于哪里完成加法操作返回值;
基于以上痛点,程序员开发更高级的抽象,为了能够让计算机理解更接近人的思维指令,使人能理解的抽象表达自动转化为CPU理解的具体实现的机器指令。
对汇编语言的抽象总结发现:

  1. 条件转移
  2. 循环
  3. 函数
  4. balabala平铺直叙或对以上三种指令的进一步嵌套;
    于是将以上抽象的单词组成句子,出现了语法,语法使得单词有了一定的顺序,按照语法的规定,程序员可以写出人类能够识别语义的编程语言,完成了对低级语言的抽象。
    在发明了语法之后,如何让计算机理解语法?在计算机理解的机器指令与高级语言之间充当翻译角色——编译器

2.2 编译器的工作

计算机在处理编程语言时,按照语法定义把代码用树的结构组织起来,该形式称为语法树,对应的叶子节点时简单的可以直接翻译成机器指令的部分,进一步的向上翻译,该父节点又是可以翻译为机器指令的一个父父节点的子节点,层层递进,将全部代码翻译为机器指令,以上工作即是编译器的工作内容。
早期受不同CPU接受的机器指令不同,又无统一的语言标准,导致编程语言没有同意,当通用定义出一套标准的高级语言,又约定所有程序员按照该语言语法进行编程,就出现了目前市场上的C/C++、Java、Python等高级语言。
为了让不同硬件平台都可以运行一套标准的高级语言代码,工程师们进一步发明了解释器,其原理如下:CPU执行的都是机器指令,开发一种程序仿真COU执行机器指令的过程,然后自己定义一套标准指令,这样就分隔开了CPU真正执行的机器指令和编译器编译成的可执行文件;
茅塞顿开:在解释器以上,我们使用通用的国际语言,该国际语言供全体人类程序员使用,C/C++等高级语言,并最终将高级语言编译成标准的机器指令(国际语言之机器指令),在解释器以下,由各个翻译官(解释器)将标准机器指令依照不同CPU使用的内部语言,解释为各个CPU所能理解的机器指令。如原书例图:在无解释器的情况下,各种CPU以来其特有的编译器编译为仅有该型CPU可以识别的可执行程序不同类型的CPU执行其特有的可执行程序 解释器解释执行代码过程解释器解释标准指令,令各类型执行器执行代码过程
由于解释器本质上是仿真CPU执行指令的过程的程序,因此解释器的另一个名字又叫做虚拟机
以上也解释了高级语言的抽象表达能力很强,但牺牲了对底层的控制能力,而操作系统部分内容需要使用汇编语言编写的原因也是其汇编语言具有类似标准指令的控制能力,能够严格的控制底层执行。

小总结

如上图可清晰分辨出编译器和解释器所做的工作内容存在不同:

区别:解释器是解释执行的源代码,编译器是将源代码编译成目标代码。他们最大的区别是程序运行时需要解释器边解释边执行,而编译器则在运行时是完全不需要的。
解释器优缺点:优点是比较容易让用户实现自己跨平台的代码,比如Java,php等,同一套代码可以在几乎所有的操作系统上执行,而无需根据操作系统做修改。缺点是由于程序不需要编译,程序在运行时才翻译成机器语言,每执行 一次都要翻译一次,造成执行效率比较低。这也是为什么python编译的时候很慢,C程序编译起来很快的原因。

编译器优缺点:程序在执行之前需要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差。编译器的目的就是生成目标代码再由连接器生成可执行的机器码,这样的话需要根据不同的操作系统编制代码,虽然有像Qt这样的源代码级跨平台的编程工具库,但在不同的平台上仍然需要重新编译连接成可执行文件,但其执行效率要远远高于解释运行的程序。
解释器与编译器总结原文:
原文链接:https://blog.csdn.net/weixin_45905650/article/details/107916093

2.3 编译器的工作原理

编译器的工作内容是在程序执行之前,对程序文件进行编译,编译为机器指令的文件,此后运行程序即是运行该文件,也就是编译结果。即:编译器的编译过程
粗略的可以将编译器的编译过程看作上图,但实际上编译器要做如下步骤工作:

  1. 提取出每一个符号,符号(token)的含义为高级语言代码中独立最小的单词所包含的信息的载体,如例子:int 该关键字为C/C++中可拆出的最小单词,则该单词包含的信息有 ①这是一个关键字/词;②这是一个关键词int。 编译器的第一步工作即将源代码遍历一遍,找出所有的token。
    该过程我们称之为词法分析——Lexical Analysis,即不需要读懂一句英文的长难句,但需要认识长难句中的所有单词。
  2. 得到token后,尝试理解token要表达的内容;在编译器完成所有的token提取后,编译器则需要按照语法来处理token的组合(因为我们规定所有的编程语言都按照语法来编写),如以下过程,当编译器识别到一个关键字为 if 的token时,那么编译器将检查接下来的token必须为(,否则将报语法错误,进一步的token为条件表达式、)、{循环体…}。以上过程被称之为解析 Parsing,该过程为编译器检查程序员语法错误的过程 ,严格执行其标准,编译器完成语法分析,语法分析结束后我们得到了编译器解析城的语法树。
  3. 检查语法树是否合理;在确定语法树后,个人理解编译器将尝试翻译该语法树是不是正确的,如整数与字符串相加为错误,等等检查操作,此步骤通过后则说明程序合理,可以进行代码生成,该过程称为语义分析
  4. 通过语义分析后,编译器开始根据语法树生成中间代码,再对中间代码进行进一步优化。
  5. 进行代码生成工作,将中间代码转化为汇编指令… 根据不同CPU的编译器不同,得到的汇编指令也就不同,最后编译器再将汇编指令转换为机器指令。
    整个编译过程大致分为如下流程在这里插入图片描述
    整个过程实际上是编译器完成.c文件到.o文件的编译制作,即每个源文件经过编译后都会得到一个目标文件,但一个程序可能有多个源文件组成,当形成多个目标文件后,为了可执行程序能够正常运行,则需要一个介质将全部的目标文件合并成可执行程序,进一步的出现了链接器

2.3 链接器

链接器的由来,我们已知为了目标文件能够组合成可执行程序,CPU或者说程序员需要借助链接器充当流程图中的箭头来完成完整的可执行程序。
与编译器相同,链接器也是一个普通程序,其职责时把编译器产生的一众目标文件打包成最终的可执行文件。
链接器的具体职责:
① 确保文件之间的依赖关系
当编程时,分文件编写了一个其他模块会使用的函数或功能,该函数或功能会被其他模块使用,则说明两个模块之间有依赖关系,该过程称为符号决议
② 汇总合并全部的目标文件
一个完整的可执行程序由全部的目标文件组成,对所有目标文件的汇总合并生成可执行文件的工作由链接器完成;
③ 对目标文件中的一些信息重定位
编译器在对源代码进行编译时不能确定源代码所引用的函数(其他模块定义的函数)具体放在哪个内存上,此时编译器给该函数的内存地址用某个代号来代替,当链接器汇总合并生产可执行文件时,此时该代号会被重定义为真正的内存地址。
链接器的工作:

2.3.1符号决议

符号指的是代码中的变量名,在程序中,变量包含局部变量和全局变量,由于局部变量属于模块私有的不会被外部引用,故链接器不关心局部变量;链接器需要得知代码中全局变量具有以下信息:
源文件中具有x个符号可供其他模块使用;定义全局变量 供外部模块使用
源文件中具有y个有其他模块定义的符号;引用其他模块的变量
以上两个信息是有编译器告知链接器的,如下:
编译器所生成的目标文件包括:

  1. 指令部分:该部分机器指令来自源文件中定义的函数,即代码区;
  2. 数据部分:该部分机器指令为源文件中的全局变量(局部变量为程序运行后的栈区管理);
    当编译器在编译过程遇到文件包括由外部定义的全局变量或函数时,只需要找到相应的声明即可:
    int funA(int a ,int b);
    不会在编译过程中出现问题报错,编译器会在遇到外部定义的引用内容后,记录下该文件引用了哪些外部符号以及该文件可以提供哪些符号让外部所引用,并将以上两部分内容存放在一个表内,即符号表,而链接器需要完成寻找引用变量和函数的定义工作;
  3. 符号表:被包含于目标文件,提供信息由哪些外部引用的符号,即可以提供哪些符号给外部;

2.3.2静态库、动态库及可执行文件

静态库

在处理大型工程时,有很多文件是用来完成基础功能的,当业务团队开发时需要取出部分基础功能的函数来完成开发任务,会使两部分团队的代码放在一起管理,出于此目的,将基础架构团队的代码单独编译打包,对外提供一个包含所有实现函数的头文件供引用,该文件即为静态库(Static Library),在Windows操作系统中文件后缀为.lib,Linux系统中文件后缀为.a。
基于以上我们可以把一部分源文件提前单独编译成静态库。在生成可执行文件过程中,由于静态库已经被编译,应用开发团队只需要编译自己的软件,并在链接过程中把需要的静态库复制到可执行文件中,该过程称为静态链接,该方式加快项目软件编译速度。
静态库文件在项目代码中编译的过程
最终得到的可执行文件是真正计算机运行的文件,其结构域目标文件相似,分为代码区,数据区,符号表,只不过每个区均是有上一个层级的目标文件的对应区域组成,在可执行文件中有一个特殊的符号**_start**,CPU从该地址执行机器指令,经过一系列准备工作运行到main函数开始运行。
静态库的工作方式决定了在静态链接时会将库直接复制到可执行文件中,随着项目的发展,静态库中编写的基础框架代码越来越大或调用的库文件被维护的越来越大,每个程序都要调用的标准库如果很大,如C标准库,那么在生成的所有可执行文件中都会包含一份相同的代码和数据,这样造成的硬件资源浪费是巨大的,并且随着静态库中的代码迭代,每次更改都需要重新编译一次全部过程,耗费的时间以及项目管理上会出现的问题都是较大的代价;
为了解决上述静态库的问题,开发人员会选择使用动态库

动态库

Dynamic Library也叫做共享库 Shared Library、动态链接库等,Windows系统下的DLL文件,Linux系统下lib为前缀,.so为后缀的文件,在Linux系统下还可以将几个源文件打包成一个动态库,生成命令如下:
$ gcc -shared -fPIC -o libfoo.so a.c b.c
该命令将a.c和b.c两个源文件打包成动态库foo,最后生成的动态库名字为 libfoo.so。
动态库与静态库的区别:本质上,动态库与静态库相同,由代码区、数据区等,区别在于在使用时,静态库的代码区和数据区会被直接打包到可执行文件中,而动态库的使用是各个可执行文件对动态库的引用,即可执行文件中包含一小部分的信息,该信息可以零操作系统找到可执行文件引用了动态库中的哪部分内容。基于该特点,动态库可大大减小可执行文件的大小,使编译过程更快,占用储存更小。
在这里插入图片描述在这里插入图片描述
由上图可知,引用信息也构成了可执行文件的一部分,引用信息在进行动态链接时使用:
① 在程序加载时进行动态连接,可执行文件从磁盘搬运到内存的过程,称为此时的程序加载,造作系统中特定的一个程序负责程序的加载,该程序称为加载器 program loader。加载器在加载可执行文件后可以检测该可执行文件是否依赖动态库,如是,加载器启动另一个程序——动态链接器,完成可执行文件到动态库的连接工作,主要内容为:引用的动态库是否存在、地址即所引用符号的内存位置,完成后,该可执行文件才编程可运行的应用程序。
将依赖动态库libfoo.so的源文件mian.c生成(编译并连接)可执行文件pro,使用以下命令:
$ gcc -o pro mian.c /path/to/libfoo.so
② 在程序运行时进行动态链接,COU执行时,程序运行,该操作需要程序员完成可执行文件对动态库的依赖,需要在编写程序时制定特定的API来根据需求动态加载指定的动态库,如Linux下dlopen、dlsym、dlclose函数完成运行时链接动态库动作。

举例,C/C++中printf函数,实际是在C标准库中以动态库的形式实现,libc.so 该文件可以在Linux系统下查看到被缩写函数所依赖。

小结动态库优劣势

优势:

  1. 由于动态库是被可执行文件所引用的,因此在更改动态库代码时,只需重新编译动态库即可,不需要重复编译依赖动态库的程序。再次运行时更新动态库即可;
  2. 用于扩展主程序能力,通过插件的方式,升级动态库来完成某部分函数功能,如一次开发APP时预留了某个功能通过调用动态库来实现,再次对动态库进行升级,使得该子功能具备新的某种能力;
  3. 多语言混合编程,高性能要求部分使用C/C++编写动态库,主程序运行通过使用python调用该动态库。
    劣势:
  4. 与静态库相比,动态库在加载时或运行时才被链接,程序运行的性能上要略弱于一起搬运到内存的静态库。
  5. 在动态库文件中,代码是地址无关的,因为动态库在内存中只有一份,但又可以被依赖此库的进程共享,在被不同进程共享动态库时,进程所在的地址空间是不同的,因此代码不能依赖任何绝对地址(在进程共享是无法保证)。

至此 链接器生成可执行文件的内容已经结束

2.3.3 链接器的重定位功能

问题起因:变量和函数都是由内存地址的,在汇编代码中,指令不会操作任何的变量或调用函数,而是操作内存地址,汇编代码告诉CPU在哪个位置做什么样的操作执行,因此,该问题对链接器提出了在生成可执行指令时必须确定该函数在程序运行时的地址的要求。
整体生成可执行文件的步骤:
① 编译器编译源文件生成目标文件,此时无法确定函数或变量的存放地址,将其简单写为0等默认值类的地址代码,并对不能确定最终运行地址的函数和变量进行记录,如是函数(与指令相关的)放在 .relo.txt,变量(与数据相关)放在.relo.data文件中,以上步骤可以保证在链接器进入符号决议阶段是能够确定是否由链接错误.;
② 当链接器进行符号决议后无错误后,将所有目标文件中的同类型的区合并在一起,所有机器指令和全局变量在程序运行时的内存地址就可以被确定了
③ 链接器逐个扫描各个目标文件中的relo.txt段,当找到程序需要调用的函数时(以函数名作为符号查找),会发现其所在的机器指令需要修正,并且其相对与的代码段的起始地址需要偏移xx字节,具备以上信息后,链接器就可以在可执行文件中准确的找到需要call调用的指令后面的地址时多少了。 该修正符号内存地址的过程叫做重定位

链接器能够在运行之前就知道变量运行时内存地址的原因:

虚拟内存与程序内存布局
程序运行起来后时进程,进程在内存中的样子如图所示:
虚拟内存图
其中代码区在运行起来后都是从0x400000开始的,当从该地址获取到的指令属于程序A,说明CPU此时执行A程序,若是B则此时在执行B程序,虚拟内存技术让所有的进程都认为自己独占了全部内存,在32位操作系统下任何进程都认为自己占用了4GB的内存,而不在意真实的物理内存有多大,因此,每个程序都有一个标准的内存布局,如上图所示,而在程序员使用编程语言时,也可以基于该标准的内存布局进行程序编写,链接器也是根据该布局确定符号运行时的内存地址的。
如代码区在64位系统下从0x400000开始,栈区永远抢占最高地址,一头一尾,链接器按照链表也可找到变量对应的内存地址,尽管该地址时虚拟的 假的,但只要符合标准,并有专门的映射关系,CPU就可以找到真实的物理地址,每个虚拟内存到物理的映射关系存储于页,以页为单位来维护映射关系,记录该映射关系的叫做页表
实际运行时,程序的虚拟内存和物理内存
由上图可知:
1. 每个进程的虚拟内存都是标准的,大小一致,各区域排放顺序移植,只是在各个进程之间,这些区域的大小可能不同,看内存的生长情况;
2. 真实的物理内存大小与虚拟内存大小无关,物理内存中也无分区的说法(不考虑操作系统);
3. 每个进程有自己的页表,相同的虚拟内存在查询页表后的到不同的物理内存,这是CPU从同样的虚拟内存地址可以获取到不同内容的根本原因,虚拟内存技术。

总结

此篇章介绍由高级编程语言到可执行文件的过程,以结果为产物的介绍了源代码文件,目标文件,可执行文件,分文件编写中的静态库、动态库,以工作工具介绍了编译器、链接器的工作内容和工作产物,介绍了虚拟内存于真实运行的物理内存环境,以此粗略的引入计算机底层的原理。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值