第五章-保护模式进阶

操纵系统进入保护模式后,OS需要设置虚拟内存、内存管理等功能,这些软件的工作离不开硬件的支持

Ⅰ.获取物理内存容量

Linux支持多种方式确定内存容量,例如detect_memory函数通过BIOS的0x15中断号的三个子功能,每个子宫内功能号需要存在EAX或AX中。

  • EAX=0xE820:遍历主机上全部内存。
  • AX=0xE801: 分别检测低 15MB和 16MB~4GB 的内存,最大支持 4GB 。
  • AH=0x88:最多检测出 64MB 内存,实际内存超过此容量也按照 64MB 返回。
1.利用 BIOS 中断0x15 子功能 0xE820 获取内存

BIOS 中断0x15 子功能 0xE820 能够遍历所有的内存,返回值为地址范围段描述符ARDS,取BaseAddrLow+LengthLow 为求得的内存容量。ADRS根据内存类型Type[OS可用/不可用]划分,将遍历的内存信息保存在20个字节的地址范围描述符中,总共包含以下几个参数

在这里插入图片描述

调用0x15此子功能的步骤为:

  1. 设置调用前输入寄存器
  2. 调用中断int 0x15
  3. 在CF位为0的情况下,读取输出地址范围描述符

在这里插入图片描述
在这里插入图片描述

(1)执行中断前后的汇编指令过程:

根据上述e820访存的过程编写汇编过程

;init params
ards_nr dw 0		;count the num of ards
ards_buf 244 db 0	;ards buffer
;------------init input regs------------
mov edx,0x534d4150
mov di,ards_buf		;di point to ards_buf
xor ebx,ebx			;clear

.e820_mem_get_loop:
	mov ecx,20
	mov eax,0x0000E820	;every call back will change eax to 0x534d4150,so each loop need to reinit eax
	int 0x15				; get the mem by interrupt
	jc .e820_failed_so_try_e801						; if carry == 1 then jump,when carry =1 means error occurred.
	inc word [ards_nr]		;ards num++
	add di,cx	;ards buffer addr + 20					; divide mem into 4B 
	cmp ebx,0	;if CF=0 & ebx=0,meet the last ards
	jnz .e820_mem_get_loop	

自增assembly指令inc a 相当于 add a,1

(2)根据返回结果获取内存容量

地址范围段描述符ARDS,取BaseAddrLow+LengthLow 为求得的内存容量,依此遍历所有ards_buf,计算最大的内存容量

mov cx, [ards_nr]	;cx as count reg
xor edx,edx			;edx reg as temp reg
mov ebx,[adrs_buf]	;ebx point to ards_buf addr
.find_max_mem_area:
	mov eax,[ebx]
	add eax,[ebx+8]
	add ebx,20
	cmp edx,eax		;冒泡排序,找出最大 , edx 寄存器始终是最大的内存容量
	jge	.next ards		;有符号大于等于则跳转
	mov eax,edx

.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok	

jge指令表示有符号数字大于等于则跳转

2.利用 BIOS 中断 0x15 子功能 0xe801 获取内存

BIOS 中断 0x15 子功能 0xe801最多只能读取4GB的内存,即最多支持32位地址总线,返回结果保存在两组寄存器中。 低于 15MB的内存以 IKB 为单位大小来记录,单位数量在寄存器 AX 和 CX 中记录,其中 AX 和 CX 的值是一样的,所以在 15MB 空间以下的实际内存容量=AX*1024。 AX 、CX最大值为 0x3c00,即0x3c00*1024=15MB。 16MB~4GB 是以 64KB为单位大小来记录的,单位数量在寄存器 BX 和 DX 中记录,其中 BX 和 DX 的值是一样的,所以 16MB 以上空间的内存实际大小=BX*64*1024.

在这里插入图片描述

此中断的调用步骤如下。

  1. 将 AX 寄存器写入 0xE801 。
  2. 执行中断调用 int 0x15 。
  3. 在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器便会有对应的结果。
(1)执行中断前后的汇编指令过程:
.e820_failed_so_try_e801:
	mov ax,0xE801
	int 0x15
	jnz .e801_failed_so_try_88

