【LinuxfromScratch】是什么两年多来,我的工作一直专注于x86Linux系统,从驱动到中间层,再到应用层开发。随着内容的不断扩大,感觉很多基本的东西都快忘记了,比如下表(《深入理解 LINUX 内核》第47页):这个表描述了Linux系统中的几个段描述符信息。对于数据段和代码段,如果仔细阅读相关书籍,就会知道这些描述符是什么意思,但是:为什么这些段的Baseaddresses都是0x00000000?为什么限制是0xfffff?为什么它们的类型和优先级DPL不同?不同的?如果不了解x86平台的一些基础知识,真的很难读完这本书!更何况,随着Linux内核代码体积的不断扩大,最新的5.13版本压缩文件大小已经超过百兆:这么庞大的怪物,怎么可能真的学好Linux?!即使是从Linux0.11版本开始,很多代码都显得很吃力!周末在整理一些满是灰尘的书看书的时候,发现了几本以前看过的好书:王爽的《汇编语言》,李忠的《从实模式到保护模式》,马朝晖翻译的《汇编语言程序设计》等。所有非常非常旧的书。又看了一遍,真的觉得内容写的真好!一些概念、原理和设计思想的描述清晰透彻。Linux系统中很多与段、内存、寄存器相关的设计都可以在这些书中找到基本的支持。于是乎,我就有了一个想法:能不能把这些书里面和Linux系统相关的内容重新阅读整理一下,但绝不是简单的知识传授。思前想后,我有以下想法:第一,确定最终目标:学习Linux操作系统;这些书都是用汇编语言写的,底层知识比较基础。我们会淡化汇编语言部分,专注于Linux操作系统相关的原理部分;我们不会严格按照本书的内容和顺序输出文章,而是将几本书的相关部分放在一起学习、讨论;部分内容可以与Linux2.6版本中的相关部分进行对比分析,以便日后学习Linux内核部分时,找到底层支持;最后希望自己能坚持这个系列,也算是自己的整理吧。一句话:基础知识!作为第一章的第一章,本文将描述下图的执行步骤:现在开始吧!古老的Intel8086处理器8086是Intel第一款16位处理器。1978年出生的,应该比你们都大。它在英特尔公司的所有处理器中占有非常重要的地位,是整个英特尔32位架构处理器(IA-32)的鼻祖。那么,问题来了,什么是16位处理器?有人把处理器的位数和地址总线的位数搞混了!我们知道,CPU在访问内存时,是通过地址总线传输物理数据的。地址。8086CPU有20位地址线,可以传输20位地址。每条地址线代表一位,所以20位能代表的最大值是2的20次方。也就是说:最大能位于1M地址的内存称为CPU的寻址能力。但是,8086处理器是16位的,因为:运算符一次最多可以处理16位数据;寄存器最大宽度为16位;寄存器和运算符之间的路径是16位;也就是说:在8086处理器里面,一次可以处理、传输和暂存的最大长度是16位,所以我们说它是一个16位结构的CPU。什么是主内存?计算机的本质是数据的存储和处理,那么参与计算的数据从何而来呢?那就是称为内存(Storage或Memory)的物理设备。从广义上讲,任何可以存储数据的设备都可以称为存储器,例如:硬盘、U盘等。但是在计算机内部,有一种专门与CPU相连的存储器,用于存储程序和程序。正在执行的数据,一般称为内存或主存,简称内存或主存。内存是以字节为单位组织的,单次访问的最小单位是1字节,是最基本的存储单位。每个存储单元,也就是一个字节,对应一个地址,如下图所示:CPU通过地址总线决定:访问内存中的哪个存储单元中的数据。第一个字节的地址为0000H,第二个字节的地址为0001H,依此类推。在图中的内存中,最大存储单元的地址是FFFFH,十进制为65535,所以这块内存的容量为65536字节,即64KB。这里可以考虑一个原子操作问题。在Linux内核代码中,很多地方都用到了原子操作,比如互斥锁的实现代码。为什么原子操作需要限制变量类型为int?这涉及到对内存的读写操作。虽然内存的最小单位是字节,但经过精心设计和安排,可以按字节、字、双字来访问不同位数的CPU。换句话说,一个16位处理器可以处理16位二进制数,而一个32位处理器只需一次访问就可以处理32位二进制数。什么是寄存器?在CPU内部,有一些是代表0或1的电信号。这些二进制数的一组电信号出现在处理器的内部电路上。它们是高电平和低电平的组合,代表二进制数。每一点。在处理器内部,必须使用称为寄存器的电路锁存此数据。因此,寄存器本质上是一种内存。只是它们在处理器内部,CPU访问寄存器比访问内存更快。处理器总是很忙。在其运行过程中,所有数据只能在寄存器中暂时存储一小段时间,然后再发送到其他地方,这就是它被称为“寄存器”的原因。8086中的寄存??器都是16位的,可以存放2个字节,也就是1个字。高字节在前(bit8~bit15),低字节在后(bit0~bit7)。8086中有以下寄存器:刚才说了,这些寄存器都是16位的。由于需要兼容老式处理器,AX、BX、CX、DX这四个寄存器也可以作为两个8位寄存器使用。例如:AX代表一个16位寄存器,AH和AL分别代表一个8位寄存器。movAX,5D表示发送005D到AX寄存器(16位)movAL,5D表示发送5D到AL寄存器(8位)三总线当我们启动一个应用程序时,这个程序的代码和数据被加载到物理内存中。CPU无论是读取指令还是操作数据,都需要与内存进行交互:确定存储单元的地址(地址信息);设备选择、读取或写入命令(控制信息);读取或写入数据(数据信息);在计算机中,有一条专门连接CPU和其他芯片的数据总线,称为总线。从逻辑上分类,它包括以下三类总线:地址总线:用于确定存储单元的地址;控制总线:CPU控制外部周期;数据总线:在CPU和内存或其他设备之间传输数据;8086有20条地址线,称为地址总线的宽度,可以寻址2的20次方内存单元。同理,8086数据总线的宽度为16,即一次可以传输16位数据。控制总线决定了CPU可以进行多少种外部控制,决定了CPU控制外部设备的能力。CPU如何寻址内存?在Linux2.6内核代码中,编译器生成的地址称为虚拟地址(也称:逻辑地址)。经过段转换后,这个逻辑地址就变成了线性地址,线性地址再被分页。转换,得到最终物理内存上的物理地址。还记得文章开头的段描述符表吗?代码段和数据段描述符的起始地址都是0x00000000,也就是说:虚拟地址和转换后的线性地址的值是相等的(后面会明白为什么会这样)。我们再来看8086中更简单的地址转换,前面说过,内存是线性存储设备,CPU是靠地址来定位各个存储单元的。对于8086CPU,它有20条地址线,可以传输20位地址,达到1MB的寻址能力。而8086是16位结构,一次性处理、传输、暂存的内部地址只有16位。从内部结构来看,如果单纯从内部向地址总线发送地址,只能发送一个16位的地址。在这种情况下,寻址能力只有64KB。那么如何才能充分利用这20条地址线呢?8086CPU采用内部合成两个16位地址的方式,组成一个20位的物理地址,如下图:第一个16位地址称为段地址,第二个16位地址称为偏移地址。地址加法器用下面的公式“合成”一个20位的物理地址:物理地址=段地址×16+空间偏移地址。CPU在执行这些指令时,把CS寄存器当作段寄存器,把IP寄存器当作偏移量寄存器,然后计算CSx16+IP的值,得到指令的物理地址。从上面的描述可以看出,8086CPU似乎是使用这种地址合成方式,因为寄存器不能直接输出20位的物理地址。其实更本质的原因是:8086CPU只是想通过基地址+偏移量的方式寻址内存(这里的基地址就是将段地址左移4位)。也就是说,即使CPU具备了直接输出20位地址的能力,它仍然有可能采用基地址+偏移量的方式进行内存寻址。想一想:我们在Linux系统中编译库文件时,通常会在编译选项中加上-fPIC选项,表示编译后的动态库是地址无关的,加载到内存时需要重定位。基址+偏移寻址方式为重定位提供了底层支持。我们如何控制CPU?CPU其实是一个很纯粹很死板的东西。它唯一做的事情就是:从CS:IP这两个寄存器指定的内存单元中取出一条指令,然后执行这条指令:当然,需要事先定义一组指令集。在内存中的指令区,存放的所有指令都必须是合法的,否则CPU无法识别。每条指令都使用一些特定的数字(指令代码)来指示CPU执行特定的操作。CPU识别这些指令,当它看到这些指令代码时,CPU就知道指令代码后面有几个字节的操作数,需要进行什么样的操作。例如:指令代码F4H表示停止处理器,当CPU执行该指令时,它停止工作。(其实这里说CPU有点不准确,因为CPU是一个包含很多设备的整体,可能这里说CPU中的执行单元更准确。)还有一点可以提前说一下:everything内存中是数据,至于哪部分数据作为指令执行,哪部分数据作为指令操作的“变量”,这完全是操作系统的设计者计划好的。在8086处理器这一层,只要把CS:IP“指向”的内存区当做一条指令执行。从上面的描述可以看出,在CPU中,程序员可以用指令读写的设备只有寄存器,我们可以通过改变寄存器的内容来控制CPU。更直白的说:我们可以通过改变CS和IP寄存器的内容来控制CPU执行目标指令。作为一名合格的嵌入式开发人员,每个人都可能在单片机中配置过一些寄存器,以达到一些功能定义和端口复用的目的。其实这些操作都可以看作是我们对CPU的控制。如果CPU就像一个木偶,那么寄存器就是控制木偶的绳索。我们拿工控领域的CPU和PLC编程做个类比。我们得到一个新的PLC设备后,只有一个运行时(runtime),这个运行时执行的工作是:扫描所有的输入端口,并锁定在输入映像区;执行一个运算和控制逻辑,得到一系列的输出信号,锁存到输出图像区;输出图像区的信号刷新到输出端口;在全新的PLC中,第二步所需的运算和控制逻辑可能不存在。因此,PLC无法仅用一个运行时间来完成一项有意义的工作。为了使PLC达到特定的控制目标,我们还需要利用PLC厂家提供的上位机编程软件,开发出运行控制逻辑程序。编程语言一般是梯形图。这个程序下载到PLC后,就可以控制运行时间做一些有意义的工作。我们可以简单的认为梯形图是用来控制PLC的运行时间的。对于CPU来说,如果想让它执行某个内存单元的指令,只需要修改寄存器CS和IP即可。换句话说:只要程序的内存布局足够清晰,CPU就可以把玩在手心,它可以在任何地方执行任何代码。CPU执行指令流程了解了地址转换和内存寻址之后,CPU执行一条指令所需的最小单元仍然存在:指令缓冲区和控制电路。简单来说:指令缓冲区用于缓存从内存中读取的指令,控制电路用于协调各种设备对总线等资源的使用。如下图,一共有4条指令:以第一条指令为例,一共经过5步:把CS:IP的内容送到地址加法器,计算出20位的物理地址20000H;控制电路将20位地址发送到地址总线;将内存中20000H单元的指令B82301通过数据总线发送到指令缓冲区;指令偏移寄存器IP的值需要增加3,指向下一条等待要执行的偏移地址(因为指令码B8代表当前指令长度为3字节);执行指令缓冲区中的指令:将值0123H送入寄存器AX;以上是一条指令执行的最基本步骤,当然现代处理器的指令执行流程要比这里复杂得多。万丈高楼拔地而起!本文只介绍CPU执行一条指令所需要的最低知识点。本文转载自微信公众号“IOT物联网小镇”,可通过以下二维码关注。转载本文请联系物联小镇公众号。
