兰陵月 发表于 2017-12-4 21:40:45

X86汇编语言-从实模式到保护模式—笔记(30)-第13章 程序的动态加载和执行(8)

本帖最后由 兰陵月 于 2017-12-5 21:50 编辑

13.3在内核中执行
从主引导扇区转移到内核后,程序会在内核程序的第532行开始执行。从第532行到第536行,主要是显示一个提示信息,其中使用了内核公用例程段的put_string过程,在该过程中又调用另一个过程put_char,这两个过程与第8章的两个过程名字一样,功能也是一样,但是实现过程稍有区别,这个区别当然是由于进入到保护模式下而导致的。第532行,将常数core_data_seg_sel的值传送给ecx寄存器。core_data_seg_sel在内核程序第8行有定义,它的值为0x30,注释为内核数据段的选择子。0x30的二进制表示方式为:0000000000110_0_00,从它的二进制方式我们可以看到内核数据段选择子的RPL位为“00”,表明其有最高特权级;它的TI位值为“0”,表明对应的段描述符在GDT中;索引值为110B,表明的对应的段描述符在GDT中为6#描述符。从图13-019中我们可以看到6#描述符是内核数据段描述符。第533行,把ECX寄存器中的段选择子值给段寄存器DS,处理器将在GDT中查找索引值为6的段描述符,在通过合法检查后,将该段描述符加载到DS的描述符高速缓存器中。检查的内容主要有:1、首先检查描述符是否超边界,本程序中索引值为6,而6×8+7等于55,因为55≤段界限63,因此本项检查通过;2、描述符类别字段检查,从主引导程序第115行我们可以知道数据段的属性值为0x00409200,其二进制表示形式为:0000 0000 0100 0000 1001 0010 0000 0000,可以看到该描述符的TYPE字段为0010,该值是一个有效值,因此本项检查通过;3、检查描述符的类别是否和段寄存器的用途匹配。本指令引用的段寄存器为DS,DS寄存器可以加载可读可写的数据段,可以加载可执行可读的代码段。本描述符TYPE字段为0010,表明这是一个可读可徐向上扩展的数据段。描述符类别和段寄存器相匹配,因此本项检查通过;4、检查描述符中的P位,如果P=0,表明虽然描述符已被定义,但该段实际并不存在于物理内存中。此时,处理器中止处理,引发异常中断11。一般来说,应当定义一个中断处理程序,把该描述符所对应的段从硬盘灯外部存储器调入内存,然后置P位。中断返回时,处理器将再次尝试刚才的操作。如果定义了调入内存的中断处理,则本检查最终会通过,如果没有中断11的处理程序,则本检查将不会通过;5、其他方面的检查。只有可以写入的数据段才能加载到SS的选择器,CS寄存器只允许加载代码段描述符。对于DS、ES、FS和GS的选择器,可以向其加载数值为0的选择子。第532行、第533行的意思是将内核的数据段与默认的段寄存器挂钩,因为内核程序需要使用自己的数据段。第535行、第536行,调用公共例程段内的一个过程来显示字符串。该call指令属于直接远转移,指令中给出了公共例程段的选择子和段内的偏移量。字符串是在第362行,用标号message_1声明,该处初始化了一段文字。显示例程put_string位于公共例程段内,在第37行定义。基本上,该过程的代码组成和工作原理都和从前一样,但也有不同之处。首先,这里的代码是32位模式的,字符串的地址由DS:EBX传入,过程返回用retf指令,而不是ret。这意味着,必须以远过程调用的方式使用它。在put_string过程的内部调用了另一个过程put_char。其中的movsd用于传送双字,不过是movsb还是movsw或者是movsd,在16位模式下,是把由DS:SI指向的源操作数传送到由ES:DI指向的目的地址。但是在32位模式下,源和目的则分别是DS:ESI和ES:EDI。第536行执行后,程序跳转至公共例程段标号put_string处,即第37行;第39行,因为过程内涉及到要操作ECX寄存器,所以将ECX中的内容压栈进行保护;第41行,将ebx指向的一个字符给cl;第42行,cl与自己进行或操作,最终结果还是自己,但是会影响标志寄存器的内容;第43行,判断标志寄存器的ZF是否为1,如果cl为零,则或操作后,ZF将变成1,如果cl不为零,则或操作后,ZF将变成0。如果ZF为1,则表明cl的值为零,表明字符串到结尾处,程序跳转至第48行,恢复ECX的值;第49行,程序远程返回。如果ZF为0,则表明cl的值不为零,是一个需要在屏幕上显示的字符串。调用put_char过程在屏幕上显示这个字符。显示完毕后,EBX自加1,指向下一个字符串,再次进行上面的比较。无论如何,最终程序都会来到第49行、第50行,然后返回第536行的下一句指令。回到第44行,当EBX指向一个需要在屏幕上显示的字符时,程序调用过程put_char,来到第56行。第56行,pushad,8个32位通用寄存器全进栈指令,压入堆栈的顺序是:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI,然后堆栈指针寄存器ESP的值减32,所以ESP进栈的内容是PUSHAD执行之前的值。同样,第140行,POPAD指令,该指令从堆栈弹出内容以PUSHAD相反的顺序送到这些通用寄存器,从而恢复PUSHAD之前的寄存器内容。但堆栈指针寄存器SP的值不是由堆栈弹出,而是通过增加32来恢复。回到第59行,第59行~第70行用来获取光标的位置数据,为什么要得到光标的数据,因为显示完一个字符后,光标需要向前推进一个位置。同时也还可能会发生几种情况:一是字符串显示在屏幕上时,可能超过最右下角,这种情况再显示的话就需要滚屏;二是有时候读取的字符是0x0d,0x0a等换行和回车等控制性字符,这样需要移动光标的值。我们来复习一下前面学过的知识,在书的第142页,第8章硬盘和显卡的访问与控制第8.4.4小节内容。光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值,该寄存器是可读可写的。光标寄存器要通过索引寄存器间接访问。索引寄存器的端口号0x3d4,可以向它写入一个值,用来指定内部的某个寄存器。两个8位光标寄存器的索引值分别是0x0e和0x0f,分别用于提供光标位置的高8位和低8位。指定了寄存器之后,要对它进行读写,可以通过数据端口0x3d5来进行。回到第59行,将索引寄存器的端口号0x3d4传送给dx;第60行,将高8位的光标寄存器索引值0x0e传送给al;第61行,将0x0e写入端口号0x3d4,指定要操作的高8位光标寄存器;第62行,dx自加1,端口号变成0x3d5;第63行,从端口0x3d5读出一个字节;第64行,将读出的字节传送至ah中;第66行,dx自减1,变成0x3d4;第67行,将低8位的光标寄存器索引值0x0f传送给al;第68行,将0x0f写入端口号0x3d4,指定要操作的低8位光标寄存器;第69行,dx自加1,端口号变成0x3d5;第70行,从0x3d5端口读取一个字节到al。第70行执行完毕之后,寄存器AX中就是完整的光标位置数据。第71行,将光标位置数据给BX留存备份。第73行,将cl的值与0x0d(回车符,回车符的意思是回到本行行首)进行比较。我们知道cl的值由第41行得来,它是刚刚取到的字符。第74行,jnz .put_0a,如果cl是回车符,则执行第75行;第75行~第78行,将光标位置数据除以80,再用得到的商乘以80,这样就得到光标在本行行首的位置数据,得到该位置数据后,跳转到标号.set_cursor;再回到第74行,假如cl不是回车符,则跳转到第82、83行,将cl与0x0a(换行符)进行比较;第84行,假如cl是换行符,则将光标位置数据加上80,变到下一行,同时跳转到标号.roll_screen处,进行是否滚屏的处理。假如不是换行符(程序执行到这里的时候表明cl同时也不是回车符,表明是一个需要输出到屏幕上的字符),则跳转到标号.put_other处,进行字符的输出处理。第89行,因此要操作ES寄存器,因此先将ES的值压栈保存;第90行,将常数video_ram_seg_sel给EAX。Video_ram_seg_sel在第10行有定义,值为0x20,注释为视频显示缓冲区的段选择子。0x20用二进制形式可以表示为:0000000000100_0_00,可以看到该段选择子RPL=00,TI=0,索引值为100,即4。从图13-019可以看到,这是4#描述符(显示缓冲区描述符),段基址0x000B8000,段界限0x07FFF的数据段;第91行,将该选择子值传送至es寄存器;第92行,将光标位置数据逻辑左移1位,相当于乘以2,这就是光标位置在内存中的偏移地址;第93行,将cl中的字符写入该偏移地址;第94行,es操作完毕,弹出恢复。第97行,将bx的值逻辑右移1位,相当于除以2,这样又恢复了其光标位置身份,而不是偏移地址身份了;第98行,将bx的值自加1,即往前推进一个位置。第100行,无论是通过换行即从第86行跳转过来,还是通过显示字符串执行到此处,程序都会来到这里。但读取到了回车符时程序并不会来到这里,因为如果读到了回车符后,光标位置不会增加,只会变小(比如光标原来本行除行首外的位置)或者不变(比如光标本来就在行首),既然光标位置不会增加,那当然就不需要考虑到滚屏的情况了,这也就是第80行并不会跳转到这里,而是直接跳转到设置光标程序处的原因。回到第101行,现在BX(存放光标位置数据)变大了,因此我们要考虑是否滚屏,第101行,将bx与2000做比较,为什么与2000做比较,因为屏幕最右下角的光标位置数据为1999;第102行,jl .set_cursor,如果bx值小于2000,代表还不需要滚屏,程序直接跳转到标号.set_cursor处,直接移动光标。如果bx大于等于2000,则需要进行滚屏操作。因为要到段寄存器ds和es,所以第104行、105行,将这两个寄存器压栈保护,滚屏实际上就是将第2行到第25行的内容复制到第1行到第24行,然后再讲第25行用空格填充。用了movsd指令,这里不再细说。滚屏操作后,光标肯定到第25行行首位置,此时光标位置数据为1920,所以将该位置数据给BX。不管是回车、换行,还是显示可打印字符,光标位置数据都会发生变化,最终都要将变化后的光标位置数据写回光标寄存器,在屏幕上设置光标。第127行~第132行,将光标位置高8位写入;第134行~第138行,将光标位置低8位写入;第140行,恢复各寄存器的值;第141行,程序返回,用ret返回,这是段内返回,返回到put_string中。提示信息显示结束之后,程序运行到第539行。第539行到第565行的主要工作是显示处理器品牌信息。处理器的功能是很强劲的,同时,在处理器内部也隐藏着太多的秘密,除了处理器的型号,还有大量的特征信息,比如高速缓存的数量、是否具备温度和电源管理功能、逻辑处理器的数量、高级可编程中断控制器的类型、线性(物理)地址的宽度、是否具有多媒体扩展和单指令多数据指令等特性。但是由于处理器的更新换代,软件也跟着更新换代,但是原来的老处理器仍然在使用,这个时候使用新指令的软件就不能运行在旧版的处理器上了。因此,在决定程序是否能运行前,我们必须探测和挖掘处理器内部的秘密。cpuid指令(CPU Identification)用于返回处理器的标识和特性信息。EAX用于指定要返回什么样的信息,也就是功能。有时候,还要用到ECX寄存器。cpuid指令执行后,处理器将返回的信息放在EAX、EBX、ECX或者EDX中。原则上,在使用cpuid指令前,先要检测处理器是否支持该指令;接着再用cpuid指令检测是否支持所需要的功能。下图图13-020为扩展到32位长度的标志寄存器EFLAGS,它的ID标志位(位21)可以被设置和清除,则不支持CPUID指令,反之,该处理器支持该指令。这个标志最开始出现在80486上,而80486处理器已经很久远了,现在已经没人使用这样的计算机了,所以,一般情况下,不需要检测处理器是否支持cpuid指令。
为了探测处理器最大能够支持的功能号,应该先用0号功能来执行cpuid指令:mov eax,0cupid。处理器执行后,将在EAX中返回最大可以支持的功能号。同时,还在EBX、ECX和EDX中返回处理器供应商的信息。对于Intel处理器来说,返回的信息如下:EBX←0x756E6547(对应字符串“Genu”,G在BL中,其他类推)EDX←0x49656E69(对应字符串“ineI”,i在DL中,其他类推)ECX←0x6C65746E(对应字符串“ntel”,n在CL中,其他类推)组合起来就是“GenuineIntel”。要返回处理器的品牌信息,需要使用0x80000002~0x80000004号功能,分三次进行。该功能仅被奔腾4之后的处理器支持,正确的做法是先用0号功能执行cpuid指令,以判断自己的处理器是否支持。本程序未这样做,省略(但这是不应该的)。第539行~第558行,分三次获得处理器的品牌信息,并依次存入标号cpu_brand处的52字节长度的缓冲区中,程序中直接使用了每隔4字节的偏移,很好理解。Cpuid返回的处理器信息是ASCII码,因此不需要再进行转换,直接就可以在屏幕上显示输出。第560、561行,输出标号cpu_brnd0处的字符,即一个回车一个换行,再输出两个空格。第562、563行,输出标号cpu_brand处的字符信息,我们刚才通过三次cpuid指令得到的处理器的品牌信息ASCII码就存放标号cpu_brand处的缓冲区里。第564、565行,输出标号cpu_brnd1处的字符,即两个换行符两个回车符。到这里为止,处理器的品牌信息就输出完成了。
页: [1]
查看完整版本: X86汇编语言-从实模式到保护模式—笔记(30)-第13章 程序的动态加载和执行(8)