jnz命令,如果CF寄存不为0,则跳转

(2)根据返回结果获取内存容量
;获取ecx的内存容量,以Byte为单位	
mov cx,0x400		;1KB换算成Byte,cx/ax*1024
mul cx				;16为操作数乘法,积的高 16 位在 DX 寄存器,低 16 位在 AX 寄存器
shl edx,16			;dx的0-15位移至16-31位
and eax,0x0000FFFF	;高位清零
or edx,eax
add edx,0x100000	;加上1MB
mov esi,edx			;因为乘法运算需要借助dx和ax存储运算结果,因此需要将edx结果保存

;获取ebx的内存容量,以Byte为单位
xor eax,eax
mov ax,bx			;操作数bx放入ax寄存器,为了后面的mul操作
mov ecx,0x10000		;64KB换算成Byte
mul ecx				;32为操作数乘法,积的高 32 位在 EDX 寄存器,低 32 位在 EAX 寄存器
;该寻址方法最大就是4GB,也就是32位,因此可以直接用EAX计算

add esi,eax
mov edx,esi			;edx 为总内存大小
jmp .mem_get_ok

两个同位宽的寄存器相加可以用or

3.利用 BIOS 中断 0x15 子功能 0x88 获取内存

只能识别最大 64MB的内存

在这里插入图片描述

中断返回后, AX 寄存器中的值,其单位是 1KB。此中断的调用步骤如下

  1. 将 AX 寄存器写入 0x88 。
  2. 执行中断调用 int 0x15 。
  3. 在 CF 位为 0 的情况下,CF和AX寄存器便会有对应的结果。
(1)执行中断前后的汇编指令过程:
.e801_failed_so_try_88:
	mov ah,0x88
	int 0x15
	jc .error_ht

jc指令表示为1则跳转

(2)根据返回结果获取内存容量
	mov cx,0x400		;变1KB为Byte
	mul cx				;16为操作数乘法,积的高 16 位在 DX 寄存器,低 16 位在 AX 寄存器
	shl edx,16
	or edx,eax
	add edx,0x100000		;+1MB

Ⅱ.启动内存分页机制,畅游虚拟空间

1.分段内存管理的缺陷

早期采用内存分段方式管理内存,但是当内存容量较小,且运行的进程需要频繁切换,可能会出现最大空闲空间小于待装载的进程空间,造成阻塞情况发生,如下图。

image-20230714213349622

针对这个问题提出两种解决方案:

(1)等待满足待执行进程空间要求的空闲内存出现。pass掉,系统实时性影响较大

(2)根据内存中进程的驻存时间长短撤出进程块,实现动态装载。

CPU如何实现内存段的动态装载呢?

为保证撤出内存的段可以在需要时被调回内存,因此需要在段对应的段描述符表(GDT/LDT)中加入控制信息,记录段的位置以及访问信息,即P——是否在内存中,A——是否访问过。

(1)若 P 位为 1 ,表示该段在内存中存在。访问过该段后, CPU 将段描述符中的 A 位置 1 ,表示CPU近来刚访问过该段。

(2)若 P 位为 0,说明内存中并不存在该段,这时候 CPU 将会抛出个 NP (段不存在)异常,转而去执行中断描述符表中 NP 异常对应的中断处理程序,此中断处理程序是操作系统负责提供的,该程序的工作是将相应的段从外存(比如硬盘)中载入到内存,并将段描述符的 P 位置 1 ,中断处理函数结束后返回, CPU 重复执行这个检查,继续查看该段描述符的 P 位,此时已经为 1 了,在检查通过后,将段描述符的 A 位置 1

内存段是何时移出到外存上的呢?

CPU统计每个段的在周期内A位置1的次数,即内存段的使用频率,故此处要求每次用完内存段以后需要将置为1后的A位清零。当物理内存不足时,可以将使用频率最低的段换出到硬盘,以腾出内存空间给新的进程。

内存缺段时如何调回内存中的呢?

当需要访问内存段时,重新开启NP异常,中断处理程序加载、段描述符P位置1,A位置1。

