来看内核加载器程序c03_ldr.asm,它的入口点位于第46行,也就是标号start所在的位置。从这里开始,我们先获取光标位置,然后显示字符串。此时,处理器依然工作在实地址模式下,依然可以使用BIOS例程来显示字符串。
第47~49行获取光标位置;第51~56行显示字符串,其方法和从前一样,所以不需要多讲。字符串是在第10行的标号msg0处定义的,它的意思是“鼠侠x64学堂”。就像每个系统都有一个名字一样,我们这个小系统也需要一个名字。我想象着自己和亲爱的读者们一起共同学习x64架构,这就组成了鼠侠x64学堂。字符串的长度是用标号arch0减去msg0得到的,以字节计,或者说以字符计。
第51行将字符串所在的段内偏移量传送到寄存器BP,但是要访问这个字符串还需要一个段地址,还要让段寄存器DS指向字符串所在的段。实际上在这个时候,段寄存器DS正指向字符串所在的段。原因很简单,回到主引导程序c03_mbr.asm,在进入内核加载器之前,第70~72行,我们已经设置了DS和ES,DS和ES的内容是内核加载器物理地址右移4位的结果,所以,DS和ES所指向的段就是内核加载器程序所在的段。
回到内核加载器程序c03_ldr.asm,注意我们这套课程的目标,我们是要学习x64架构的处理器,所以当前所使用的处理器必须是一个x64架构的处理器,不然后面的程序就无法执行。为此,我们必须检测当前处理器是不是一个x64架构的处理器。
要想知道处理器是否支持x64架构,可以用功能号0x80000001执行cpuid指令,这个功能号要求处理器返回扩展的处理器签名和特性标志。指令执行后,处理器用EDX返回扩展特性和标志,它的位29用于指示处理器是否为x64架构,或者说是否支持IA-32e模式。
但是,很多老的处理器不一定支持0x8000 0001号功能,特别是那些不支持x64架构的处理器。为此,需要首先检测处理器是否支持0x8000 0001号功能。要想知道处理器是否支持0x8000 0001号功能,需要返回处理器能够支持的最大扩展功能号,并比较这个最大扩展功能号是不是大于0x8000 0001即可。
第58~59行,我们先在EAX中指定0x8000 0000号功能,然后执行cpuid指令。这个功能号用于返回处理器所支持的最大扩展功能号。此时,处理器用EAX返回它所支持的最大扩展功能号。
接着,我们用cmp指令将返回的最大扩展功能号与0x8000 0000进行比较。如果小于或者等于关系成立,则意味着处理器不支持0x8000 0001号功能,同时也意味着处理器不支持x64架构,所以转到标号no_ia_32e处执行。这个标号在前面不远处,第31行。
在标号no_ia_32e这里,我们先获取光标位置,然后显示字符串。字符串是在第13行定义的,它的意思是“x64不可用(未安装64位处理器)”。字符串显示之后,紧接着清中断,然后停机。这是可以理解的,如果当前处理器不支持x64架构,程序也就没有必要往下执行了,你需要换一台计算机再继续学习。
相反,如果处理器支持0x8000 0001号功能,则继续往下执行,用0x8000 0001号功能执行cpuid指令。此时,处理器用EDX返回扩展特性和标志,它的位29用来表明当前处理器是否为x64架构,是否支持ia-32e模式。
为了检测EDX的位29,我们需要用位测试指令bt。bt不是变态,是bit test,意思是位测试,这个指令的目的操作数用来指定一个位串,这个位串包含了我们要测试的那个比特。以下是bt指令的两种格式。
位串可以在寄存器里,也可以用内存地址来指定,所以目的操作数部分用 r/m 表示;源操作数用来指定要测试的比特,可以用寄存器来指定(用 r 表示),也可以用8位立即数来指定(用 imm8 表示)。
无论如何,在找到被测试的比特后,处理器将它原样传送到标志寄存器的CF标志位。然后,通过进位标志CF就知道被测试的比特是0还是1。
回到程序中,位串在EDX中,我们要测试位29,所以,第65行执行后,被测试的比特被传送到标志寄存器的进位标志CF。
接下来,第67行,如果进位标志是0,也就是没有进位,则意味着当前处理器不支持x64架构,不支持IA-32e模式,那就没什么好说的,只能转到no_ia-32e这里执行,显示提示信息并停机。相反,如果当前处理器支持x64架构,则这条jnc指令不发生转移,而是往下执行。下面的第69~78行用来显示信息,表明处理器支持x64架构。信息依然是用BIOS功能调用来显示的,显示的内容是在第12行定义的,意思是“x64可用(64处理器已经安装)”。
需要特别注意的是,在VirtualBox虚拟机上有一个奇怪的问题,虚拟机的处理器是否支持x64架构,居然和你选择的操作系统类型有关,不知道是有意的,还是一个缺陷。具体地说,如果你在安装虚拟机时选择的操作系统不是64位的,则上述代码将测试出你的处理器不支持x64架构。
因此,在创建VirtualBox虚拟机时,系统的“类型”一栏应选择“Other”,而版本则应当选择“Other/Unknown(64-bit)”。
一旦确定当前处理器属于x64架构,则继续往下执行,显示处理器商标信息。商标信息是一串字符,固化在处理器内部,需要用cpuid指令分3次才能全部取出,所使用的功能号分别是0x8000 0002、0x8000 0003和0x8000 0004。但是在此之前,按照标准的做法,需要先判断处理器是否支持这些功能号。
所以,第81~84行,我们首先返回当前处理器支持的最大扩展功能号,判断它是否小于0x8000 0004。如果小于关系成立,则意味着当前处理器不支持获取商标信息,于是转到标号.no_brand这里执行,实际上是跳过商标信息的获取和显示,继续往后面执行其他功能。
第86~105行,如果处理器支持获取商标信息,则我们连续用0x8000 0002、0x8000 0003和0x8000 0004号功能分3次获取商标信息,最终形成一个完整的字符串。这没有什么好说的,在《x86汇编语言:从实模式到保护模式》这本书里我们已经讲过。商标信息一共48个字符,从标号brand(第16行)开始定义了48字节的空间来保存这个商标字符串,初始值都为0。
回顾一下,在进入内核加载器之后,数据段寄存器DS指向哪里?从内核加载器开始的位置,是一个独立的段,段寄存器DS就指向这个段。同时,标号brand代表的数值是它相对于这个段起始处的偏移量。商标信息的写入是以4字节为一组的,每组字符所在的段内偏移量分别是brand+0、brand+4、brand+8等。
一旦获取了当前处理器的商标信息,第107~116行调用BIOS例程予以显示。显示的内容来自标号brand_msg,但这个字符串实际上是由第15~17行合并而成的。
显示了商标信息之后,程序的执行来到标号.no_brand。在前面,如果处理器不支持获取商标信息,同样会来到标号.no_brand,所以这是一个汇合点。从这里(第118行)开始一直到第137行,我们要获取当前系统的物理内存布局信息。实际上,这部分指令及用这部分指令获取的信息对本章和下一章是无用的,只在多处理器环境下有用,所以我们跳过这一部分,留到第5章再详细介绍。
接下来,从第140行开始,我们要获取当前处理器所支持的虚拟(线性)地址尺寸和物理地址尺寸。有关虚拟地址和物理地址,我们在第2章里已经详细做了说明。获取虚拟地址尺寸和物理地址尺寸有助于我们创建分页系统的相关表项。
要获取物理地址和虚拟地址尺寸,可以用功能号0x8000 0008调用cpuid指令。指令执行后,EAX的位0到位7是物理地址尺寸;位8到位15是虚拟地址尺寸,或者叫线性地址尺寸。
首先,我们需要判断当前处理器是否支持获取地址尺寸信息。第140~144行,我们用功能号0x8000 0000执行cpuid指令,返回最大支持的扩展功能号,然后比较功能号是否小于0x8000 0008。如果确实小于0x8000 0008,则表明处理器不支持返回地址尺寸的功能,所以要转到标号.no_plsize处执行。由于没能获取到物理地址和线性地址尺寸,所以在转移之前要先指定一个默认的地址尺寸。默认的物理地址尺寸为36(0x24),默认的线性地址尺寸为48(0x30)。
如果处理器支持功能号0x8000 0008,那么,第146~147行用来获取实际的物理地址尺寸和线性地址尺寸。
无论如何,程序的执行都将来到标号.no_plsize这里。此时,寄存器AX中保存着物理地址尺寸和线性地址尺寸,我们要把它保存到系统数据区留作后用,系统数据区用来保存整个系统全局的数据。
如图3-4所示,系统数据区在主引导程序的上面,物理地址是0x00007e00。为了方便,我们在全局定义文件global_def.asm里将它定义为符号常量SDA_PHY_ADDR。
访问系统数据区是临时性的动作,所以要用第151行的push指令保存DS以便将来恢复。接着,第152~153行,将DS切换到系统数据区。这是在实地址模式下,要将系统数据区的物理地址右移4位,得到逻辑段地址。如图3-5所示,这是系统数据区的布局,最低2字节分别是处理器的物理地址宽度和线性地址宽度。第154行,将AX中的地址数据保存到系统数据区内偏移为0的字内;第155行,恢复段寄存器DS的原始内容。
接下来,我们还要在屏幕上显示地址尺寸信息。如果只是在屏幕上显示数字,肯定让看的人觉得莫名其妙,所以还必须告诉屏幕前的人,显示的数字是物理地址尺寸和线性地址尺寸。为此,我们在内核加载器程序的前面构造了一个跨越多行的字符串。这个字符串的第一部分是从第19行的标号cpu_addr开始,其内容为“Physical address size:”,意思是“物理地址尺寸”。
图3-5 系统数据区的内存布局
接着,从标号paddr开始,定义了3字节的空间,并初始化为空格字符。处理器的物理地址尺寸不可能超过3位数,通常是两位数,比如36根地址线。物理地址尺寸是一个二进制数字,只有1字节,我们要将它转换为可打印的数字字符填写在这里。
接下来,我们又用伪指令db定义了一个字节的逗号,接着是另一个字节串“Linear address size:”,意思是“线性地址尺寸”。
在标号laddr这里,定义了3字节的空间,并初始化为空格字符。这里用来填写处理器的线性地址尺寸,线性地址尺寸是一个二进制数字,只有1字节,我们要将它转换为可打印的数字字符填写在这里。
以上,我们用6行组成一个完整的字符串,字符串用回车和换行(第24行的0x0d和0x0a)结束。在这个字符串中,paddr处的3字节,以及laddr处的3字节是需要在程序中填写的。
第158~169行,我们先将物理地址尺寸转换为数字字符,填写在前面的字符串中,转换和填写的工作是用一个循环来完成的。物理地址尺寸和线性地址尺寸保存在AX中,我们先处理物理地址尺寸,在这个过程中将破坏AX的内容,所以先用第158行的push指令将AX压栈保存。接着,第160行的and指令清除AX的高8位,这将在AL中保留物理地址尺寸数据,AH的内容为0。第163~169行是一个循环,我们用除以10取余数的方法分解数位并转换为数字字符。在循环之前,第162行在BL中设置除数10。
在循环体中,我们每次先将AX除以BL中的10。这是16位除法,商在AL中,余数是我们分解出来的数位,在AH里。分解出来的数位要加上0x30转换为数字字符,然后写入标号paddr这里。写入的位置实际上是paddr+si,SI的初值为2,每循环一次,就用dec指令减一,所以是从paddr这里从后往前写的。之所以倒着写,是因为在分解数位时先分解出个位上的数字,然后分解出十位上的数字。
每次循环的结尾,都要用第168行的and指令将AX的高8位,也就是AH清零,只保留AL中的商,用于在下一次循环中继续分解剩余的数位。如果AL中的商为零,则and指令执行后AX的内容肯定为零,意味着可以结束分解,此时,第169行的jnz指令不发生转移,直接往下执行。
接下来要将线性地址尺寸转换为数字字符填写在前面的字符串中。转换和填写的工作也是用循环来完成的,和前面是一样的。首先,第172行,从栈中恢复AX,恢复后的AX包含了物理地址尺寸和线性地址尺寸。第174行的shr指令将AX右移8位,如此一来,AH用零填充,AH里原先是线性地址尺寸,被移动到AL。第177~183行是一个循环,和前一个循环相同,只不过是将分解出来的数位保存到laddr这里。
数位分解之后,就可以显示了。字符串的显示依然是用BIOS例程来完成的,先是设置光标位置,接着调用字符串显示例程。字符串的地址来自标号cpu_addr,字符串的长度是用表达式protect-cpu_addr将两个标号相减得到的。
最后假定处理器的物理地址是36位的,而线性地址是40位的,则这段代码在屏幕上的显示效果是这样的:
现在请思考一下,在内核加载器程序里,为什么标号paddr和laddr处的3字节都初始化为空格字符?