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

标签 x86 operating-system bootloader protected-mode real-mode

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

内核存在于第二个扇区(就在引导加载程序之后)等。

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

谢谢你。

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

最佳答案

在尝试进入保护模式之前,您需要进行几项设置:
在内存中初始化一个 GDT
您需要内存中的全局描述符表。它至少需要这些选择器的空间:

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

  • 在保护模式下,选择器是 GDT 或 LDT 的索引。代码和数据描述符告诉CPU在将选择器加载该索引时使用的存储器的基础地址和长度。LGDT指令集GDTR .
    在内存中初始化一个 TSS
    TSS 段告诉 CPU 您将在哪里存储 TSS。 TSS 中最初内置的一些功能很少有用,因为如果您手动进行上下文切换,则上下文切换会更快。然而,有一点是必不可少的:它存储了内核在进程从 ring3 转换到 ring0 时使用的堆栈。内核根本不能信任调用者。它不能假设调用者没有发疯并破坏堆栈指针。当从 ring3 转换到 ring0 时,CPU 从 TSS 加载堆栈指针,并将调用者堆栈段和偏移量压入内核堆栈,然后再压入代码段和偏移量返回地址。LTR指令用 TSS 段加载任务寄存器。
    在内存中初始化一个 IDT
    当各种事件发生时,IDT 让 CPU 查找要执行的操作。基本目的是异常处理。 CPU 将异常实现为中断。操作系统必须为所有异常设置处理程序。LIDT指令加载 IDTR .
    下面介绍硬件中断。
    如果在处理异常时发生异常,则发生双故障异常。如果在处理双重故障时发生异常,CPU 会将其转换为关闭消息给主板。发生这种情况时,典型的主板会重置 CPU,BIOS 会在其 bootstrap 启动代码中看到重置是意外的,并且会重新启动。
    初始化中断 Controller
    硬件设备还提供硬件中断(与前面提到的软件中断相反)。当设备需要服务时会发生硬件中断。
    如果您打算支持旧机器,那么您需要代码来使用和处理 8259 中断 Controller 。
    您需要代码来处理中断、保存上下文、确认中断,并以某种方式调用驱动程序或将工作项排在某处以服务硬件。
    当硬件设备断言其中断控制线(在古代系统上)或当 MSI 中断数据包到达 CPU 时(在能够并配置为使用 MSI 的现代系统上),中断 Controller 被设置为激发 CPU 处理中断.
    如果您想要最大的功能并需要支持多个处理器,那么您必须...
    初始化 APIC
    APIC 顾名思义:高级可编程中断 Controller 。
    APIC 允许对优先级、屏蔽和处理器间通信进行复杂的控制。它太大而复杂,无法在此处正确覆盖。
    初始化分页
    分页被分解为两级查找。顶层称为页面目录。第二级称为页表。
    每个页面由 1024 个 32 位页面描述符组成。高 20 位是该页表条目的物理地址的高 20 位。低位包含几个许可标志,让操作系统检测内存的使用情况,以便可以智能地交换/丢弃/保留它。
    每个页目录条目描述了该内存范围的一个 4KB 页表的基地址。页目录的每个条目都指向一个页表,该页表最多可以映射 4MB 的内存。
    页表的每个页描述符描述了对 4KB 内存范围的权限、访问历史和基址。
    因此操作系统必须为页目录分配至少一个 4KB 的页,并且为每 4MB 的内存提交至少一个 4KB 的页。请注意,您可能有稀疏映射,其中存在不存在内存的大区域,并且如果您访问它会发生页面错误。
    您可以使用 PG 启用分页一点点 CR0 . PDBR控制寄存器(CR3)告诉 CPU 页目录的物理地址。
    命令
    初始化 GDT、IDT、TSS(并在内存中分配内核堆栈内存、用户堆栈内存(如果需要)。
    在 GDT 内存的索引 1 和 2 处敲击 GDT 代码和数据条目,并将它们设置为零基地址、4GB 限制、ring0。
    设置 CR0 位 0,PE或保护启用位。
    大跳跃
    立即跳远到0x10:next-instruction其中 next-instruction 可能在链接器中解析为下一行的标签。 (您可以在堆栈上推送一个远指针并通过它间接远跳)。你需要从基地址中减去 (cs << 4) 因为跳转目标是相对于你在某个任意基数处组装的段,在实模式中设置 cs .
    进入保护模式后,你必须加载所有的段寄存器,因为 CPU 做了一堆权限检查,并在 CPU 中设置了几个在保护模式下不同的内部东西。
    告诉组装者!
    请注意,在该分支目标之后,您突然需要开始以不同的方式组装指令。在远跳之前,您处于实模式,但是一旦 cs 加载,CPU 中的很多事情都发生了变化,它实际上改变了它解码指令的方式。它假设 32 位寄存器和地址,地址大小前缀告诉它是 16 位。
    在实模式下,情况正好相反,地址大小或操作数大小前缀告诉它是 32 位。因此,您需要使用某种汇编程序指令来告诉汇编程序反转这些前缀的使用并更改各种内容以处理 32 位模式。
    显然,您需要设置堆栈。在为 LDT、IDT 等设置描述符地址时,您必须多次处理线性地址。
    现在您可以设置页目录和页表,加载 PBDR .
    在切换页表时,每个页目录条目都可以被标记为不被刷新。通常内核模式对每个进程都有相同的映射。
    通常每个进程都有自己的页目录,并共享内核表。它的用户模式分配是在它自己的用户内存范围的私有(private)页表中完成的。
    尽管不需要分页,但它启用了许多非常酷的功能和保护。你可能想要它。
    在您启用分页并加载 PDBR 之后,根据每个定义,您都完全处于保护模式,并且您已经实现了一段核心代码以在 x86 架构上实现操作系统。

    关于x86 - 引导加载程序后如何从实模式切换到保护模式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36968829/

    相关文章:

    algorithm - 缓存友好的离线随机读取

    assembly - 变量应该在寄存器中存储多长时间?

    linux - 与 Linux 内核汇编中的引导加载程序

    assembly - NASM 和 8 位内存偏移混淆

    x86 - Intel 8086汇编中CS和IP寄存器的用途是什么?

    assembly - 为什么传统模式下的 syscall/sysret 被认为是 "sufficiently poorly designed"?

    c - 当 CPU 需要大于 0xC0000000 的地址时,如何执行地址转换(virt->phy)?

    assembly - MikeOS 中的 .CAT 文件

    c - 实模式 OS 中的 16 位 .com C 程序

    gcc - 将 NASM 和 64 位 C 代码编译并链接到引导加载程序中