但是,分段系统存在明显缺陷:当内存特别小,无法容纳任何一个内存段,或者频繁置换内存段,导致系统响应时间很长。针对上述问题,本质原因是,编译器对分段内存管理方式的线性地址分配时连续的,与线性地址一一对应的物理地址也要求时连续的。故考虑若可以建立连续线性地址和非连续物理地址之间的映射关系,那么就可以解决内存段置换的问题啦。

2.开启分页篇章

分页内存管理**通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续 。**因为访存速度决定了CPU的运行速度,因此页表存储在硬件中,查找页表的工作也是交由硬件完成。

(1)一级页表

分页机制是建立在分段机制的基础上的,将大小不等的段拆分成大小相等的页,首先通过段基地址+偏移地址得到线性地址,判断是否开启分页模式,如果分页,通过页表项检索页表,得到逻辑地址;否则通过分段模式访存。每个页表项均为4Byte大小,每个页表最多容纳1M的页表项,因此标准页的大小为1MB。

在这里插入图片描述

分页机制的作用有两方面

  • 将连续线性地址转换成不连续的物理地址。
  • 用大小相等的页代替大小不等的段。

在这里插入图片描述
分页机制如何找到访存地址
在这里插入图片描述

传统的映射关系是线性地址和物理地址一一映射,当存在4G的页表数,32位的地址需要4Byte的页表项存储,那么页表占用的内存为4G*4Byte=16G。

分页机制的原理:一个页表项对应一个页,所以,用线性地址的高 20 位作为页表项的索引,每个页表项要占用 4 字节大小,所以这高 20 位的索引乘以 4 后才是该页表项相对于页表物理地址的字节偏移量。用 cr3 寄存器中的页表物理地址加上此偏移量便是该页表项的物理地址,从该页表项中得到映射的物理页地址,然后用线性地址的低 12 位与该物理页地址相加,所得的地址之和便是最终要访问的物理地址。

(2)二级页表

既然有了一级页表,为什么还要搞个二级页表呢?

1)1级页表项大小为4B,最多可容纳1M个页表项,因此最多占用4MB。

2)一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 IGB,用户进程要占用低 3GB 。

3)每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。

因此我们期望:不要一次性地将全部页表项建好,需要时动态创建页表项

也就是二级页表:无论是几级页表,标准页的尺寸都是 4M,这一点是不变的。所以 4GB 线性地址空间最多有 1M 个标准页 。 一级页表是将这 1M 个标准页放置到一张页表中,二级页表是将这 1M 个标准页平均放置 1K 个页表中 。 每个页表中包含有 1K 个页表项。
在这里插入图片描述
在这里插入图片描述

标准页大小是 4KB,故地址都是 4K 的倍数,因此32位地址线上有12位是空闲的,可以用来作为标志位记录页表使用信息。

总结启用分页机制,我们要按顺序做好三件事 。

(1 )准备好页目录表及页表。
(2 )将页表地址写入控制寄存器 cr3 。【cr0用来开启保护模式】
(3 )寄存器 cr0 的 PG 位置 1 。 【是否启用分页模式】

分页会产生内部碎片,分段会产生外部碎片

step1:配置CR3寄存器
在这里插入图片描述

要求页目录的起始地址是 4KB的整数倍 ,设置CR3寄存器的[31:12]位为物理地址的高20位,CR3的[11:0]中除了PCD和PWT两个和高速缓存相关的设置之外,其余位都没用,此处置0即可,那么仅需把页目录表物理地址的高 20 位写入 cr3 寄存器即可

step2:配置CR0的PG位为1

启动分页机制的开关是将控制寄存器 cr0 的 PG 位置 1, PG 位是 cr0寄存器的最后一位:第 31 位 。

(3)规划页表之操作系统与用户进程的关系

规划页表本质是规划内存布局,但保护模式为用户程序和操做系统分配了不同的特权级,用户程序运行本质是用户程序+操作系统的部分功能完成的,因此必须支持用户进程和操作系统共享。为了保证每个用户程序都能共享操作系统,因此将操作系统放在用户虚拟内存的高地址3G开始的位置,比如Linux,它就运行在虚拟地址的 3GB 以上,其他用户进程都运行在 3GB 以下。 。

