跟我一起玩《linux内核设计的艺术》第1章(一)——触电后的那些事

本文深入探讨了8086/8088 CPU的16位实模式和BIOS启动过程,解释了BIOS启动地址0xFFFF0的由来,以及16位实模式如何通过两套16位寄存器扩展到20位地址空间。同时,文章揭示了中断向量表的工作原理,指出0x00019中断服务程序在启动过程中的作用,以及BIOS设置与CMOS之间的关系。文章适合对计算机启动原理感兴趣的读者进一步了解。
摘要由CSDN通过智能技术生成


        先说,在度娘普照天下的时代,一些基础概念已经不需要再解释了,不然写到猴年马月。我只负责填坑。

1.1  16位实模式和BIOS启动原理

        整个内容倒是不多,无非是解释计算机启动先从BIOS开始,而BIOS的入口地址默认放在地址0xFFFF0中,而这些过程的操作属于实模式操作,因此玩的绝对地址。
        坑一:书上第一页的小贴士明明说“实模式”的特性是一个20位的存储器地址空间,第二页BIOS的启动原理又说“x86系列CPU分别在16位实模式……”,喂,实模式到底多少位?好吧我们来观察下BIOS的启动地址是0xFFFF0!!!,多少位还不清楚么?(我们都知道16进制中的一位对应二进制中的4位,而0xFFFF0共有五位,自然5×4=20位)

        我们说CPU要执行指令,就必须要寻址,而地址的组成恰恰就是CS:IP,书上解释得很清楚,CS和IP其实就是CPU中两个寄存器,CS存放代码段地址,IP存放具体某条指令在代码段中的偏移地址,于是CS:IP就是cpu要寻址时的完整格式。而所谓16位实模式中的16位,指的是小贴士中说的指令指针寄存器IP是16位吗?,那代码段CS位是20-16 = 4位了?,虽然偏移地址16位+代码段地址4位=20位地址空间这种说法不算错。但如果CS寄存器只有4位,换算成16进制就只有1位了,怎么可能赋值CS = 0xF000?实际情况应该是,CS和IP两个寄存器在实模式下都是16位模式,用两个16位拼出20位地址空间而已。原则上两个16位寄存器确实可以拼出32位地址空间,只是实模式用不到那么多而已——20位就是实模式内存寻址空间,实模式规定的换算公式决定了地址空间只有20位。(既然IP/EIP小贴士解释了16位的意思,为啥CS小贴士就不顺带解释下呢)

        来来来,演算下,我们知道16进制的1位对应二进制的4位,也就是对应2^4 =16种取值……废话,之所以叫16进制,每一位当然按16个16个的进位啊!现在16进制的0xFFFF0有5位,要换算成二进制如何表示呢?16^5 = (2^4)^5 = 2^20 = 2^10×2^10=2^20=1024×1024,反正5位16进制,或者说20位二进制表达的数,其代表的寻址空间就是1MB对吧?刚好1M,好记,好运算,这应该是实模式规定20位地址空间的最直接理由。

        坑二:0xFFFF0是怎么从CS=0xF000,IP=0xFFF0,然后就变成CS:IP=0xFFFF0的呢?相信有部分读者在此会稍显困惑。其实实模式地址计算公式很简单:CS:IP=CS×2^4 + IP。(注意乘以2^4相当于左移一个16进制位,这里可不是二进制哦^^)。我们知道BIOS程序肯定也是16位的,如果说CS是CPU硬件逻辑在加电时自动将寄存器CS置为0xF000,那么具体左移位操作很可能是通过总线置1的方式,将0xF000变成0xF0000,然后通过16进制的“或”操作,CS|IP=0xFFFF0。至于总线又是怎么玩的那属于硬件范畴了(人家书上都说了“这是个纯硬件完成的动作!”),而且具体实现方式可能还不唯一。更坑的是,有些设备总线初始化时默认值为:CS=0xFFFF,IP=0x0000,照样能拼出0xFFFF0,你咬它去?

。。。。。。。。。。。。。。。。。。可爱的分割线。。。。。。。。。。。。。。。。。。。

在坑一的位置,关于两个16位拼出20位的解释,是我之前针对书本现有的线索做出的猜测。但有天不小心往祖坟上刨这20位的来历,刨出了这么一篇文章:

8086/8088的寻址问题
===============
  8088和80286都是16位CPU,Intel当初为什么会警告IBM和盖茨呢?到底发生了什么?
  要了解发生了什么,我们要看看处理器的内部,会看到巨大的差异。首先,你找一片8088CPU,把包装磨掉,磨到CPU硅片,放到显微镜下,你会看到8086/88的内部结构,它根本不是一个新的设计,而是两个并联运行的8085(8位)微处理器再多那么一点点。
  每个8085有它自己的8位数据和16位寻址能力。结合2个8位数据寄存器假装16位寄存器很容易。事实上这没有任何新东西,RCA COSMAC微处理器就使用16个8位寄存器,可作为内部的8位或16位寄存器使用,你可以有多达16个8位寄存器或8个16位寄存器或两者的任何组合。现在,一个中国的普通IC厂都可以轻易设计的出来。
  可能由于受当时生产工艺所限,8088只能有40个脚,intel的设计“精英”左思右想,确定了20条地址线(1M的寻址空间),而且16条数据线还要和20条地址线中的16条复用(分时复用,即一会是地址线,一会是数据线,对此要想了解,可看8088芯片手册的时序部分,也可看8052单片机书籍,它的地址线和数据线也是复用的)。
  
  到了问题的实质了,8088内的两个8085各有一套16位寻址寄存器,如何让他们寻址20位的1M地址呢?其实把他们并在一起形成32位寻址很简单,如果是那样后来的很多麻烦可能就都没有了(如A20门),但当时那些“精英”可能认为32位寻址(4G地址空间)那是扯淡,估计地球消失了也用不到那么多的内存吧?再说了老板逼的又紧,于是他们采用了在一个硬件上使用两个8085非常好实现的方法--分段:
  他们把1024K地址空间分成16字节的段,共64K个段,用一个8085的16位寻址寄存器作地址偏移寄存器(故段的长度是64K),而另一个8085的16位寻址寄存器作16字节段的段地址寄存器,注意,他保存的不是16字节段的地址,而是16字节段的序号(0,1,...65535)。
      这样做的好处是:只要在两8085CPU之间加一个移位器和一个20位的加法器,就可以完成20位的地址寻址--一个8085的地址寄存器(段地址--就是16字节段的序号)左移4位(*16 = 16字节小段的首地址),加上另一个8085的地址寄存器就可以啦,哈哈!可以向老板交差了,制作成本低,设计速度快,有钱不抢是孙子!至于以后,。。。。
————————————————

 

上面精彩的文字,跟我阅读时猜测关于双16位拼出20位的说法不谋而合。当然,有很多童鞋可能没完全读懂上面的文字,这个不怪你,因为我对作者的描述是持怀疑态度。按照上面的说法,应该有两套16位寻址寄存器,怎么分段?加粗的部分看着头大不?

上面提到的1024K地址空间,其实就是20位总线能构成的地址空间大小,即2^20=1024K。里面提到的精英当年用派克笔在草稿本上应该是这么写的:1024K = 2^10×2^10 = 2^16×2^4 = 2^10×2^6×2^4 =  2^10×64×16 = 64K × 16…好了,这分配方案,到底是64K个段,每个段16B;还是16个段,每个段64KB?考虑到一个bootsect.s源码就有512字节,这得消耗多少个段?是后者吗?即16个段,每个段64KB,这样段偏移才能覆盖基本的代码区?应该是这样的!从计算公式来看:CS×16+IP,而CS是代码段寄存器,既然它总是占据16进制的最高位(16进制的第五位,二进制的高4位,它只能在中范围内做16以内的偏移),可见代码段总共是16个,每个代码段64KB。这个是猜测的前提。

好了,之前CPU只有纯16位寻址寄存器,因此能寻址的范围只有2^16也就是64K,因此只能寻址64K个字节,也就是64KB。现在阔了,有两个16位寻址寄存器了,可以分段了!于是其中一个16位寻址寄存器就可以只用自己的4位来标识16个“段”了(也就是标识最高位:段位),如果代码段的缩写是CS,那么相当于就是标识了16CS(为描述16个段原谅我瞎编符号)。而每一个段有64KB,由另一个16位寻址寄存器来负责标识64KB的段偏移。

我这个猜测对于CS=0xF000,IP=0xFFF0的初始值好像很有说服力,因为BIOS也是利用段偏移和段内代码偏移来提取的。但是对于CS=0xFFFF,IP=0x0000的初始值,会不会是第一种情况呢?

其实啊,64KB只是每一个段的最大值,并不见得是唯一值,事实上,段的划分,是根据具体情况而定的o(* ̄︶ ̄*)o。

        坑三:好了,折腾半天无非是把CPU里两个寄存器初始化然后拼出一个BIOS入口地址0xFFFF0,然后我们到RAM里去找到绝对地址0xFFFF0,并执行里面的BIOS代码……等等,凭什么你拼出0xFFFF0,老子RAM里0xFFFF0地方就必须有BIOS代码??

        如果看到这里你有这样的疑问,那么恭喜,你具备深入探究的特质。答案是,通过映射BIOS的ROM到RAM,而书的1.12就会解释得很清楚。

        坑四:小贴士中用0x00100 = 256推出0x00400 = 1024,再一次提醒了我们不但要熟练掌握二进制的运算,还要熟悉16进制的常见取值。如果二进制(00100)2那应该是2^(3-1) = 2^2 = 4,那16进制0x00100那应该是16^(3-1) = 16^2 = (2^4)^2 = 2^8 = 256。虽说这些是常识但容易突发性懵圈。为便于后面的学习,有必要总结一下用2进制和16进制来计算字节时的关系:

       

