bootloader后如何从实模式切换到保护模式?
How to switch from real mode to protected mode after bootloader?
我刚刚为我的 OS 完成了一个非常简单的引导加载程序,现在我正在尝试切换到保护模式并跳转到内核。
内核存在于第二个扇区(紧接在引导加载程序之后)和其他扇区。
谁能帮我解决代码问题?我添加了评论以显示我的困惑所在。
谢谢。
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
您需要内存中的全局描述符 table。它至少需要这些选择器的空间:
- 您需要一个 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 加载堆栈指针,并将调用者堆栈段和偏移量压入内核堆栈,然后压入代码段和偏移量 return 地址。
LTR
指令用 TSS 段加载任务寄存器。
在内存中初始化一个IDT
IDT 允许 CPU 查找发生各种事件时要做什么。基本目的是异常处理。 CPU 将异常实现为中断。操作系统必须为所有异常设置处理程序。
LIDT
指令加载 IDTR
.
下面介绍了硬件中断。
如果在处理异常的过程中出现异常,则出现双重故障异常。如果在处理双重故障时发生异常,CPU 会将其转换为向主板发送的关闭消息。发生这种情况时,典型的主板会重置 CPU,BIOS 会在其 bootstrap 启动代码中看到重置是意外的,它会重新启动。
初始化中断控制器
硬件设备也提供硬件中断(相对于前面提到的软件中断)。当设备需要服务时会发生硬件中断。
如果您打算支持旧机器,那么您需要代码来使用和处理 8259 中断控制器。
您需要代码来处理中断、保存上下文、确认中断以及以某种方式调用驱动程序或在某处排队工作项以服务硬件。
中断控制器被设置为激发 CPU 处理中断,当硬件设备断言其中断控制线(在古代系统上)时,或者当 MSI 中断数据包到达 CPU(在能够并配置为使用 MSI 的现代系统上)。
如果您想要最大的功能并需要支持多个处理器,那么您必须...
初始化APIC
APIC 顾名思义:高级可编程中断控制器。
APIC 允许对优先级排序、屏蔽和处理器间通信进行复杂的控制。它太大太复杂,无法在此处正确涵盖。
初始化分页
分页被分解为两级查找。顶层称为页面目录。第二层称为页面table.
每页由1024个32位页描述符组成。高 20 位是该页 table 条目的物理地址的高 20 位。低位包含几个权限标志,让 OS 检测内存使用情况,因此可以智能地 swapped/discarded/kept.
每个页面目录条目都描述了该内存范围的一个 4KB 页面 table 的基地址。页目录的每个条目都指向一页 table,最多可以映射 4MB 的内存。
页面的每个页面描述符 table 描述了 4KB 内存范围的权限、访问历史和基地址。
所以操作系统必须至少为页目录分配一个4KB的页面,并且每提交4MB的内存至少分配一个4KB的页面。请注意,您可能有稀疏映射,其中存在不存在内存的大区域,如果您访问它会发生页面错误。
您使用 CR0
的 PG
位启用分页。 PDBR
控制寄存器 (CR3) 告诉 CPU 页目录的物理地址。
订单
在内存中初始化GDT、IDT、TSS(并分配内核栈内存、用户栈内存(如果需要)。
在 GDT 内存的索引 1 和 2 处敲击 GDT 代码和数据条目,并将它们设置为具有零基地址、4GB 限制、ring0。
设置 CR0 位 0,PE
或保护启用位。
大跃进
立即跳转到 0x10:next-instruction
,其中下一条指令可能在链接器中解析为下一行的标签。 (您可以将一个远指针压入堆栈并通过它间接跳远)。您需要从基址中减去 (cs << 4),因为跳转目标是相对于您在某个任意基址上组装的段而言的,在实模式 cs
.
中设置
你 必须 在进入保护模式后加载所有的段寄存器,因为 CPU 做了一堆权限检查并在 CPU 在保护模式下是不同的。
告诉汇编程序!
请注意,在那个分支目标之后,您突然需要开始以不同的方式汇编指令。在远跳转之前,您处于实模式,但一旦加载 cs,CPU 中的很多东西都发生了变化,它实际上改变了解码指令的方式。它假定 32 位寄存器和地址,地址大小前缀告诉它是 16 位。
在实模式下,情况正好相反,地址大小或操作数大小前缀告诉它是 32 位的。因此,您需要使用某种汇编程序指令来告诉汇编程序反转这些前缀的用法并更改各种内容以处理 32 位模式。
显然您需要设置堆栈。在为 LDT、IDT 等设置描述符地址时,您已经多次处理线性地址。
现在您可以设置页面目录和页面 tables,加载 PBDR
.
每个页面目录条目都可以标记为在切换页面时不刷新 tables。通常内核模式对每个进程都有相同的映射。
通常每个进程都有自己的页面目录,并共享内核 tables。它的用户模式分配是在其自己的私有页面 tables 上完成的,用于用户内存范围。
虽然分页不是必需的,但它提供了许多非常酷的功能和保护。你可能想要它。
启用分页并加载 PDBR 后,根据每个定义,您完全处于保护模式,并且您已经实现了一大块核心代码以在 x86 架构上实现操作系统。
@doug65536 的回答非常广泛和富有表现力,但是过分了。它涵盖了使处理器进入大多数操作系统所需的最终工作状态所需的一切。然而,当我们谈论在 x86 架构上从真正的 16 位模式切换到受保护的 32 位模式时,我们需要执行的操作要少得多,而这些操作确实是完成切换所必需的。
1。您的代码需要知道物理内存的布局。
原因是您的代码应该接触内存中的数据以切换到保护模式。因此,您需要知道此数据的位置。不幸的是,将如此重要的地址传递到代码中的唯一方法是使用带直接参数的指令(如 mov ax, 07C00h
)。因此,您必须在编译时知道您的代码和数据在内存中的位置!
2。您应该正确设置数据段寄存器。
道理是一样的:你需要内存中的触摸数据,所以你应该正确地处理这个数据。在 x86 实模式处理器中采用分段模型。在这个模型中,CPU 由两部分构造一个 20 位内存地址:隐式使用的 16 位段地址和显式指定的 16 位指针(段 * 16 + ptr)。专用的 16 位段寄存器存储段地址。您通常需要其中两个:代码段寄存器 (CS) 和数据段寄存器 (DS)。好消息是,如果您的代码已经在执行,那么 CS 已经初始化。所以,你不需要关心它。坏消息是同样的故事不适用于 DS,在接触内存之前设置它是你的责任。
3。您需要在内存中拥有或构建全局描述符 Table (GDT)。
GDT 是在保护模式下管理处理器的中央数据结构。一般至少需要三个段:
- 空段描述符。它是 x86 上 GDT 的强制性第一项。
- 代码段。需要切换到保护模式。
- 数据段。不需要直接执行此项目即可执行切换。但是,一旦您已经处于保护模式,您肯定需要访问内存中的某些数据。所以,如果你想在切换后做一些有用的事情,你必须在 GDT 中有一个数据段描述符。
4。您需要在内存中拥有或构建全局描述符 Table FWORD 指针。
要将 GDT 加载到 CPU,您需要具有 GDT 的内存中 FWORD 描述符。该描述符包含一个 16 位大小的 GDT(以字节为单位减一)和一个 32 位 GDT 在内存中的线性地址。是的,这个描述符也必须位于内存中。
5。您需要加载 GDT。
一旦你完成了 1-4(你有 GDT 和它的 FWORD 描述符,你知道它们的地址,并且你有 DS 初始化来寻址它们),你可以并且应该使用[将 GDT 加载到 CPU =11=]指令。之后,不仅是您本人,您的处理器也会知道 GDT。
6.您应该在切换前禁用中断。
切换后,CPU改变了中断处理的策略。在实模式中使用并基于通过中断向量 Table (IVT) 分派并由 BIOS 设置的将不再起作用。要在保护模式下处理中断,您需要设置和加载中断描述符Table (IDT)。一旦进入 PM 并且在加载 IDT 之前,到达 CPU 的任何中断都会使您的系统崩溃。因此,在切换之前,您必须使用 cli
指令禁用中断或准备并加载 IDT。
7。在 CR0 中启用 PM 模式。
完成 1-6 后(加载 GDT 并禁用中断),您最终可以通过在 CR0 寄存器中设置适当的标志来启用保护模式。
8。跳入保护模式。
最后但同样重要的是,在 CR0 (mov CR0, reg
) 指令中启用 PM 后,立即,您应该放置一个 jmp dword imm16:imm32
,其中 'imm16' 被 GDT 的代码段选择器替换,imm32 被跳转的目标地址替换。该指令重新加载 CPU(设置 CS 寄存器)的内存分段机制,并允许 CPU 继续获取和执行指令。如果没有这一步,内存分段保护子系统将几乎立即使您的代码崩溃。笔记!如果您的目标不是系统崩溃,则不允许在 mov CR0, reg
和 jmp dword imm16:imm32
之间放置任何其他指令!
就是这样!
从实模式切换到保护模式所需的唯一数据结构是正确设置和加载 GDT。
其他一切(准备 IDT、加载 TSS、重新加载新的 GDT 等)都不是切换到保护模式所必需的。当您已经处于保护模式时,您可以完成这些任务。
我刚刚为我的 OS 完成了一个非常简单的引导加载程序,现在我正在尝试切换到保护模式并跳转到内核。
内核存在于第二个扇区(紧接在引导加载程序之后)和其他扇区。
谁能帮我解决代码问题?我添加了评论以显示我的困惑所在。
谢谢。
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
您需要内存中的全局描述符 table。它至少需要这些选择器的空间:
- 您需要一个 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 加载堆栈指针,并将调用者堆栈段和偏移量压入内核堆栈,然后压入代码段和偏移量 return 地址。
LTR
指令用 TSS 段加载任务寄存器。
在内存中初始化一个IDT
IDT 允许 CPU 查找发生各种事件时要做什么。基本目的是异常处理。 CPU 将异常实现为中断。操作系统必须为所有异常设置处理程序。
LIDT
指令加载 IDTR
.
下面介绍了硬件中断。
如果在处理异常的过程中出现异常,则出现双重故障异常。如果在处理双重故障时发生异常,CPU 会将其转换为向主板发送的关闭消息。发生这种情况时,典型的主板会重置 CPU,BIOS 会在其 bootstrap 启动代码中看到重置是意外的,它会重新启动。
初始化中断控制器
硬件设备也提供硬件中断(相对于前面提到的软件中断)。当设备需要服务时会发生硬件中断。
如果您打算支持旧机器,那么您需要代码来使用和处理 8259 中断控制器。
您需要代码来处理中断、保存上下文、确认中断以及以某种方式调用驱动程序或在某处排队工作项以服务硬件。
中断控制器被设置为激发 CPU 处理中断,当硬件设备断言其中断控制线(在古代系统上)时,或者当 MSI 中断数据包到达 CPU(在能够并配置为使用 MSI 的现代系统上)。
如果您想要最大的功能并需要支持多个处理器,那么您必须...
初始化APIC
APIC 顾名思义:高级可编程中断控制器。
APIC 允许对优先级排序、屏蔽和处理器间通信进行复杂的控制。它太大太复杂,无法在此处正确涵盖。
初始化分页
分页被分解为两级查找。顶层称为页面目录。第二层称为页面table.
每页由1024个32位页描述符组成。高 20 位是该页 table 条目的物理地址的高 20 位。低位包含几个权限标志,让 OS 检测内存使用情况,因此可以智能地 swapped/discarded/kept.
每个页面目录条目都描述了该内存范围的一个 4KB 页面 table 的基地址。页目录的每个条目都指向一页 table,最多可以映射 4MB 的内存。
页面的每个页面描述符 table 描述了 4KB 内存范围的权限、访问历史和基地址。
所以操作系统必须至少为页目录分配一个4KB的页面,并且每提交4MB的内存至少分配一个4KB的页面。请注意,您可能有稀疏映射,其中存在不存在内存的大区域,如果您访问它会发生页面错误。
您使用 CR0
的 PG
位启用分页。 PDBR
控制寄存器 (CR3) 告诉 CPU 页目录的物理地址。
订单
在内存中初始化GDT、IDT、TSS(并分配内核栈内存、用户栈内存(如果需要)。
在 GDT 内存的索引 1 和 2 处敲击 GDT 代码和数据条目,并将它们设置为具有零基地址、4GB 限制、ring0。
设置 CR0 位 0,PE
或保护启用位。
大跃进
立即跳转到 0x10:next-instruction
,其中下一条指令可能在链接器中解析为下一行的标签。 (您可以将一个远指针压入堆栈并通过它间接跳远)。您需要从基址中减去 (cs << 4),因为跳转目标是相对于您在某个任意基址上组装的段而言的,在实模式 cs
.
你 必须 在进入保护模式后加载所有的段寄存器,因为 CPU 做了一堆权限检查并在 CPU 在保护模式下是不同的。
告诉汇编程序!
请注意,在那个分支目标之后,您突然需要开始以不同的方式汇编指令。在远跳转之前,您处于实模式,但一旦加载 cs,CPU 中的很多东西都发生了变化,它实际上改变了解码指令的方式。它假定 32 位寄存器和地址,地址大小前缀告诉它是 16 位。
在实模式下,情况正好相反,地址大小或操作数大小前缀告诉它是 32 位的。因此,您需要使用某种汇编程序指令来告诉汇编程序反转这些前缀的用法并更改各种内容以处理 32 位模式。
显然您需要设置堆栈。在为 LDT、IDT 等设置描述符地址时,您已经多次处理线性地址。
现在您可以设置页面目录和页面 tables,加载 PBDR
.
每个页面目录条目都可以标记为在切换页面时不刷新 tables。通常内核模式对每个进程都有相同的映射。
通常每个进程都有自己的页面目录,并共享内核 tables。它的用户模式分配是在其自己的私有页面 tables 上完成的,用于用户内存范围。
虽然分页不是必需的,但它提供了许多非常酷的功能和保护。你可能想要它。
启用分页并加载 PDBR 后,根据每个定义,您完全处于保护模式,并且您已经实现了一大块核心代码以在 x86 架构上实现操作系统。
@doug65536 的回答非常广泛和富有表现力,但是过分了。它涵盖了使处理器进入大多数操作系统所需的最终工作状态所需的一切。然而,当我们谈论在 x86 架构上从真正的 16 位模式切换到受保护的 32 位模式时,我们需要执行的操作要少得多,而这些操作确实是完成切换所必需的。
1。您的代码需要知道物理内存的布局。
原因是您的代码应该接触内存中的数据以切换到保护模式。因此,您需要知道此数据的位置。不幸的是,将如此重要的地址传递到代码中的唯一方法是使用带直接参数的指令(如 mov ax, 07C00h
)。因此,您必须在编译时知道您的代码和数据在内存中的位置!
2。您应该正确设置数据段寄存器。
道理是一样的:你需要内存中的触摸数据,所以你应该正确地处理这个数据。在 x86 实模式处理器中采用分段模型。在这个模型中,CPU 由两部分构造一个 20 位内存地址:隐式使用的 16 位段地址和显式指定的 16 位指针(段 * 16 + ptr)。专用的 16 位段寄存器存储段地址。您通常需要其中两个:代码段寄存器 (CS) 和数据段寄存器 (DS)。好消息是,如果您的代码已经在执行,那么 CS 已经初始化。所以,你不需要关心它。坏消息是同样的故事不适用于 DS,在接触内存之前设置它是你的责任。
3。您需要在内存中拥有或构建全局描述符 Table (GDT)。
GDT 是在保护模式下管理处理器的中央数据结构。一般至少需要三个段:
- 空段描述符。它是 x86 上 GDT 的强制性第一项。
- 代码段。需要切换到保护模式。
- 数据段。不需要直接执行此项目即可执行切换。但是,一旦您已经处于保护模式,您肯定需要访问内存中的某些数据。所以,如果你想在切换后做一些有用的事情,你必须在 GDT 中有一个数据段描述符。
4。您需要在内存中拥有或构建全局描述符 Table FWORD 指针。
要将 GDT 加载到 CPU,您需要具有 GDT 的内存中 FWORD 描述符。该描述符包含一个 16 位大小的 GDT(以字节为单位减一)和一个 32 位 GDT 在内存中的线性地址。是的,这个描述符也必须位于内存中。
5。您需要加载 GDT。
一旦你完成了 1-4(你有 GDT 和它的 FWORD 描述符,你知道它们的地址,并且你有 DS 初始化来寻址它们),你可以并且应该使用[将 GDT 加载到 CPU =11=]指令。之后,不仅是您本人,您的处理器也会知道 GDT。
6.您应该在切换前禁用中断。
切换后,CPU改变了中断处理的策略。在实模式中使用并基于通过中断向量 Table (IVT) 分派并由 BIOS 设置的将不再起作用。要在保护模式下处理中断,您需要设置和加载中断描述符Table (IDT)。一旦进入 PM 并且在加载 IDT 之前,到达 CPU 的任何中断都会使您的系统崩溃。因此,在切换之前,您必须使用 cli
指令禁用中断或准备并加载 IDT。
7。在 CR0 中启用 PM 模式。
完成 1-6 后(加载 GDT 并禁用中断),您最终可以通过在 CR0 寄存器中设置适当的标志来启用保护模式。
8。跳入保护模式。
最后但同样重要的是,在 CR0 (mov CR0, reg
) 指令中启用 PM 后,立即,您应该放置一个 jmp dword imm16:imm32
,其中 'imm16' 被 GDT 的代码段选择器替换,imm32 被跳转的目标地址替换。该指令重新加载 CPU(设置 CS 寄存器)的内存分段机制,并允许 CPU 继续获取和执行指令。如果没有这一步,内存分段保护子系统将几乎立即使您的代码崩溃。笔记!如果您的目标不是系统崩溃,则不允许在 mov CR0, reg
和 jmp dword imm16:imm32
之间放置任何其他指令!
就是这样!
从实模式切换到保护模式所需的唯一数据结构是正确设置和加载 GDT。
其他一切(准备 IDT、加载 TSS、重新加载新的 GDT 等)都不是切换到保护模式所必需的。当您已经处于保护模式时,您可以完成这些任务。