为了实现共享操作系统,让所有用户进程 3GB~4GB 的虚拟地址空间都指向同一个操作系统,也就是所有进程的虚拟地址 3GB~4GB 本质上都是指向的同一片物理页地址,这片物理页上是操作系统的实体代码。实现起来也比较容易,只要保证所有用户进程虚拟地址空间 3GB~4GB 对应的页表项中所记录的物理页地址是相同的就行啦。

(4)启用分页机制

在这里插入图片描述

(1)设置页目录表、页表、页表项

比较复杂,有时间研究!!!

(2)重新设置GDT表,将GDT表放在虚拟地址的3G-4G空间内,即将原始GDT表段基地址+0xc0000000

;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
sgdt [gdt_ptr]

;将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx,[gdt_ptr+2]				
or dword [ebx+0x18+4],0xc0000000		;视频段是第3个段描述符,每个描述符是8字节,故0x18。
					      ;段描述符的高4字节的最高位是段基址的31~24位

;将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000

add esp, 0xc0000000        ;将栈指针同样映射到内核地址

(3)设置CR3和CR0寄存器

;setting CR3 addr
mov eax, PAGE_DIR_TABLE_POS  
mov cr3,eax

;setting CR0 PG = 1
mov eax,cr0
or eax,0x80000000
mov cr0,eax

(4)重新加载GDT表

lgdt [gdt_ptr]

Ⅲ.加载内核

之前在操作系统下编写用户程序,可以用标准库调用函数,其实标准库本质是系统调用。

1.二进制程序的运行方法

用户程序运行依赖操作系统,那么操作系统如何加载用户程序的呢?

(1)知道用户程序的入口地址

为了保证操作系统能从程序中自动提取入口地址,因此在用户程序中加入文件头保存用户的入口地址、程序内存容量等信息,即目前用户程序的构成:文件头+程序体

在这里插入图片描述

CPU只能执行纯粹的二进制可执行文件,但加入程序头的程序程序就不再是纯粹的二进制可执行文件了,因此在加载到内存后提取程序头信息后,跳转到程序体才可以执行

(2)使用jmp/call指令跳转

2.ELF格式的二进制文件

用户程序中含有多个段segment和多个节section,段包括数据段、代码段,每个段由节构成,段和节的关系可视为一个函数,以及构成函数的每条语句。每个段、节对应一个段文件头、节文件头,由段文件头表和节文件头表分别对段文件头和节文件头统一管理。由于不同段的大小、数量,以及节的大小、数量不同,因此描述段、节信息的文件头表的存储位置具有一定的随机性,为保证CPU执行程序时可以快速读取段、节的文件头信息,提出使用具有固定地址的ELF管理段文件头和节文件头,通常位于文件最开始的部分。

ELF 格式的作用体现在两方面, 一是链接阶段,另一方面是运行阶段 :
在这里插入图片描述

下图为ELF头的组成成员:
在这里插入图片描述
其中,e_ident数组成员构成:
在这里插入图片描述
ELF在链接和运行中使用,链接时为保证兼容性,必须指定程序的位数和编码格式,如32位、大端字节序等。e_ident[5]大小端字节序,和大家分享一个技巧,用 file 命令就能够查看到 elf格式的可执行程序是 LSB,还是 MSB 。

3.将内核载入内存

内核被加载到内存后, loader 还要通过分析其 elf 结构将其展开到新的位置,所以说,内核在内存中有两份拷贝,一份是 elf格式的原文件 kernel.bin,另一份是 loader 解析 elf格式的 kernel.bin 后在内存中生成的内核映像(也就是将程序中的各种段 segment 复制到内存后的程序体),这个映像才是真正运行的内核 。

Ⅳ.特权级

1.特权级分级

内核只信任自己的代码,因此内核代码的特权级为最高级0,系统程序为特权级1,2,最后为用户特权级为最低级3。

2.任务状态段——TSS
(1)定义