字节描述数字16进制2进制位描述指数描述
1byte(字节)1              0x0001                 0000,0000,0000,00018bit(位)16^0 = 2^0
8byte8              0x0008                 0000,0000,0000,100064bit(16^0)×8 = 2^3
16byte16              0x0010                 0000,0000,0001,0000128bit(16^1)×1 = 2^4
32byte32              0x0020                 0000,0000,0010,0000256bit(16^1)×2 = 2^5
64byte64              0x0040                 0000,0000,0100,0000512bit(16^1)×4 = 2^6
128byte128              0x0080                 0000,0000,1000,00001024bit(16^1)×8 = 2^7
256byte256              0x0100                 0000,0001,0000,00002048bit(16^2)×1 = 2^8
512byte512              0x0200                 0000,0010,0000,00004096bit(16^2)×2 = 2^9
1024byte(1KB)1024              0x0400                 0000,0100,0000,00008192bit(16^2)×4 = 2^10
2KB2048              0x0800                 0000,1000,0000,0000……(16^2)×8 = 2^11
4KB4096              0x1000                 0001,0000,0000,0000……(16^3)×1 = 2^12
8KB8192              0x2000                 0010,0000,0000,000065536bit(16^3)×2 = 2^13
……
64KB65536           0x1,0000        0001,0000,0000,0000,0000……(16^4)×1 = 2^16
128KB……           0x2,0000        0010,0000,0000,0000,0000……(16^4)×2 = 2^17
256KB……           0x4,0000        0100,0000,0000,0000,0000……(16^4)×4 = 2^18
512KB……           0x8,0000        1000,0000,0000,0000,0000……(16^4)×8 = 2^19
1024K(1MB)……         0x10,00000001,…………(16^5)×1 = 2^20
2MB……         0x20,00000010,…………(16^5)×2 = 2^21
4MB……         0x40,00000100,…………(16^5)×4 = 2^22
8MB……         0x80,00001000,…………(16^5)×8 = 2^23
16MB……       0x100,0000…………(16^6)×1 = 2^24
…………
128MB……       0x800,0000…………(16^6)×8 = 2^27
256MB……     0x1000,0000…………(16^7)×1 = 2^28
512MB……     0x2000,0000…………(16^7)×2 = 2^29
1024M(1GB)……     0x4000,0000…………(16^7)×4 = 2^30
2GB……     0x8000,0000…………(16^7)×8 = 2^31
4GB……  0x1,0000,0000…………

(16^8)×1 = 2^32

        一个牛掰的程序猿对于上表那可以说是倒背如流。当然,这不是靠背的,而是靠多年工作积累的数字记忆。我标粗体的部分是推荐大家一定要记住的常用16进制标志的字节数。

        可能还是会有人理不清字节byte和位bit的关系。其实可以这样简单理解:计算机都是按最小单位bit来计算的,但bit的位数太大,为了方便人类计算,才会依次规定出B(字节)、K、M、G、T这些换算单位,它们换算因子数除了字节是8以外(8bit = 1byte),其余单位换算都是按1024(2^10)来计算。原则上讲,单位肯定是由小到大依次换算。比如我们刚好想描述1024个bit位,那一般不会用1K bit位这种表达方式(虽说某些情况下也有1Kb这种说法),而是换算成128byte而已(1024b ÷ 8 = 128B)。当然,在描述系统位数的时候还是会用“32位系统”或“64位系统”这种表达方式,而不会用“4字节系统”或“8字节系统”。

        坑五:这个其实属于《深入》那本书里的老话题了。我们知道32位系统的最大寻址空间是4GB,意思是我们用某段内存中的每一位(bit)标识另一段内存上的1字节空间(也就是8bit),就好像一个班长对应8个战士那样,上表4G的指数描述是2^32也刚好印证的这个结论(2^32 = 2^10×2^10×2^10×2^2=  1Gb*4 = 4G,每个数对应一个字节,就是可以描述4GB地址空间了)。

        熟记对应表后,我们能很快掌握:根据0x400对应1K字节,推出中断向量表(0x00000~0x003FF)占用1KB内存空间;根据0x00100是256字节,推出BIOS数据区(0x00400~0x004FF)占用256B,

        坑六:对启动而言,BIOS最重要的加载是中断向量表,BIOS数据区以及中断服务程序。对图1-2可能有疑问:为啥中断向量表和中断服务程序不放在一起,还要被BIOS数据区分割?另外,还有0x00500~0x00E05A夹在中间干啥?别急,这部分预留给bootsect,这是1.2要讨论的内容。

