引导加载程序后如何从实模式切换到保护模式?

Dav*_*vid 4 x86 operating-system real-mode protected-mode bootloader

我刚刚为我的操作系统完成了一个非常简单的引导加载程序,现在我正在尝试切换到保护模式并跳转到内核.

内核存在于第二个扇区(在引导加载程序之后)并且打开.

任何人都可以帮我解决我的代码吗?我添加了评论,以显示我的困惑在哪里.

谢谢.

BITS 16 

global start
start:
    ; initialize bootloader and stack
    mov     ax, 0x07C0
    add     ax, 288
    mov     ss, ax
    mov     sp, 4096
    mov     ax, 0x07C0
    mov     ds, ax

    call    kernel_load
    hlt

kernel_load:
    mov     si, k_load
    call    print

    mov     ax, 0x7C0
    mov     ds, ax
    mov     ah, 2
    mov     al, 1
    push    word 0x1000
    pop     es
    xor     bx, bx
    mov     cx, 2
    mov     dx, 0
    int     0x13

    jnc     .kjump
    mov     si, k_fail
    call    print
    ret

.kjump:
    mov     si, k_succ
    call    print

    ; this is where my confusion starts

    ; switch to protected mode???
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax

    ; jump to kernel? 
    jmp     0x1000:0

    hlt

data:
    k_load  db "Initializing Kernel...", 10, 0
    k_succ  db "Kernel loaded successfully!", 10, 0
    k_fail  db "Kernel failed to load!", 10, 0

print:
    mov     ah, 0x0E
.printchar:
    lodsb
    cmp     al, 0
    je      .done
    int     0x10
    jmp     .printchar
.done:
    ret

times 510-($-$$) db 0
dw 0xAA55
Run Code Online (Sandbox Code Playgroud)

dou*_*536 7

在尝试进入保护模式之前,您需要设置几件事:

在内存中初始化GDT

您需要内存中的全局描述符表.它至少需要这些选择器的空间:

  • 您需要一个ring0 32位代码描述符
  • 您需要一个ring0 32位数据描述符
  • 您需要GDT细分
  • 您需要一个IDT细分
  • 您需要TSS细分
  • 您可能需要一个LDT段(每个进程都应该有一个LDT,它在每个进程中以相同的线性地址开始,然后一个LDT描述符可以处理每个进程,并且分页将处理切换).

在保护模式下,选择器是GDT或LDT的索引.代码和数据描述符告诉CPU在选择器加载该索引时要使用的基址和内存长度.

LGDT指令设置LTDR.

在内存中初始化TSS

TSS段告诉CPU您要在哪里存储TSS.最初内置于TSS中的一些功能非常有用,因为如果手动执行上下文切换会更快.但是,它有一点必不可少:它存储堆栈供内核在进程从ring3转换为ring0时使用.内核不能相信呼叫者在所有.它不能假设调用者没有发疯并破坏堆栈指针.当从ring3转换到ring0时,CPU从TSS加载堆栈指针,并在推送代码段和偏移返回地址之前将调用程序堆栈段和偏移量推送到内核堆栈.

LTR指令使用TSS段加载任务寄存器.

在内存中初始化IDT

IDT允许CPU查找发生各种事件时要执行的操作.基本目的是异常处理.CPU将异常实现为中断.操作系统必须为所有异常设置处理程序.

LIDT指令装载IDTR.

硬件中断如下所述.

如果在处理异常时发生异常,则会发生双重故障异常.如果在处理双重故障时发生异常,则CPU会将其转换为关闭主板的关闭消息.当发生这种情况时,典型的主板将重置CPU,BIOS将在其引导启动代码中看到重置是意外的,并且它将重新启动.

初始化中断控制器

硬件设备还提供硬件中断(与前面提到的软件中断相反).当设备需要服务时,会发生硬件中断.

如果您打算支持旧机器,那么您需要使用代码并处理8259中断控制器.

您需要代码来处理中断,保存上下文,确认中断,以及以某种方式调用驱动程序或将工作项排队到某个地方以维护硬件.

中断控制器设置为激活CPU处理中断,当硬件设备断言其中断控制线(在古代系统上),或当MSI中断数据包到达CPU时(在能够并配置为使用MSI的现代系统上) .

如果您想要最大的功能并且需要支持多个处理器,那么您必须......

初始化APIC

APIC正如其名称所说:高级可编程中断控制器.

APIC允许对优先级,屏蔽和处理器间通信进行复杂的控制.在这里真正覆盖它太大而复杂.

初始化分页

分页被分解为两级查找.顶级称为页面目录.第二级称为页表.

每页包含1024个32位页面描述符.高20位是该页表条目的物理地址的高20位.较低位包含多个用于权限的标志,并允许操作系统检测内存的使用情况,以便智能地交换/丢弃/保留.

每个页面目录条目描述该内存范围的一个4KB页表的基址.页面目录的每个条目指向一个页面表,该表可以具有最多4MB的内存映射.

页表的每个页面描述符描述4KB范围内存的权限,访问历史和基址.

因此,操作系统必须为页面目录分配至少一个4KB页面,并为每4MB内存提交至少一个4KB页面.请注意,您可能具有稀疏映射,其中存在没有内存的大区域,并且如果您访问它,则会发生页面错误.

你可以使用PGbit来启用分页CR0.该PDBRMSR寄存器告诉CPU页目录的物理地址.

订购

初始化GDT,IDT,TSS(并在内存中分配内核堆栈内存,用户堆栈内存(如果需要)).

在GDT内存的索引1和2处进行GDT代码和数据输入,并将它们设置为零基址,4GB限制,ring0.

将CR0位0置1 PE或保护使能位.

大跳跃

立即跳转到0x10:next-instruction下一条指令可能在链接器中解析到下一行的标签.(你可以将一个远指针推到堆栈上并通过它间接跳远).您需要从基地址中减去(cs << 4),因为跳转目标是相对于您在某个任意基础上组装的段,在实模式中设置cs.

必须因为CPU做了一堆的权限检查,并建立了在保护模式不同的CPU中的一些内部的东西进入保护模式后,加载所有的段寄存器.

告诉汇编程序!

请注意,在该分支目标之后,您突然需要以不同方式开始组装指令.在远程跳转之前,你处于实模式,但是一旦加载了cs,CPU中就会发生很多变化,实际上它改变了解码指令的方式.它假设32位寄存器和地址,地址大小前缀表示它是16位.

在实模式中,反过来说,地址大小或操作数大小前缀告诉它是32位.因此,您需要使用某种汇编程序指令来告诉汇编程序反转这些前缀的用法并更改各种事项以处理32位模式.

显然你需要设置堆栈.在设置LDT,IDT等的描述符地址时,您必须多次处理线性地址.

现在您可以设置页面目录和页面表,加载PBDR.

每个页面目录条目都可以标记为在切换页表时不刷新.通常,内核模式对每个进程都具有相同的映射.

通常,每个进程都有自己的页面目录,并共享内核表.其用户模式分配是针对用户存储器范围的自己的私有页表进行的.

虽然不需要分页,但它可以提供许多非常酷的功能和保护.你可能想要它.

在启用分页并加载PDBR之后,按照每个定义,您完全处于保护模式,并且已经实现了一大块核心代码以在x86体系结构上实现操作系统.