任务就是一段在处理器上运行的程序,TSS (task state segment),作为任务在处理器中的唯一标识符。起初,在没有操作系统时,任务完整的部署在处理器中;当加入了操作系统后,程序被分成了用户程序和操作系统内核程序,这两部分加一起才是让处理器运行的完整的程序,因此一个用户程序在执行时需要进行这两种特权级切换用户程序的特权切换本质上时处理器的特权切换,特权切换会涉及到保护现场和恢复现场,即栈操作。因此讨论在处理器特权级切换时,主要用到了那些栈,每个栈需要保存的主要参数。

(2)TSS特权级切换过程

首先,每个任务的每个特权级下只能有一个栈,特权级总共包含4级,正常来说,一个程序应该存在4个栈,但是可以看到TSS中只有3个栈:SS0和esp0、SS1和esp1、SS2和esp2,它们分别代表0级段选择子和偏移量、1级段的段选择子和偏移量、 2 级段的段选择子和偏移量。那么为什么没有3级呢?
在这里插入图片描述

那就需要分析TSS的三个栈的功能了。对处理器来说,只有高级和低级两种特权转换,一个用户程序是从低特权级开始执行,穿插着高特权级和低特权级转换,最后一定会回到低特权级

  • 低特权级->高特权级

    通过中断门、调用门完成。

    需要开发人员提前将高特权级地址写入TSS中,以便处理器在特权级切换时,从TSS中取出地址存入SS和ESP中。

  • 高特权级->低特权级

    只能通过返回指令完成

    由于处理器最后的执行目标是用户程序,需要做的就是在经过高特权级处理之后,能够跳转回用户程序继续执行,索性,在经过最后的一个返回指令后,一定会跳转到低优先级的。

因此可以解释为什么TSS只有三个特权级栈了。但是三个特权级栈对于不同任务不是全使用的,一个任务可有拥有的栈的数量取决于当前特权级是否还有进一步提高的可能,即取决于它最低的特权级别。 对于特权级为3的任务,只考虑比他特权级高的栈,那么他需要3个,0、1、2;对于特权级为1的任务,只考虑比他特权级高的任务,那么只需要0特权级的栈。

(3)如何访问TSS

TSS 是硬件支持的系统数据结构 ,和GDT表是相同的访问原理,GDT通过lgdt指令存入GDTR寄存器并访问,TSS则是通过TR(task register)寄存器访问

(3)获取TSS的过程

用户程序经过预处理、编译、汇编、链接后生成可执行文件,执行时,将文件加载到内存中,处理器通过CS:IP执行程序指令,指令中包含了特权级,因此段选择子结构中的特权级对应的就是CS寄存器的最后两位,不仅是程序的特权级,同时也是处理器特权级:
在这里插入图片描述

0-1位是特权级RPL/CPL(request/current privilege level)位,2位是表标志TI(table indicator)位,3-15位是段描述符Index位。

(4)一致性代码段

通过段描述符设置

特权级规定了低特权级不能访问高特权级的资源,高特权级不能主动跳转到低特权级。那么对于用户程序的代码段来说,只能实现平级转移,就是同特权级下的跳转,没办法跳转到比它低的,也没办法跳转到比它特权级高的。那么对于低特权级程序来说,处理器的特权级升高之后,程序想干什么就干什么,多少都觉得有点恐怖,有没有一种好办法,既执行高特权级代码段上的指令,又不提升特权级? 一种方式是利用一致性代码段。 本质上是跳转到≥自己特权级的代码段执行,自己的特权级并没有提高。

在段描述符中,如果该段为非系统段(段描述符的 S字段为 0 ),可以用 type字段中的 C 位来表示该段是否为一致性代码段。 C 为 1时则表示该段是一致性代码段, C 为 0 时则表示该段为非一致性代码段 。 上面所提到的代码段是非一致性代码段,所以只能平级转移 。
一致性代码段也称为依从代码段, Confonning,用来实现从低特权级的代码向高特权级的代码转移。一致性代码段是指如果自己是转移后的目标段,自己的特权级( DPL) 一定要大于等于转移前的 CPL,即数值上 CPL≥DPL,也就是一致性代码段的 DPL 是权限的上限,任何在此权限之下的特权级都可以转到此代码段上执行 。

缺陷:无脑跳转同样致命。