1.2.1 加载操作系统内核程序并为保护模式做准备

        之前书上提到,中断向量表是(0x00000~0x003FF),总共1024个字节,而中断向量表存储的是地址——中断服务程序的地址,而常识告诉我们地址一般用四个字节存储,具体来讲就是CS:IP组成的四字节,因此1024个字节只能存储256个中断向量。

        在1.2.1,按照书上的说法,上电后执行了一些BIOS代码后,计算机完成自检等操作,并强调这些读者不需要关心。然后紧接着就说CPU接收到一个int 0x19中断,并在中断向量表中找到0x19中断服务程序是0x0E6F2,然后CPU就指向这个中断服务程序,即“启动加载服务程序”的入口地址——把软盘第一扇区的程序(512B)加载到内存中指定位置。当然,这个程序肯定是BIOS预先设计固定好的。

        坑七:我倒要好好问问读者,如果你之前没有系统学习过嵌入式,果真就读懂了么?先提个质疑:既然中断向量表的范围是(0x00000~0x003FF)刚好1024字节,而每个中断向量的地址占4字节,总共256个中断向量,如果真是这么完美的设计,那么第一个中断向量的地址信息应该存储在0x00000~0x00003这四个字节中,0x00000就是这个信息的首地址,以此类推,首地址应该分别是0x00000、0x00004、0x00008、0x0000C、0x00010、0x00014、0x00018、0x0001C、0x00020……然而现在CPU接收到的却是0x00019?为什么?如果你真的去翻手册查一查BIOS的中断向量表就会发现,0x00019确实属于0x018 ~0x01B,但中断用途属于“保留”,也就是没有规定用途。而现在0x00019却成了“启动加载服务程序”的直接推动者,难道不奇怪么?

        坑八:当你毫无感知的仗着书上这几段文字,就以为自己了解的计算机启动过程,那就更天真了,随便问一句:既然BIOS的ROM默认不可修改,那开机的BIOS设置到底改的是哪里?这些都是细节问题,如果不去问为什么,可能永远也不得而知。这里我就稍微简介一下。

        触电前先明白一个常见的困惑就是,开机按DEL键启动的到底是BIOS设置还是CMOS设置?好像这两种说法都有,而且某些自以为是的人还会专门“纠正”告诉你“CMOS设置”的说法更准确……事实上,两种说法都没错,只是针对角度不同而已。你开机启动的第一个程序肯定是BIOS程序,这个程序写在BIOS ROM中。但稍微有点计算机常识的人就知道,一个可移植的,灵活的程序,不可能脱离数据而运行,BIOS也不例外,运行BIOS程序时依托的各种复杂的数据,就存储在CMOS芯片中,或者说CMOS RAM中,既然是RAM,那当然是可修改的,也就是我们用DEL键进入的修改界面。有了灵活多变的数据,BIOS程序就能根据不同的需求运行不同的启动方案。可见CMOS数据为BIOS程序服务,它们是个整体,硬说“BIOS设置”的提法错,完全没必要。

        触电后,主板时钟在电压稳定时,会向CPU发送一个开始工作的脉冲信号,相当于开机命令。CPU收到开机命令后,会自动构造CS:IP = 0xFFFF0,此时南桥芯片会负责把0xFFFF0对应到BIOS ROM中的0xFFFF0位置,也就是BIOS程序的入口地址。这里有个问题书上没说清楚:我们知道BIOS ROM上的程序早晚会映射到RAM中,但具体什么时候映射的?到底是0xFFFF0位置与ROM匹配成功后就将ROM信息全部拷贝到RAM,还是说BIOS程序执行起来后才拷贝?这个我也不知道,有兴趣的可以去研究下。

        好了,回到“把软盘第一扇区的程序(512B)加载到内存中指定位置”,这个所谓的指定位置,就是0x07C00,刚好在0x00500~0x00E05A的范围内,也就回答了坑六中提到的问题。

        被加载的第一扇区中有重要的OS启动代码,具体到linux 0.11,那就是引导程序bootsect.s,这一步也标志着linux终于登场,而接生婆就是0x0E6F2中存储的中断服务程序。好了,而剩下其他扇区的加载工作的就要交给bootsect.s自己了,正所谓:

        一生二,二生三,三生万物!

        CPU启BIOS,BIOS启bootsect.s,bootsect.s启万物!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值