64位模式听起来就很厉害呢,看它是32位的整整一倍呢,寻址能力也由4G到达了16EB呢!
太天真了,没有16EB(没有面包但是有土地),它要有我自己吃了它,满意了吧!
但是,有最大2^52=1M*1MB=1T*1000B=1PB(这还是好的,虽然是线性地址,据说地址线有的也有52位的,2^40次方就只有1TB寻址大小呢)大小的寻址能力呢,比32位好就是!
What`s up!神马玩意说好的64位呢不应该是2^64=16EB么怎么整整缩了2^12,啊,假如intel欺骗了你,不要悲伤,不要心急,忧郁的日子将会过去,希望......
吐槽完毕,回归正题intel这些生产CPU的公司估计为了节省经费少12根地址线少了整整多少寻址大小,额,自己算去吧敲敲计算机的事,有句话好叫积小流,得以成江海。intel积小钱得以成巨款(不错,还挺押韵的),总之让个位失望了,它没有2^64=16EB的寻址能力所以内存条插再多也没用(实际上你也插不到16EB)所以别问以后会不会有128位CPU,现在连会不会有64位完全寻址能力的CPU都没有,就别想那么多了。
那么究竟怎么进入64位模式呢?首先一个4级页表是“必不可少”的,what`s up!32位时还只有两层呢,咋直接变4层了,不要慌,如果你懒你可以整一个1GB大小的页,YES,1GB懒人的福利,当然前提是要CPUID告诉你CPU支持,瞬间就放弃了有没有
首先你需要亿些些表,就像你出生时要办的各种证明一样,进入64位你必须要提交各种表格,其中最必要的两项就是GDT表和页表(不知道是不是不开分页机制就可以不要,但是最好要),但说这么多虚的不实在,还是要从实践说起,先讲讲64位模式支持检测,上代码!
;=======================================================检查是否支持IA32E
check_ia32e_support:
xor edx,edx
mov eax,80000001h
cpuid
bt edx,30 ;没想到吧,叕改了一点点东西
jnc not_support
ret
not_support:
mov cx,size_cpu_nos
mov bl,red
mov bp,str_cpu_nos
call print
jmp sys_halt
看了这份代码懂的人一定会说,不是要先用cpuid的80000000h功能号先检查它是否支持80000001h及以上的cpuid功能吗?oh,不必多次一举,看到xor edx,edx了吗?先把这个要返回参数的寄存器清零,之后等它返回,如果不支持,它的edx寄存器值应该还是0,而此时bt edx,30(检测edx的第30位是否为1,如果为1则carry表示支持IA32E模式<-貌似也叫IA64)一定是会not carry,所以一句jnc not_support给这个不支持64位操作系统的用户来一句灵魂慰问让TA彻底放弃装64位操作系统的想法,连最大扩展功能号都不需要检测,少了好几行汇编指令,是不是很有逻辑!
而具体CPUID返回的参数我就不多花字数来写了,在许多blog都有到说呢,查查就能找到,好像叫cpuid扩展功能号吧, 在书的296面也有讲到,当然我还是要推荐一篇文章,因为书上296面edx的30位被作者直接抹黑了,实际上那个位是检测是否支持IA64的关键一位,在这篇文章上有讲到:
既然CPU支持了64位那么怎么进入呢?先看成功进入的图
由图可知进入64位模式需要一张页表,一张gdt表,当然还要更改一些cr的值,在loader加载期间,会在1.4MB的物理内存中0x9a000的位置构建页表,其代码如下所示
;构建页目录表
mov ax,SelData32
mov es,ax
mov ds,ax
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
mov dword[PML4_ADDR_ST],PDPT_ADDR_ST|0x07
mov dword[PDPT_ADDR_ST],PDT_ADDR_ST|0x07
mov dword[PDT_ADDR_ST],0x00000|0x83
mov dword[PDT_ADDR_ST+8],0x200000|0x83
mov dword[PDT_ADDR_ST+16],0x400000|0x83
mov dword[PDT_ADDR_ST+24],0x600000|0x83
mov dword[PDT_ADDR_ST+32],0x800000|0x83
;据计算1024*768*3Byte=0x240000byte 所以大概也就2MB多一点
;映射帧缓存
mov eax,[VBE_MODE_ADDR+40]
or eax,0x83
mov dword[PDT_ADDR_ST+0x1000-72],eax ;0MB
add eax,200000h
mov dword[PDT_ADDR_ST+0x1000-64],eax ;2MB
add eax,200000h
mov dword[PDT_ADDR_ST+0x1000-56],eax ;4MB
当然,这么操作必须要在1.4MB以下的干净的内存(貌似在真机上1.4MB内存空间默认是干净的,我在两台不同平台的电脑上实验过,当然,也在一台笔记本上确认过,结果的确如此)空间,否则嘛...,直接崩溃,为什么?看清楚上面的是dword是32位变量,而64位下的页表是64位的,如果内存是脏的(也就是说已经写过了数据),那么其高32位的基址可能不为零,如果用这样的页表则会出现我之前所遇到的情况,虚拟机上运行一切正常,而一放到真机上就直接重启了,查了几周没查出来(当然这几周没有专门去整这件事)
至于帧缓存的映射则是靠VBE_MODE_INFO_BLOCK里头的物理地址指针来映射的,为什么不按书《一个64位操作系统的设计与实现》上的0xe0000000来映射呢,这个原因也很简单,因为我真机测试时电脑叕重启了,what`s up.再仔细一看书上第279面表7-27下面的文字(也亏得我能找到):
这个地址可以不是0xe0000000,甚至在同一平台下它的位置都可以是0xc0000000
绝,绝了,于是便只好乖乖用物理平台提供的值去映射了,但具体为什么映射在0x3ee00000,其实我随便挑的,反正bochs会告诉我在哪,我只需要直接填到代码里就好了
随后是GDT表,这个GDT表一看就知道不是神马简单的玩意,写这玩意好像都花了我几天时间去学,一方面学了如何用汇编表示二进制数(后来发现ubuntu自带计算器的编程模式居然可以直接将二进制转16进制),另一方面嘛...,另一面去网上搜了大把关于GDT表的资料,当然后来看了一眼这本书的208和228瞬间感觉前面搜的资料都白费了,因为一眼就看懂了
于是,我就写了如下代码的32位GDT表和64位的GDT表
;======================================================================临时GDT表
GDT32_ST:
GDT32_NULL:
dq 0x0000000000000000
GDT32_CODE:
dw 0xffff;limit_low低16位的限长
dw 0x0000;base_low 低16位的基址
db 0x00 ;base_mid 中8 位的基址
db 10011010b ;fst_flag,type_flag 第一个标志与类型标志
db 11001111b ;scd_flag,limit_hig 第二个标志与高4位的限长
db 0x00 ;base_high 高8 位的基址
GDT32_DATA:
dw 0xffff;limit_low低16位的限长
dw 0x0000;base_low 低16位的基址
db 0x00 ;base_mid 中8 位的基址
db 10010010b ;fst_flag,type_flag 第一个标志与类型标志
db 11001111b ;scd_flag,limit_hig 第二个标志与高4位的限长
db 0x00 ;base_high 高8 位的基址
GDT32_END:
GDT64_ST:
GDT64_NULL:
dq 0x0000000000000000
GDT64_CODE:
dw 0x0000;limit_low低16位的限长
dw 0x0000;base_low 低16位的基址
db 0x00 ;base_mid 中8 位的基址
db 10011000b ;fst_flag,type_flag 第一个标志与类型标志
db 00100000b ;scd_flag,limit_hig 第二个标志与高4位的限长
db 0x00 ;base_high 高8 位的基址
GDT64_DATA:
dw 0x0000;limit_low低16位的限长
dw 0x0000;base_low 低16位的基址
db 0x00 ;base_mid 中8 位的基址
db 10010010b ;fst_flag,type_flag 第一个标志与类型标志
db 00000000b ;scd_flag,limit_hig 第二个标志与高4位的限长
db 0x00 ;base_high 高8 位的基址
GDT64_END:
GDT32_DESC dw GDT32_END - GDT32_ST - 1
dd GDT32_ST
SelCode32 equ GDT32_CODE - GDT32_ST
SelData32 equ GDT32_DATA - GDT32_ST
GDT64_DESC dw GDT64_END - GDT64_ST - 1
dd GDT64_ST
SelCode64 equ GDT64_CODE - GDT64_ST
SelData64 equ GDT64_DATA - GDT64_ST
随后,再用lgdt将GDT的基址和长度加载到GDTR寄存器里头去(不知道为什么不直接把GDT加载到CPU的缓存里,不是说CPU有L1L2L3这么多级缓存吗,明明加载进去运行应该会更快的才对),之后再将GDT表的段选择子加载到段寄存器中设置栈顶,代码如下所示
;重载gdt表
;db 0x66 ;没想到吧,我把这玩意注释掉了,看了眼书发现这是声明32位宽的前缀
;反正前面已经有了[bits 32]这里可以不要,顺便做个笔记
lgdt [GDT64_DESC]
mov ax,SelData64
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov esp,7c00h ;设置栈顶
当然,如上代码通过了运行检验(指虚拟机上)
之后我们需要置位一些cr(controll register)寄存器的值,让我们的系统进入64位模式,代码如下所示
;置位PAE标志位,即CR4的第5位
mov eax,cr4
bts eax,5
mov cr4,eax
;将页目录表的地址指向PML4_ADDR_ST,
mov eax,PML4_ADDR_ST
mov cr3,eax
;置位IA32_EFER寄存器以启动IA-32e模式
mov ecx,0x0c0000080
rdmsr
bts eax,8
wrmsr
;启动分页机制,以及保护模式使能位
mov eax,cr0
bts eax,0
bts eax,31
mov cr0,eax
;转跳至paw_loader
jmp SelCode64:PawLoaderADDR
虽然不会讲到每一个标志位,但是我会把上述代码开启的标志位说明一下:
PAE允许访问32位以上的物理地址,好像也叫物理地址扩展标志位,部分32位CPU也可以将此位置位,并置位CR4.PSE-36(前提是CPUID eax=80000001h时edx的17位被置位了),这样32位CPU可以访问36位,甚至是40位的物理地址,并且可以开启4MB分页模式,然而线性地址的寻址能力保持32位不变(这可能是为什么32位CPU这么操作会降低运行速度的原因,毕竟当所有低4GB内存用完访问高于4GB内存时就需要频繁切换页表了)
cr3寄存器不是控制功能的,而是用来记录页表基址的
在32位下:
其为32位寄存器,不可以直接赋值,必须要靠其它寄存器间接赋值,同时,它的低12位不可用,保持为0,这意味着页表必须保持4K对齐
在64位下:
其为64位寄存器,同样要间接赋值,其高32位无效(开玩笑的,这得看你CPU最大支持的物理地址),低12位用于PCID功能,这里我没用这个功能,只是将它单纯地指向了一个物理地址
MSR寄存器组是开启IA32E模式的关键:
通过rdmsr和wrmsr指令更改msr寄存器组中的IA32_EFER.LME置位,使用rdmsr和wrmsr之前不要忘记给ecx(rax)寄存器写上msr寄存器地址,这里IA32_EFER则是在MSR寄存器组地址0x0c0000080的位置,之后将它的第八位IME(IA32-E Mode Enable)置位,该寄存器功能是这样的
最后是cr0:
这个寄存器控制着分页(当然,据说不置位PG也可以,好像线性地址会与物理地址保持一致)和保护模式(PE,不是体育,是Protection Enable,置位可以运行32位模式的程序,当然需要一个32位的GDT选择子)的开启,其中PE和PG分别位于cr0的0位和31位,如下图所示
不容易,终于写完了,我还是收回上一章的话吧,这玩意,不简单。下一章就要开始将pawloader了,不仅要讲整个操作系统的启动流程,还要要挂新的源码链接了,说实话,内存分配,不好写啊,PCI驱动没有内存驱动,不好写啊!
当然如果文中有错误,请各位不吝赐教,在下方留言。