(5)门、调用门与 RPL 序

通过门描述符设置

门结构其实是含有程序起始地址的描述符,处理器通过访问门描述符可以实现特权级跨越。门描述符与段描述符不同,段描述符指向的是内存空间,门描述符指向的是一段程序。总共包含4种门结构:任务门结构、中断门结构、陷阱门结构、调用门结构。
在这里插入图片描述
任务门描述符可以放在 GDT、 LDT 和 IDT (中断描述符表)中,调用门可以位于 GDT 、 LDT 中,中断门和陷阱门仅位于 IDT 中。

  1. 调用门

call 和 jmp 指令后接调用门选择子为参数,以调用函数例程的形式实现从低特权向高特权转移,可用来实现系统调用 。 call 指令使用调用门可以实现向高特权代码转移, jmp 指令使用调用门只能实现向平级代码转移。

2.中断门

以 int 指令主动发中断的形式实现从低特权向高特权转移, Linux 系统调用便用此中断门实现。

L3. 陷阱门
以 int3 指令主动发中断的形式实现从低特权向高特权转移,这一般是编译器在调试时用。

  1. 任务门

任务以任务状态段 TSS 为单位,用来实现任务切换,它可以借助中断或指令发起。当中断发生时,如果对应的中断向量号是任务门,则会发起任务切换。也可以像调用门那样,用 call 或 jmp 指令后接任务门的选择子或任务 TSS 的选择子。

现代操作系统中调用门和任务门基本废弃不用,中断门一直保留使用至今,陷阱门提供给调试器使用。

真象还原进度总结

到目前为止,操作系统已实现了内核的功能,目前功能以及架构位:

1.mbr.s

MBR为引导程序,功能为装载内核加载器到内存中。位于硬盘0盘1道1扇区,大小为512Byte,且最后一个字节为0x55aa。磁盘访问方式包括两种CHS和LBA,CHS根据盘号、道号、扇区号访问磁盘,LBA地址从1开始编码,允许通过地址访问磁盘的任意位置。

上电的一瞬间,BIOS进行设备自检、初始化中断向量表、找到磁盘0盘1道1扇区,大小为521Byte,且最后一个字节为0x55aa的程序,开启LBA磁盘访问方式读取磁盘数据,将其放入内存0x7c00地址。具体需要操纵硬盘控制器的端口寄存器,设置24位的LBA地址、读写操作,读取设备状态寄存器等,向接口寄存器读写数据通过in/out指令

在这里插入图片描述

(1)MBR具体流程为:
  1. 硬盘读取操作接口寄存器地址表。
  2. 配置接口寄存器。包括设置硬盘读取地址的模式,即LBA/CHS地址,设置地址、设置读写模式、读取硬盘状态寄存器。
  3. 确认硬盘状态,放入/读数据。
(2)为什需要接口读取外设,如硬盘?

IO 接口是连接 CPU 与外部设备的逻辑控制部件 。为了解决CPU与外设处速度不一致(缓冲)、时序不一致、信号不一致(并行信号,串行信号),电平不一致(TTL电平,CMOS电平,范围不一致,直接读取可能会存在错读)

2.loader.s

loader为加载器,主要功能为加载内核到内存中。分为两步:1.从实模式过渡到保护模式;2.从分段模式过渡到分页模式

1.实模式->保护模式

(1)设置全局段描述符表GDT

(2)开启A20地址线,实模式的地址线只有20位,即0-19,因此地址线的第20位作为实模式和保护模式区别之一

(3)设置CR0寄存器的PE=1

Q1:LDT、GDT、段选择子如何配合工作的

(1)CPU从访问GDT表请求中取出段选择子,根据段选择子的特权级、访问GDT/LDT类别、段描述符表索引值,来检索GDT/LDT表。

(2)确定特权后,根据段描述符索引从GDT/LDT表中获取段基地址,结合段选择子的偏移地址获得物理地址,访存。

Q2:保护模式下出现了需要在内存中常驻的GDT,那么如何分配他的存储地址?

保护模式下地址线有32位,段基址需要用 32 位地址来表示 。

未完待续。。。。。。。。。。。。。

