从实模式切换到保护模式经历下面过程:
- cmpw $0, %cs:realmode_swtch;首先有没有切换到保护模式的例程(subroutine);如果有则跳转到这个例程处,在我们调试环境中没有。
cmpw $0, %cs:realmode_swtch
jz rmodeswtch_normal //跳转rmodeswtch_normal
- 跳转到default_switch处,通过outb %al, $0x70向IO 0x70端口处,写入0x80值,来屏蔽NMI。
default_switch:
cli # no interrupts allowed ! 0x90c56
movb $0x80, %al # disable NMI for bootup
# sequence
outb %al, $0x70
lret
- 设置code32的开始地址,也就是跳转到head.S处的地址(0x10 0000)
rmodeswtch_end:
# we get the code32 start address and modify the below 'jmpi'
# (loader may have changed it)
movl %cs:code32_start, %eax
movl %eax, %cs:code32
code32_start: # here loaders can put a different
# start address for 32-bit code.
#ifndef __BIG_KERNEL__
.long 0x1000 # 0x1000 = default for zImage
#else
.long 0x100000 # 0x100000 = default for big kernel
#endif
- 通过判断loadflags和type_of_loader值来判断是否需要搬移保护模式代码(head.S)到0x100000(bzImage)处;grub已经把内核保护代码搬移到0x100000处,直接跳转到end_move_self
testb $LOADED_HIGH, %cs:loadflags //0x81 0x90ac3
jz do_move0 # .. then we have a normal low //0x90ac9
# loaded zImage
# .. or else we have a high
# loaded bzImage
jmp end_move # ... and we skip moving //0x90acb
end_move:
# then we load the segment descriptors
movw %cs, %ax # aka SETUPSEG //0x90af2
movw %ax, %ds //0x90af4
# Check whether we need to be downward compatible with version <=201
cmpl $0, cmd_line_ptr //0x99000 0x90af4
jne end_move_self # loader uses version >=202 features //0x90afc
cmpb $0x20, type_of_loader
je end_move_self # bootsect loader, we know of it
- 判断A20是否打开,如果没有打开,这尝试着上节说的三种方式打开,在我们的环境中已经打开。
- 加载中断向量寄存器(idt)和全局描述表寄存器(gdtr)。
# set up gdt and idt
lidt idt_48 # load idt with 0,0
xorl %eax, %eax # Compute gdt_base
movw %ds, %ax # (Convert %ds:gdt to a linear ptr)
shll $4, %eax
addl $gdt, %eax
movl %eax, (gdt_48+2)
lgdt gdt_48 # load gdt with whatever is
gdt:
.fill GDT_ENTRY_BOOT_CS,8,0
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
gdt_end:
.align 4
.word 0 # alignment byte
gdt_48:
.word gdt_end - gdt - 1 # gdt limit
.word 0, 0 # gdt base (filled in later)
把gdt的标签的地址给gdt_48高32位,低16位是GDT的大小。
7. 重启协处理器
# make sure any possible coprocessor is properly reset..
xorw %ax, %ax
outb %al, $0xf0
call delay
outb %al, $0xf1
call delay
- 屏蔽所有中断
# well, that went ok, I hope. Now we mask all interrupts - the rest
# is done in init_IRQ().
movb $0xFF, %al # mask all interrupts for now
outb %al, $0xA1
call delay
movb $0xFB, %al # mask all irq's but irq2 which
outb %al, $0x21 # is cascaded
- 设置CR0的PE(Protedted Enable)
movw $1, %ax # protected mode (PE) bit
lmsw %ax # This is it!
- 设置esi为32位指针
flush_instr:
xorw %bx, %bx # Flag to indicate a boot
xorl %esi, %esi # Pointer to real-mode code
movw %cs, %si
subw $DELTA_INITSEG, %si //si=0x9020-x020=0x9000
shll $4, %esi # Convert to 32-bit pointer esi=0x90000
- 跳转到0x100000处,也就是compressed/head.S处
# jump to startup_32 in arch/i386/boot/compressed/head.S
#
# NOTE: For high loaded big kernels we need a
# jmpi 0x100000,__BOOT_CS
#
# but we yet haven't reloaded the CS register, so the default size
# of the target offset still is 16 bit.
# However, using an operand prefix (0x66), the CPU will properly
# take our 48 bit far pointer. (INTeL 80386 Programmer's Reference
# Manual, Mixing 16-bit and 32-bit code, page 16-6)
.byte 0x66, 0xea # prefix + jmpi-opcode
code32: .long 0x1000 # will be set to 0x100000
# for big kernels
.word __BOOT_CS //0x10
.byte 0x66,0xea 0x66是机器码的前缀,告诉CPU取48位,也就是6个字节.也就是ljmp 16bit:32bit
CS=code32[15:0]=_BOOT_CS=0x10 EIP=code32[47:16]=0x10000,这样就成功跳转到0x10000处,也就是compressed/head.S处。
说明:
CPU切换保护模式和实模式,可以通过处置位CR0寄存器的PE位。
PE位为1---------->CPU工作在保护模式下
PE位为0---------->CPU工作在实模式下
段寄存器和段描述符寄存器
段描述符寄存器(GDTR)是48位寄存器,结构如下
32 bits(the location of the GDT in memory) | 16 bits (size of GDT) |
---|
段选择寄存器(CS,SS,DS,ES)是16位寄存器,结构如下
索引号13位 | TI(1位) | RPL(2位) |
---|
全局段描述符
整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置。可以把GDT的入口地址存放在GDTR中。这样CPU就可以通过GDTR去访问GDT。
我们将GDT设定到某个内存位置后,通过LGDT指令将GDT的入口地址装入此寄存器。
LGDT的使用如下:
LGDT src
IDTR(Limit) SRC[0:15];
IDTR(Base) SRC[16:47] AND 00FFFFFFH;
全局描述符的某项的结构如下:
比如在Linux中的head.S中这样初始化全局描述符:
ENTRY(cpu_gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* 0x0b reserved */
.quad 0x0000000000000000 /* 0x13 reserved */
.quad 0x0000000000000000 /* 0x1b reserved */
.quad 0x0000000000000000 /* 0x20 unused */
.quad 0x0000000000000000 /* 0x28 unused */
.quad 0x0000000000000000 /* 0x33 TLS entry 1 */
.quad 0x0000000000000000 /* 0x3b TLS entry 2 */
.quad 0x0000000000000000 /* 0x43 TLS entry 3 */
.quad 0x0000000000000000 /* 0x4b reserved */
.quad 0x0000000000000000 /* 0x53 reserved */
.quad 0x0000000000000000 /* 0x5b reserved */
.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
bit40~bit46,对应于描述项中的type以及S标志和DPL位段。
KERNEL_CS:DPL=0,表示0级:S位为1,表示代码段或者数据段:type为1010,表示代码段,可读,可执行,尚未收到访问。
KERNEL_CS:DPL=0,表示0级:S位为1,表示代码段或者数据段:type为0010,表示代码段,可读,尚未收到访问。
比如cpu_gdt_table在内存中的位置是0x1000,则gdtr是0x1000 00。
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
#define GDT_ENTRY_DEFAULT_USER_DS 15
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
逻辑地址转换成线性地址的过程
例如给出逻辑地址:比如在代码CS=0x21
21h:12345678h转换为线性地址。
a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
问题:gdtr存放的地址是物理地址吗?是不是CPU直接从gdtr拿到的地址是物理地址,直接找到gdt,然后根据偏移量,找到相应的段描述符;而不是还要进行一层转换?
答:lgdt gdt_48加载的是物理地址.。
控制寄存器(CR0,CR1,CR2,CR3,CR4)
控制寄存器可以控制CPU的一些特性。
CR0寄存器说明:
CR0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
CR0的第1 位是监控协处理位MP(Moniter coprocessor),它与第3位一起决定:当TS=1时操作码WAIT是否产生一个“协处理器不能使用”的出错信号。第3位是任务转换位(Task Switch),当一个任务转换完成之后,自动将它置1。随着TS=1,就不能使用协处理器。
CR0的第2位是模拟协处理器位 EM (Emulate coprocessor),如果EM=1,则不能使用协处理器,如果EM=0,则允许使用协处理器。
CR0的第4位是微处理器的扩展类型位 ET(Processor Extension Type),其内保存着处理器扩展类型的信息,如果ET=0,则标识系统使用的是287协处理器,如果 ET=1,则表示系统使用的是387浮点协处理器。
CR0的第31位是分页允许位(Paging Enable),它表示芯片上的分页部件是否允许工作。
CR0的第16位是写保护未即WP位(486系列之后),只要将这一位置0就可以禁用写保护,置1则可将其恢复。
CR1是未定义的控制寄存器,供将来的处理器使用。
CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
CR3是页目录基址寄存器,保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会。
CR4在Pentium系列(包括486的后期版本)处理器中才实现,它处理的事务包括诸如何时启用虚拟8086模式等。
LMSW r16/m16: 将操作数装入CR0的0-15位。
ljmp
ljmp $0xfebc, $0x12345678
Long jump, use 0xfebc for the CS register and 0x12345678 for the EIP register
realmode_swtch:
A 16-bit real mode far subroutine invoked immediately before entering protected mode.
The default routine disables NMI, so your routine should probably do so, too.