过程详解 | 操作系统16位实模式切换到32位保护模式

VSole2021-09-22 17:09:11

这篇文章的内容是我在研究自制操作系统时遇到的一个问题,即如何从16位实模式切换到32位保护模式,网上的资料比较少,讲得也不是很详细,在这跟大家分享一下我的解决方案。

前置知识:

计算机是如何启动的?

(http://www.ruanyifeng.com/blog/2013/02/booting.html)

自制操作系统:引导扇区的实现

(https://www.cnblogs.com/jiqingwu/p/os_boot_sector.html)

1、实模式和保护模式

请参考:实模式和保护模式

(https://blog.csdn.net/qq_37653144/article/details/82818191)

现代CPU有三种工作模式:实模式、保护模式和虚拟8086模式。CPU在加电后自动进入16位实模式,在主引导记录MBR加载特定的操作系统后,由操作系通经过一些设置切换到32位保护模式。

以WIN7为例,首先由MBR载入启动管理器bootmgr,在bootmgr中进行实模式到保护模式的切换,随后装载winload.exe,然后通过winload.exe加载Windows7内核,从而启动整个Windows7系统。

2、实模式到保护模式的切换

从实模式切换到保护模式大致可以分为以下几个步骤:

1、屏蔽中断

2、初始化全局描述符表(GDT)

3、将CR0寄存器最低位置1

4、执行远跳转

5、初始化段寄存器和栈指针

1、屏蔽中断

在16位实模式下的中断由BIOS处理,进入保护模式后,中断将交给中断描述符表IDT里规定的函数处理,在刚进入保护模式时IDTR寄存器的初始值为0,一旦发生中断(例如BIOS的时钟中断)就将导致CPU发生异常,所以需要首先屏蔽中断。

屏蔽中断可以使用cli指令:

cli

2、初始化GDT

屏蔽中断后我们就可以正式开始切换到保护模式的过程了。首先我们要完成GDT的初始化,这也是整个切换过程中最复杂的一步。

32位保护模式下的地址计算方式和16位实模式不一样,16位保护模式支持20位的寻址,物理地址等于段地址 * 16 + 偏移地址,其中段地址可以通过段寄存器获得,段地址和偏移地址都是16位。

在32位保护模式中,段与段之间是互相隔离的,当访问的地址超出段的界限时处理器就会阻止这种访问,因此每个段都需要有起始地址、范围、访问权限以及其他属性四个部分,这四个部分合在一起叫做段描述符(Segment Descriptor),总共需要8个字节来描述。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit,显然我们无法用16-bit的段寄存器来直接存储64-bit的段描述符。

解决的办法是将所有64-bit的段描述符放到一个数组中,将16-bit段寄存器的值作为下标来访问这个数组(以字节为单位),获取64-bit的段描述符,这个数组就叫做全局描述符表(Global Descriptor Table, GDT)。

上面这个就是64位段描述符的具体结构,包括段起始地址Base,限长Limit,Access Byte和Flags都是访问权限,结构如下:

Access Byte和Flags各项的解释,我直接复制OS Dev)了,更详细的解释可以参考《x86汇编语言:从实模式到保护模式》这本书的11.3节:

Pr: Present bit. This must be 1 for all valid selectors.

Privl: Privilege, 2 bits. Contains the ring level, 0 = highest (kernel), 3 = lowest (user applications).

S: Descriptor type. This bit should be set for code or data segments and should be cleared for system segments (eg. a Task State Segment)

Ex: Executable bit. If 1 code in this segment can be executed, ie. a code selector. If 0 it is a data selector.

DC: Direction bit/Conforming bit.

Direction bit for data selectors: Tells the direction. 0 the segment grows up. 1 the segment grows down, ie. the offset has to be greater than the limit.

Conforming bit for code selectors:

  • If 1 code in this segment can be executed from an equal or lower privilege level. For example, code in ring 3 can far-jump to conforming code in a ring 2 segment. The privl-bits represent the highest privilege level that is allowed to execute the segment. For example, code in ring 0 cannot far-jump to a conforming code segment with privl==0x2, while code in ring 2 and 3 can. Note that the privilege level remains the same, ie. a far-jump form ring 3 to a privl==2-segment remains in ring 3 after the jump.
  • If 0 code in this segment can only be executed from the ring set in privl.

RW: Readable bit/Writable bit.

Readable bit for code selectors: Whether read access for this segment is allowed. Write access is never allowed for code segments.

Writable bit for data selectors: Whether write access for this segment is allowed. Read access is always allowed for data segments.

Ac: Accessed bit. Just set to 0. The CPU sets this to 1 when the segment is accessed.

Gr: Granularity bit,粒度位,如果Gr置0则段界限limit是以字节为单位的,即段的大小为1B到1MB,如果Gr置0则以4KB为单位,段的大小从4KB到4GB。

Sz: Size bit,置0表示该段的偏移地址或操作数是16位的,比如说代码段该位置0,则取指令时会使用16位的指令指针寄存器IP来取指令,而栈段在进行栈操作时,会使用SP寄存器;置1表示使用的偏移地址或操作数是32位的,使用EIP和ESP寄存器。

Flags区域中空余的2位其中一位是给64位CPU使用的,这里我们置为0即可,另一位是留给用户软件使用的,CPU并不会使用。

GDT可以通过LGDT指令加载,加载后GDT的地址和大小被保存到了GDTR寄存器:

 

LGDT指令的操作数为6字节的数据,2个低位字节为GDT的大小减一,减一的原因是Size的最大值是65535,而GDT最多可以有65536字节(最多8192项)。4个高位字节为GDT的32位地址。

我们可以在汇编代码中定义GDT并用LGDT指令加载GDT,然后让MBR将我们这一段汇编代码加载到内存中的某个地址,然后跳转到这个地址执行,这样就完成了GDT的初始化。

接下来我们就来看一下在汇编代码中定义GDT的方法。Intel处理器规定GDT中的第一个描述符必须是空描述符,这是因为寄存器和内存单元的初始值一般都是0,如果程序设计有问题,就会无意中用全0的索引来选择描述符,gdt_null的作用就有点像C语言中的空指针。用汇编代码定义如下:

gdt_start:; 第一个描述符必须是空描述符gdt_null:    dd 0    dd 0gdt_end:

dd 0定义了一个4字节的数据,两个dd 0正好是8字节,也就是一个描述符的大小。

接下来我们要定义代码段和数据段,为了方便,我们可以将代码段和数据段的起始地址都设为0,大小都设为4GB,这种设置方式也叫作平坦模式。现在的定义如下:

gdt_start:; 第一个描述符必须是空描述符gdt_null:    dd 0    dd 0; 代码段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 数据段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end:

此时代码段和数据段的访问权限和其他属性如下:

代码段:Access byte 10011010bPr = 1        ; 标记为有效的段Privl = 0    ; ring3权限S = 1        ; 代码段或数据段Ex = 1        ; 可执行,即代码段DC = 0        ; 标记该段只能在Privl权限(即ring3权限)下执行RW = 1        ; 可读Ac = 0        ; 该段的内容是否被修改,默认为0,由CPU置位Flags 1100bGr = 1        ; 粒度为4KBSz = 1        ; 32位模式
数据段:Access byte 10010010bPr = 1        ; 标记为有效的段Privl = 0    ; ring3权限S = 1        ; 代码段或数据段Ex = 0        ; 不可执行,即数据段DC = 0        ; 标记该段只能在Privl权限(即ring3权限)下执行RW = 1        ; 可写Ac = 0        ; 该段的内容是否被修改,默认为0,由CPU置位Flags 1100bGr = 1        ; 粒度为4KBSz = 1        ; 32位模式

接着我们要定义栈段,由于我们的引导程序是加载到07c00h这个地址的,因此我们可以将07c00h以下的空间都作为栈空间,加入栈段后的定义如下:

gdt_start:; 第一个描述符必须是空描述符gdt_null:    dd 0    dd 0; 代码段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 数据段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 栈段描述符gdt_stack:    dw 0x7c00 ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 01000000b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end:
栈段:Access byte 10010010bPr = 1        ; 标记为有效的段Privl = 0    ; ring3权限S = 1        ; 代码段或数据段Ex = 0        ; 不可执行,即数据段DC = 0        ; 标记该段只能在Privl权限(即ring3权限)下执行RW = 1        ; 可写Ac = 0        ; 该段的内容是否被修改,默认为0,由CPU置位Flags 1100bGr = 0        ; 粒度为1BSz = 1        ; 32位模式

接着我们定义6字节的GDT描述符:

; GDT descriptiorgdt_descriptor:dw gdt_end - gdt_start - 1 ; Size of our GDT, always less one of the true sizedd gdt_start ; Start address of our GDT

接着调用lgdt指令将GDT描述符加载到gdtr寄存器,整个GDT的初始化就算是完成了:

lgdt [gdt_descriptor]

3、将CR0最低位置1

CR0是系统内的32位控制寄存器之一,可以控制CPU的一些重要特性。其中最低位是保护允许位(Protected Mode Enable, PE),PE位置1后CPU进入保护模式(注意此时还是16位保护模式,不是32位保护模式),置0时则为实模式。现在我们要进入保护模式,即将CR0的最低位置1,汇编代码如下:

; 把 cr0 的最低位置为 1,开启保护模式mov eax, cr0or eax, 0x1mov cr0, eax

有关CR0其他位的功能以及其他控制寄存器可以查看Control register,这里不再赘述。

4、执行远跳转

将cr0最低位置1后,CPU就进入了保护模式,此时需要马上执行一条远跳转指令:

jmp 08h:PModeMain

这条指令有两个作用,第一个作用是将cs段寄存器的值修改为08h,切换到保护模式后,CPU寻址的方式就从实模式中的段地址 * 16 + 偏移地址改为了通过gdt寻址,所以这里的08h是段选择子而不是段地址,并且远跳转指令会自动将cs的值修改为对应的段选择子,这里是08h。

之前提到过,段描述符中的Sz位确定操作数的默认大小,将代码段寄存器切换到08h后,代码段描述符的Sz位为1,即32位模式,此时才真正进入了32位保护模式。

远跳转的另一个作用是清空CPU的流水线,流水线的作用在计组中有提到过,为了加速指令的执行,CPU在执行当前指令时会同时加载并解析接下来的一些指令,在进入保护模式之前,已经有许多指令进入了流水线,这些指令都是按16位模式处理的,而进入保护模式后的指令都是32位,所以这里通过一个远跳转来让CPU清空流水线。

切换到32位模式后,就应该执行32位的指令了,所以从PModeMain开始的指令都采用32位模式编译,通过[bits 32]这个标记实现:

[bits 32]PModeMain:

5、初始化段寄存器和栈指针

上一步中我们将代码段寄存器cs初始化成了0x08,现在我们还需要初始化其他的段寄存器如数据段寄存器ds,拓展段寄存器es,栈段ss以及fs,gs两个由操作系统使用的段。

另外我们还需要初始化栈指针ebp和esp,代码如下:

[bits 32]PModeMain:    mov ax, 0x10        ; 将数据段寄存器ds和附加段寄存器es置为0x10    mov ds, ax             mov es, ax    mov fs, ax          ; fs和gs寄存器由操作系统使用,这里统一设成0x10    mov gs, ax    mov ax, 0x18        ; 将栈段寄存器ss置为0x18    mov ss, ax    mov ebp, 0x7c00     ; 现在栈顶指向 0x7c00    mov esp, ebp

此时我们就可以开始执行32位保护模式下的代码了。

3、测试:32位保护模式下的打印

接下来我们要实现的是32位保护模式下的打印功能,测试我们是否已经成功进入32位保护模式了。

在32位保护模式中我们无法使用实模式下的BIOS中断指令进行打印,但是天无绝人之路,我们仍然可以通过直接修改显存来进行打印。

显卡包括显存本身是外围设备,但CPU很友好的把显存映射到了0x8B000这个物理地址,因此我们可以直接通过mov指令来访问显存。在文本模式下,被打印的字符在显存中总共占2个字节,第一个字节是字符的ASCII码,第二个字节是字符的颜色属性,这里与BIOS中int 10调用的颜色属性是一样的:

打印字符串的代码我们可以写成一个函数,具体的内容大家看下面的代码和注释:

; 打印字符串; @param 被打印的字符串print:    push ebp    mov ebp, esp    mov ebx, [ebp+8]    ; [ebp+8]即传入的字符串参数    xor ecx, ecx    mov ah, 0x0f        ; ah为打印的颜色属性,0x0f为白字黑底    mov edx, 0xb8000    ; 显存的地址loop1_begin:    mov al, [ebx]       ; al为被打印的字符    cmp al, 0           ; 若al为0,结束打印    je loop1_end    mov [edx], ax       ; 向显存中写入字符及其颜色属性(2字节)    inc ebx    add edx, 2    jmp loop1_beginloop1_end:    mov esp, ebp    pop ebp    ret

函数调用:

PModeTest:    push TEST_STRING             ; 被打印字符的地址    call printPModePause:    hlt    jmp PModePause TEST_STRING db 'We are in protected mode!', 0

效果如下:

4、完整代码

完整的代码,需要在引导程序中加载这段代码到正确的位置,这里是09000h,或者直接把org 09000h改成org 07c00h,把这段代码写到主引导扇区:

org 09000h cli                     ; 屏蔽中断 lgdt [gdt_descriptor]   ; 初始化GDT ; 把 cr0 的最低位置为 1,开启保护模式mov eax, cr0or eax, 0x1mov cr0, eax jmp 08h:PModeMain [bits 32]PModeMain:    mov ax, 0x10        ; 将数据段寄存器ds和附加段寄存器es置为0x10    mov ds, ax             mov es, ax    mov fs, ax          ; fs和gs寄存器由操作系统使用,这里统一设成0x10    mov gs, ax    mov ax, 0x18        ; 将栈段寄存器ss置为0x18    mov ss, ax    mov ebp, 0x7c00     ; 现在栈顶指向 0x7c00    mov esp, ebp    jmp PModeTest PModeTest:    push TEST_STRING             ; 被打印字符的地址    call printPModePause:    hlt    jmp PModePause ; 打印字符串; @param 被打印的字符串print:    push ebp    mov ebp, esp    mov ebx, [ebp+8]    ; [ebp+8]即传入的字符串参数    xor ecx, ecx    mov ah, 0x0f        ; ah为打印的颜色属性,0x0f为白字黑底    mov edx, 0xb8000    ; 显存的地址loop1_begin:    mov al, [ebx]       ; al为被打印的字符    cmp al, 0           ; 若al为0,结束打印    je loop1_end    mov [edx], ax       ; 向显存中写入字符及其颜色属性(2字节)    inc ebx    add edx, 2    jmp loop1_beginloop1_end:    mov esp, ebp    pop ebp    ret TEST_STRING db 'We are in protected mode!', 0 gdt_start:; 第一个描述符必须是空描述符gdt_null:    dd 0    dd 0; 代码段描述符gdt_code:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10011010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 数据段描述符gdt_data:    dw 0xffff ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 11001111b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31); 栈段描述符gdt_stack:    dw 0x7c00 ; Limit (bits 0-15)    dw 0x0 ; Base (bits 0-15)    db 0x0 ; Base (bits 16-23)    db 10010010b ; Access Byte    db 01000000b ; Flags , Limit (bits 16-19)    db 0x0 ; Base (bits 24-31)gdt_end: ; GDT descriptiorgdt_descriptor:dw gdt_end - gdt_start - 1 ; Size of our GDT, always less one of the true sizedd gdt_start ; Start address of our GDT

5、A20地址线

A20的“A”是“Address”的首字符,A20也就是第21根地址线的意思。这里涉及到一个历史遗留问题,但如果我们是在Bochs这样的模拟器上进行调试的话没有影响,所以我把这个问题放到了最后来讲。

在8086处理器上只有20根地址线,最多支持20位的地址,因此每当物理地址达到最高端0xFFFFF时,再加上1又会绕回最低端0x00000。为了兼容地址回绕这个特性,后来生产的CPU都会默认关闭第21根地址线,需要操作系统在进入保护模式时手动开启。

不过某些BIOS,包括模拟器(qemu, Bochs等)会默认帮我们打开A20地址线,所以我们不需要手动开启了,我们可以用一段程序来测试A20地址线是否打开:

PModeTest:    mov edi, 0x112345    mov esi, 0x012345    mov [esi], esi    mov [edi], edi    mov eax, [esi]    cmp eax, edi    je A20_DISABLE    push aA20_ENABLE    call print    jmp PModePauseA20_DISABLE:    push aA20_DISABLE    call printPModePause:    hlt    jmp PModePause aA20_ENABLE db 'A20 Enable', 0aA20_DISABLE db 'A20 Disable', 0

测试结果,可以看到在Bochs中A20地址线确实是默认开启的:

从80486处理器开始,0x92端口的第2位用来控制A20地址线,将0x92端口的第2位置1即可开启A20地址线,如果要手动开启A20地址线的话,可以使用以下代码:

in al, 92hor al, 00000010bout 92h, al


保护模式segment
本作品采用《CC 协议》,转载必须注明作者和本文链接
在x86系统下,总说一个进程有4GB空间,那么按照这个说法来说,在windows上起一个进程就要占用4GB空间,两个进程就要占用8GB空间,但是实际上是我们电脑的物理内存往往只有8GB,16GB多一点的可能有32GB,我们却启动了几十个进程,这显然是矛盾的。
这篇文章的内容是我在研究自制操作系统时遇到的一个问题,即如何从16位实模式切换到32位保护模式,网上的资料比较少,讲得也不是很详细,在这跟大家分享一下我的解决方案。
最近,QEMU 的一个bug导致 kexec 之后的内核崩溃,因为 kexec-ed 内核将无法解压缩压缩的 initrd。此错误仅影响在 QEMU 中使用压缩内联的新 Linux 内核。物理机器上的 kexec,以及在 QEMU 中使用压缩的 initrd 引导 Linux 内核的其他方法,均不受影响。当然,在 QEMU 中,使用未压缩的initrd来创建新Linux内核是有效的,因此,如果您想在 QEMU中使用基于kexec的引导加载程序,则可能必须使用未压缩的initrd才能引导目标系统。
近日,苹果公司在官网发布公告称正在评估iPhone上的一项突破性安全措施——隔离模式(Lockdown Mode,又称锁定模式),为极少数面临高级间谍软件和针对性网络攻击风险的用户提供一种极端的保护模式
在全国范围率先推出《设置指南》,是上海网信办探索加强未成年人网络保护的一项创新之举,用于指导上海属地重点网络平台率先高标准、严要求地设置“青少年模式”,为未成年人绿色上网提供更好的服务保障。《上海市未成年人网络保护行动倡议书》发布
新的“Migo”恶意软件针对Linux服务器,利用Redis进行加密劫持。使用用户模式​​Rootkit隐藏其活动,使检测变得困难。保护您的Redi 服务器并保持警惕!
写这篇,最开始是无聊。后面想想在windows三环直接操控VmWare里面WIN10的内存,这不就相当于实现了一套简易版的windows内存解析,对学习和了解内存是非常方便的事情,甚至说对了解整个windows内核都是一个有益的事情。具体原理就直接看代码吧,里面的注释写的比较完善。所以,我走了一个捷径:关闭win10的分页文件,重启!后续完成了上面的NTFS解析后再来测试这一块。未完成的地方,我都写了断点。
推动移动智能终端未成年人保护相关团体、行业标准上升为国家标准。《答复》显示,此前,工信部已加强手机等移动智能终端技术规范引导。同时,工信部还引导软件企业加强未成年网络保护功能开发。同时,立足电信行业监管,加强计算机、手机终端应用软件的安全威胁监测和漏洞管理,严肃查处违法违规企业,促进软件产业的健康持续发展。
0x00 简要说明 百度百科:Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 Redis因配置不当可导致攻击者直接获取到服务器的权限。 利用条件:redis以root身份运行,未授权访问,弱口令或者口令泄露等
VSole
网络安全专家