Q3:为什么进入保护模式还需要重新放置GDT表

GDT表只有一个,在loader.S中被创建后就不再改变,改变的是在内存的位置。

原因是实模式下只能访问低端 IMB 空间,所以 实模式的GDT 只能位于 IMB 之内。根据操作系统的实际情况,有可能需要把 GDT 放在其他的内存位置,所以在进入保护模式后,访问的内存空间突破了 IMB,可以将 GDT 放在合适的位置后再重新加载进来。 【保护模式将内核文件放入内存的0号地址】

3.main.c

将被编译为内核bin文件,放入虚拟内存的高3G地址。操作系统通过读取用户程序的段、节执行程序,但用户程序的段和节的数量不同、大小不同、地址不同,因此ELF统一管理用户程序的段和节。

文件内容为

int main(void){
	while(1);
	return 0;
}

此处无法调用标准库,因为标准库本质是系统调用,但是我们现在还没有操作系统。

我们现在想基于main.c文件,将其放入虚拟内存的内核空间中,即32位虚拟内存的高3G地址内。对C文件的编译当然需要用到GCC命令,但希望自己指定程序的地址,因此我们选择gcc -c -o main.o main.c命令获得待重定位文件(支持程序员自定义文件地址)。

-c 编译、汇编到目标文件,不进行链接
-o 将输出文件按指定文件名存储 

然后,通过ld命令,设置可执行文件的起始虚拟地址ld main.o -Ttext 0xc0001500 -e main -o kernel.bin

-Ttext 起始虚拟地址
-e entry
-o output_file_name

由loader将kernel.bin文件按照elf格式解析为映像文件,也就是操作系统的内核。

Q1:为什么内核的起始虚拟地址是0xc0001500

内核存放在32位虚拟内存的高3G地址内,考虑到loader.S的GDT表需要保存到虚拟内存的内核区,因此需要空出一定空间,因为loader.S最大不超过2000Byte,起始地址为0x900,因此最大可占用地址为0x900+2000 = 0x10d0,因此取整就是0x1500

4.特权级

在操作系统下,一个任务包含用户程序和它的系统调用【系统调用本质是将内核保护起来,只开放部分接口供用户程序访问,调用过程按照用户程序->系统程序->内核->系统程序->用户程序的特权级执行】,因此必然存在特权级切换,任务是在处理器上执行的,因此特权级切换本质上是给处理器的特权级切换

那么涉及特权级切换需要遵循的原则,如何保证切换前后的环境保存问题。因此引入TSS(任务状态段)来保存切换的栈地址,通过tr寄存器访问。任务状态段中含有3个栈空间信息,意味着最多可跳转到其他的3个特权级。从低优先级->高优先级可以通过中断门、调用门与TSS配合使用,需要开发人员提前将高特权级地址写入TSS中;从高特权级->低特权级只能通过返回指令。

新知识点的讲解思路

(1)结构是什么

(2)设置方式以及保存地址

的高3G地址内,考虑到loader.S的GDT表需要保存到虚拟内存的内核区,因此需要空出一定空间,因为loader.S最大不超过2000Byte,起始地址为0x900,因此最大可占用地址为0x900+2000 = 0x10d0,因此取整就是0x1500

4.特权级

在操作系统下,一个任务包含用户程序和它的系统调用【系统调用本质是将内核保护起来,只开放部分接口供用户程序访问,调用过程按照用户程序->系统程序->内核->系统程序->用户程序的特权级执行】,因此必然存在特权级切换,任务是在处理器上执行的,因此特权级切换本质上是给处理器的特权级切换

那么涉及特权级切换需要遵循的原则,如何保证切换前后的环境保存问题。因此引入TSS(任务状态段)来保存切换的栈地址,通过tr寄存器访问。任务状态段中含有3个栈空间信息,意味着最多可跳转到其他的3个特权级。从低优先级->高优先级可以通过中断门、调用门与TSS配合使用,需要开发人员提前将高特权级地址写入TSS中;从高特权级->低特权级只能通过返回指令。

新知识点的讲解思路

(1)结构是什么

(2)设置方式以及保存地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值