先说,在度娘普照天下的时代,一些基础概念已经不需要再解释了,不然写到猴年马月。我只负责填坑。
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,0001 | 8bit(位) | 16^0 = 2^0 |
8byte | 8 | 0x0008 | 0000,0000,0000,1000 | 64bit | (16^0)×8 = 2^3 |
16byte | 16 | 0x0010 | 0000,0000,0001,0000 | 128bit | (16^1)×1 = 2^4 |
32byte | 32 | 0x0020 | 0000,0000,0010,0000 | 256bit | (16^1)×2 = 2^5 |
64byte | 64 | 0x0040 | 0000,0000,0100,0000 | 512bit | (16^1)×4 = 2^6 |
128byte | 128 | 0x0080 | 0000,0000,1000,0000 | 1024bit | (16^1)×8 = 2^7 |
256byte | 256 | 0x0100 | 0000,0001,0000,0000 | 2048bit | (16^2)×1 = 2^8 |
512byte | 512 | 0x0200 | 0000,0010,0000,0000 | 4096bit | (16^2)×2 = 2^9 |
1024byte(1KB) | 1024 | 0x0400 | 0000,0100,0000,0000 | 8192bit | (16^2)×4 = 2^10 |
2KB | 2048 | 0x0800 | 0000,1000,0000,0000 | …… | (16^2)×8 = 2^11 |
4KB | 4096 | 0x1000 | 0001,0000,0000,0000 | …… | (16^3)×1 = 2^12 |
8KB | 8192 | 0x2000 | 0010,0000,0000,0000 | 65536bit | (16^3)×2 = 2^13 |
…… | |||||
64KB | 65536 | 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,0000 | 0001,…… | …… | (16^5)×1 = 2^20 |
2MB | …… | 0x20,0000 | 0010,…… | …… | (16^5)×2 = 2^21 |
4MB | …… | 0x40,0000 | 0100,…… | …… | (16^5)×4 = 2^22 |
8MB | …… | 0x80,0000 | 1000,…… | …… | (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启万物!