内核启动前的工作
在上一章的内容中,我们跳转到了setup.s
的代码部分,这章我们先讲一讲setup做了什么吧
entry start
start:
! ok, the read went well so we get current cursor position and save it for
! posterity.
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
! Get memory size (extended mem, kB)
mov ah,#0x88
int 0x15
mov [2],ax
! Get video-card data:
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width
! check for EGA/VGA and some config parameters
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx
! Get hd0 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
! Get hd1 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb
! Check that there IS a hd1 :-)
mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3
je is_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
这是这个程序最一开始的几行代码,他们做了什么呢,首先获取了光标位置,然后作为一个字存到了0x90000
。同理,获取了扩展内存的大小、一些显示类的信息和硬盘参数列表。
硬盘参数表是什么?在
PC
机中BIOS
设定的中断向量表中int 0x41
的中断向量位置存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于BIOS
来说,这里存放着硬盘参数表阵列的首地址0xFE401
。第二个硬盘的基本参数表入口地址存于int 0x46
中断向量位置处。每个硬盘参数表有16个字节大小。这些是硬件的相关知识,了解明白即可。
这些内核运行所需的机器系统数据,其中包括光标的位置,显示页面等数据,被加载到了内存的 0x90000 - 0x901FC
的位置上,咦,这段位置熟悉不,熟悉吧,这不就是bootsect
在的位置嘛,没错,他已经没用了,所以就被覆盖掉了。。。。好惨 :-),没用了就被覆盖了,而且你计算一下,这个空间一共也就512字节,新来的数据覆盖了510字节,也就两个字节幸免于难了,所以说,操作系统对于内存的使用是非常严谨的。
从实模式到保护模式
操作系统想要在32位保护下工作,这期间需要做大量的重建工作,并且持续到操作系统的main函数的执行过程
关中断并将system移动到内存地址起始位置(0x00000
)
! now we want to move to protected mode ...
cli ! no interrupts allowed !
! first we move the system to it's rightful place
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move
! then we load the segment descriptors
首先使用cli
指令屏蔽中断,准备开始乾坤大挪移,将system
模块移动到想要的位置(内存0地址处)。但是我们还记得0处存放着什么嘛,没错不是还放着BIOS的中断向量表和中断响应程序嘛,是的,被覆盖了,这也是为什么要关中断的原因,毕竟这个时候不关中断突然来一个中断不就死机了嘛。
EFLAGS
寄存器是标志寄存器,32位,包含状态标志,系统标志以及控制标志,cli就是改变了第9位IF中断允许标志位。
设置中断描述符表和全局描述符表
此时还需要通过setup程序自身提供的数据信息对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。
GDT,即全局描述表(GDT Global Descriptor Table)。
IDT,即中断描述符表(IDT interrupt Descriptor Table)。
在实模式下,对内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。在实际编程的时,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了保护模式,内存的管理模式分为两种:段模式和页模式,其中页模式也是基于段模式的。也就是说,保护模式的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式。由于保护模式运行在32位系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。另外,保护模式,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],即基地址,偏移地址与权限,它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,我们如果要通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。
解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13-bit的内容作为索引)
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
有关具体的分段与分页的信息可以参考我的另一篇博客,理论篇
! then we load the segment descriptors
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
我们来看看这所谓的idt_48
和gdt_48
到底是啥:
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
其实就是几个字,将idtr寄存器设置为了全0,将gdtr寄存器的值设置为了如下形式
那GDT是个什么地址呢,程序内也给出了,也就是gdtr寄存器指向了这个位置
gdt: ! gdt 表
.word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
GDT是保护模式下管理段描述符的数据结构,对操作系统自身的运行以及管理调度进程有着重大意义。此时由于内核尚未真正运行起来,所以还没有进程,所以现在创建的GDT第一项为空,第二项为内核代码段描述符,第三项为内核数据段描述符其余为空
开启 A20
地址线
! that was painless, now we enable A20
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
这里得注意一下:A20
地址线并不是打开保护模式的关键,只是在保护模式下,不打开A20
地址线,你将无法访问到所有的内存。 这个又是为了保持兼容性出的幺蛾子。empty_8042
这个函数的作用是测试8042
状态寄存器
! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
这一段是对中断的重新编程,涉及到 intel
的硬件问题。这些代码看起来是真的晦涩难懂,其实就是将 int 0x00 - int 0x1F
的中断重新映射了,因为这部分中断在保护模式下被 intel
保存用作内部中断和异常中断,不过所幸我们进入到保护模式了,Linus都说 that certainly wasn't fun
! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
CR0
的最后一位PE
,控制着是否开启保护模式,如果置1
,则么表示开启,此时CPU
将开始进入全新的模式。但为什么用lmsw ax
加载程序状态字的形式进行而不直接用mov cr0,ax
呢?又是历史的包袱,仅仅是为了兼容罢了。
虽然此时的Linux还是只能支持16MB物理内存,但是其线性寻址空间已经是不折不扣的4GB了。
CPU转换为保护模式的一个重要特征就是根据GDT决定后续执行哪里的程序,我们在setup.s
这个程序中也就结束了,不过怎么跳转到head.s
还是需要讲一下的
jmpi 0,8
这一行代码中的0是段内偏移地址,8是保护模式下的段选择符,用于选择描述符表和描述符表项以及所要求的的特权级,我们将8表示为二进制1000,这里的最后两个00表示的是内核特权级,与之对应的就是用户特权级11,第三位0表示的是GDT,如果是1则表示LDT,1000的1表示所选表的1项,(GDT想好排序为0项,1项,2项。。)来确定代码段的段基址和段限长等,
段描述符结构
在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符,段描述符的结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVpcHVBr-1676125525000)(C:\Users\LoveSS\Desktop\linux内核\picture\20160212173325995)]
我们还记得我们在gdt中放了什么嘛
gdt:
.word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
我们用第一项举例,第一项翻译成二进制为
0000 0000 1100 0000 1001 0010 0000 0000 0000 0000 0000 0000 0000 0111 1111 1111
根据上面的图我们可以看出,我们的段基地址 0x00000000
段限长为 0x007FF * 4KB = 8MB
特权级为 0x00 内核特权级
最终的内存分布
为了理清头绪,我们需要再写一下我们的内核内存分布
内存位置 | 内容 |
---|---|
0x00000 - 0x064B8 | 内核 |
0x064B9 - 0x8FFFF | 空 |
0x90000 - 0x901FC | setup在内存中保存的信息 |
0x901FD - 0x901FF | 空 |
0x90200 - 0x90A00 | setup程序 |
0x90A00 - 0x9FF00 | 栈(栈指向0x9FF00),并未占完 |
0x9FF01 - 0xFDFFF | 空 |
0xFE000 - 0xFFFFF | BIOS启动块 |