做者:chuyaoxinhtml
BIOS将经过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。git
提示:须要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,须要了解:github
没有学过汇编的我刚看到源码时,有点懵逼,因而,我首先查了很多关于汇编的小资料。express
Ucore中用到的是AT&T格式的汇编编程
在 AT&T 汇编格式中bootstrap
寄存器名要加上 '%' 做为前缀;数组
用 '$' 前缀表示一个当即操做数; 安全
将symbol的值设为expressionide
屏蔽系统中断函数
因为代码段在实模式下运行,因此要告诉编译器使用16位的模式编译
在x86汇编代码中,标号有惟一的名字加冒号组成。它能够出如今汇编程序的任何地方,并与紧跟其后的哪行代码具备相同的地址。
归纳的说 ,当程序中要跳转到另外一位置时,须要有一个标识来指示新的位置,这就是标号,经过在目标地址的前面放上一个标号,能够在指令中使用标号来代替直接使用地址。
目标操做数在源操做数的右边;
操做数的字长由操做符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操做数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);
一、关闭中断
二、A20 使能
三、全局描述符表初始化
四、保护模式启动
五、设置段寄存器(长跳转更新CS,根据设置好的段选择子更新其余段寄存器)
六、设置堆栈,esp 0x700 ebp 0
七、进入bootmain后读取内核映像到内存,检查是否合法,并启动操做系统,控制权交给它
CPU复位(reset)或加电(power on)的时候以实模式启动,处理器以实模式工做。在实模式下,内存寻址方式和8086相同,由16位段寄存器的内容乘以16(10H)当作段基地址,加上16位偏移地址造成20位的物理地址,最大寻址空间1MB,最大分段64KB。可使用32位指令。32位的x86 CPU用作高速的8086。在实模式下,全部的段都是能够读、写和可执行的。
实模式将整个物理内存当作分段的区域,程序代码和数据位于不一样区域,操做系统和用户程序并无区别对待,并且每个指针都是指向实际的物理地址。这样,用户程序的一个指针若是指向了操做系统区域或其余用户程序区域,并修改了内容,那么其后果就极可能是灾难性的。经过修改A20地址线能够完成从实模式到保护模式的转换。
实模式下,程序地址为真实的物理地址,能够访问任意地址空间,这样不一样进程可能访问到其它进程程序,形成严重错误。而保护模式下,程序地址为虚拟地址,而后由OS系统管理内存访问权限,这样每一个进程只能访问分配给本身的物理
内存空间,保证了程序的安全性。例如Linux系统地址访问采用分页机制,在加载程序时,由OS分配的进程能够访问的物理页空间,并设置了页目录项和页表项,才能保证程序正常运行。这样程序运行时地址间接地由OS进行管理,防止进程之间互相影响,所有由OS稳定性保证。
CR0是控制寄存器,其中包含了6个预约义标志,0位是保护容许位PE(Protedted Enable),用于启动保护模式。若是PE位置1,则保护模式启动,若是PE=0,则在实模式下运行。
关于CR0及其余控制寄存器的详细内容能够参考如下连接:https://blog.csdn.net/wyt4455/article/details/8691500
#include <asm.h> # Start the CPU: switch to 32-bit protected mode, jump into C. # The BIOS loads this code from the first sector of the hard disk into # memory at physical address 0x7c00 and starts executing in real mode # with %cs=0 %ip=7c00. .set PROT_MODE_CSEG, 0x8 # kernel code segment selector .set PROT_MODE_DSEG, 0x10 # kernel data segment selector .set CR0_PE_ON, 0x1 # protected mode enable flag # start address should be 0:7c00, in real mode, the beginning address of the running bootloader .globl start start: .code16 # Assemble for 16-bit mode cli # Disable interrupts cld # String operations increment # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment # Enable A20: # For backwards compatibility with the earliest PCs, physical # address line 20 is tied low, so that addresses higher than # 1MB wrap around to zero by default. This code undoes this. seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.1 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port seta20.2: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.2 movb $0xdf, %al # 0xdf -> port 0x60 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1 # Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to physical addresses, so that the # effective memory map does not change during the switch. lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 # Jump to next instruction, but in 32-bit code segment. # Switches processor into 32-bit mode. ljmp $PROT_MODE_CSEG, $protcseg .code32 # Assemble for 32-bit mode protcseg: # Set up the protected-mode data segment registers movw $PROT_MODE_DSEG, %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %fs # -> FS movw %ax, %gs # -> GS movw %ax, %ss # -> SS: Stack Segment # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00) movl $0x0, %ebp movl $start, %esp call bootmain # If bootmain returns (it shouldn't), loop. spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment gdt: SEG_NULLASM # null seg SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel gdtdesc: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
#注释:
#include <asm.h> asm.h头文件中包含了一些宏定义,用于定义gdt,gdt是保护模式使用的全局段描述符表,其中存储着段描述符。 3 # Start the CPU: switch to 32-bit protected mode, jump into C. # The BIOS loads this code from the first sector of the hard disk into # memory at physical address 0x7c00 and starts executing in real mode # with %cs=0 %ip=7c00. 此段注释说明了要完成的目的:启动保护模式,转入C函数。 这里正好说了一下bootasm.S文件的做用。计算机加电后,由BIOS将bootasm.S生成的可执行代码从硬盘的第一个扇区复制到内存中的物理地址0x7c00处,并开始执行。 此时系统处于实模式。可用内存很少于1M。 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector .set PROT_MODE_DSEG, 0x10 # kernel data segment selector 这两个段选择子的做用实际上是提供了gdt中代码段和数据段的索引 .set CR0_PE_ON, 0x1 # protected mode enable flag 这个变量是开启A20地址线的标志,为1是开启保护模式 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader .globl start start: 这两行代码至关于定义了C语言中的main函数,start就至关于main,BIOS调用程序时,从这里开始执行 .code16 # Assemble for 16-bit mode 由于如下代码是在实模式下执行,因此要告诉编译器使用16位模式编译。 cli # Disable interrupts cld # String operations increment 关中断,设置字符串操做是递增方向。cld的做用是将direct flag标志位清零,
这意味着自动增长源索引和目标索引的指令(如MOVS)将同时增长它们。 # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero ax寄存器就是eax寄存器的低十六位,使用xorw清零ax,效果至关于movw $0, %ax。 可是好像xorw性能好一些,google了一下没有获得好答案 movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment 将段选择子清零 # Enable A20: # For backwards compatibility with the earliest PCs, physical # address line 20 is tied low, so that addresses higher than # 1MB wrap around to zero by default. This code undoes this. 准备工做就绪,下面开始动真格的了,激活A20地址位。先翻译注释:因为须要兼容早期pc,物理地址的第20位绑定为0,因此高于1MB的地址又回到了0x00000. 好了,激活A20后,就能够访问全部4G内存了,就可使用保护模式了。 怎么激活呢,因为历史缘由A20地址位由键盘控制器芯片8042管理。因此要给8042发命令激活A20 8042有两个IO端口:0x60和0x64, 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60,done! seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.1 #发送命令以前,要等待键盘输入缓冲区为空,这经过8042的状态寄存器的第2bit来观察,而状态寄存器的值能够读0x64端口获得。 #上面的指令的意思就是,若是状态寄存器的第2位为1,就跳到seta20.1符号处执行,知道第2位为0,表明缓冲区为空 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port 发送0xd1到0x64端口 seta20.2: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.2 movb $0xdf, %al # 0xdf -> port 0x60 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1 到此,A20激活完成。 # Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to physical addresses, so that the # effective memory map does not change during the switch. 转入保护模式,这里须要指定一个临时的GDT,来翻译逻辑地址。这里使用的GDT经过gdtdesc段定义。
它翻译获得的物理地址和虚拟地址相同,因此转换过程当中内存映射不会改变 lgdt gdtdesc 载入gdt movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 打开保护模式标志位,至关于按下了保护模式的开关。cr0寄存器的第0位就是这个开关,经过CR0_PE_ON或cr0寄存器,将第0位置1 # Jump to next instruction, but in 32-bit code segment. # Switches processor into 32-bit mode. ljmp $PROT_MODE_CSEG, $protcseg 因为上面的代码已经打开了保护模式了,因此这里要使用逻辑地址,而不是以前实模式的地址了。 这里用到了PROT_MODE_CSEG, 他的值是0x8。根据段选择子的格式定义,0x8就翻译成: INDEX TI CPL 0000 0000 1 00 0 INDEX表明GDT中的索引,TI表明使用GDTR中的GDT, CPL表明处于特权级。 PROT_MODE_CSEG选择子选择了GDT中的第1个段描述符。这里使用的gdt就是变量gdt。
下面能够看到gdt的第1个段描述符的基地址是0x0000,因此通过映射后和转换前的内存映射的物理地址同样。
.code32 # Assemble for 32-bit mode protcseg: # Set up the protected-mode data segment registers movw $PROT_MODE_DSEG, %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %fs # -> FS movw %ax, %gs # -> GS movw %ax, %ss # -> SS: Stack Segment 从新初始化各个段寄存器。 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00) movl $0x0, %ebp movl $start, %esp call bootmain 栈顶设定在start处,也就是地址0x7c00处,call函数将返回地址入栈,将控制权交给bootmain # If bootmain returns (it shouldn't), loop. spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment gdt: SEG_NULLASM # null seg SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel gdtdesc: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
// 用来定义段描述符的宏
#ifndef __BOOT_ASM_H__
#define __BOOT_ASM_H__
// assembler macros to create x86 segments
// 定义了一个空段描述符
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
// 以type,base,lim为参数定义一个段描述符, 其中的0xC0=(1100)2, 其
// 中的第一个1对应于段描述符中的G位,置1表示段界限以4KB为单位
// 第二个1对应于段描述符的D位,置1表示这是一个保护模式下的段描述符
// 具体的关于段描述符的格式定义在mmu.h中
// The 0xC0 means the limit is in 4096-byte units
// and (for executable segments) 32-bit mode.
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
// 可执行段
#define STA_X 0x8 // Executable segment
// 非可执行段
#define STA_E 0x4 // Expand down (non-executable segments)
// 只能执行的段
#define STA_C 0x4 // Conforming code segment (executable only)
// 可写段可是不能执行的段
#define STA_W 0x2 // Writeable (non-executable segments)
// 可读可执行的段
#define STA_R 0x2 // Readable (executable segments)
// 代表描述符是否已被访问;把选择字装入段寄存器时,该位被标记为1
#define STA_A 0x1 // Accessed
首先说明一点,这是一个历史遗留问题。
1981年8月,IBM公司最初推出的我的计算机IBM PC使用的CPU是Inter 8088.在该微机中地址线只有20根。在当时内存RAM只有几百KB或不到1MB时,20根地址线已经足够用来寻址这些 内存。其所能寻址的最高地址是0xffff,
也就是0x10ffef。对于超出0x100000(1MB)的寻址地址将默认地环绕到0xffef。当IBM公司与1985年引入AT机时,使用的是Inter 80286 CPU,具备24根地址线,最高可寻址16MB,而且有一个与8088那样实现地址寻址的环绕。
可是当时已经有一些程序是利用这种环绕机制进行工做的。为了实现彻底的兼容性,IBM公司发明了使用一个开关来开启或禁止0x100000地址比特位。因为当时的8042键盘控制器上刚好有空闲的端口引脚(输出端口P2,引脚P21),
因而便使用了该引脚来做为与门控制这个地址比特位。该信号即被称为A20。若是它为零,则比特20及以上地址都被清除。从而实现了兼容性。
当A20地址线控制禁止时,程序就像运行在8086上,1MB以上的地址是不可访问的,只能访问奇数MB的不连续的地址。为了使能全部地址位的寻址能力,必须向键盘控制器8082发送一个命令,键盘控制器8042会将A20线置于高电位,使所有32条地址线可用,实现访问4GB内存。
控制 A20 gate 的方法有 3 种:
1.804x 键盘控制器法
2.Fast A20 法
3.BIOS 中断法
ucore实验中用了第一种 804x 键盘控制器法,这也是最古老且效率最慢的一种。
因为在机器启动时,默认条件下,A20地址线是禁止的,因此操做系统必须使用适当的方法来开启它。
打开A20 Gate的代码为:
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
#从0x64端口读入一个字节的数据到al中
testb $0x2, %al
#若是上面的测试中发现al的第2位为0,就不执行该指令
jnz seta20.1
#循环检查
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
#将al中的数据写入到端口0x64中
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
第一步是向 804x 键盘控制器的 0x64 端口发送命令。这里传送的命令是 0xd1,这个命令的意思是要向键盘控制器的 P2 写入数据。这就是 seta20.1 代码段所作的工做。
第二步就是向键盘控制器的 P2 端口写数据了。写数据的方法是把数据经过键盘控制器的 0x60 端口写进去。写入的数据是 0xdf,由于 A20 gate 就包含在键盘控制器的 P2 端口中,随着 0xdf 的写入,A20 gate 就被打开了。
接下来要作的就是进入“保护模式”了。
它的GDT全称是Global Descriptor Table,中文名称叫“全局描述符表”,想要在“保护模式”下对内存进行寻址就先要有 GDT。GDT 表里的每一项叫作“段描述符”,用来记录每一个内存分段的一些属性信息,每一个“段描述符”占 8 字节。
在保护模式下,咱们经过设置GDT将内存空间被分割为了一个又一个的段(这些段是能够重叠的),这样咱们就能实现不一样的程序访问不一样的内存空间。这和实模式下的寻址方式是不一样的, 在实模式下咱们只能使用address = segment << 4 | offset的方式进行寻址(虽然也是segment + offset的,但在实模式下咱们并不会真正的进行分段)。在这种状况下,任何程序都能访问整个1MB的空间。而在保护模式下,经过分段的方式,程序并不能访问整个内存空间
为了使分段存储管理机制正常运行,须要创建好段描述符和段描述符表,全局描述符表是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。这里只须要载入已经静态存储在引导区的GDT表和其描述符到GDTR寄存器:
lgdt gdtdesc
#CPU 单独为咱们准备了一个寄存器叫作 GDTR 用来保存咱们 GDT 在内存中的位置和咱们 GDT 的长度。
#GDTR 寄存器一共 48 位,其中高 32 位用来存储咱们的 GDT 在内存中的位置,其他的低 16 位用来存咱们的 GDT 有多少个段描述符。
#16 位最大能够表示 65536 个数,这里咱们把单位换成字节,而一个段描述符是 8 字节,因此 GDT 最多能够有 8192 个段描述符。
#CPU 不只用了一个单独的寄存器 GDTR 来存储咱们的 GDT,并且还专门提供了一个指令用来让咱们把 GDT 的地址和长度传给 GDTR 寄存器:lgdt gdtdesc
gdtdesc 和 gdt 一块儿放在了 bootasm.S 文件的最底部
# Bootstrap GDT .p2align 2 # force 4 byte alignment gdt: SEG_NULLASM # null seg SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel gdtdesc: .word 0x17 # sizeof(gdt) - 1 () # 16 位的 gdt 大小sizeof(gdt) - 1
.long gdt # address gdt()# 32 位的 gdt 所在物理地址
48 位传给了 GDTR 寄存器,到此 GDT 就准备好了
如同 A20 gate 这个开关负责打开 1MB 以上内存寻址同样,想要进入“保护模式”咱们也须要打开一个开关,这个开关叫“控制寄存器”,x86 的控制寄存器一共有 4 个分别是 CR0、CR一、CR二、CR3(这四个寄存器都是 32 位的),而控制进入“保护模式”的开关在 CR0 上。
CR0中包含了6个预约义标志,0位是保护容许位PE(Protedted Enable),用于启动保护模式,若是PE位置1,则保护模式启动,若是PE=0,则在实模式下运行。
CR0 上和保护模式有关的位,如图所示:
打开保护模式的代码为:
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
由于咱们没法直接操做 CR0,因此咱们首先要用一个通用寄存器来保存当前 CR0 寄存器的值,这里第一行就是用通用寄存器 eax 来保存 cr0 寄存器的值;
而后 CR0_PE 这个宏的定义在 mmu.h 文件中,是个数值 0x00000001,将这个数值与 eax 中的 cr0 寄存器的值作“或”运算后,就保证将 cr0 的第 0 位设置成了 1 即 PE = 1 保证打开了保护模式的开关。
而 cr0 的第 31 位 PG = 0 表示咱们只使用分段式,不使用分页,这时再将新的计算后的 eax 寄存器中的值写回到 cr0 寄存器中就完成了到保护模式的切换。
ljmp $PROT_MODE_CSEG, $protcseg
其中protcseg是一个标号(标号的用途在本文中的实验相关部分已说明)
因为已经使能了保护模式,因此这里要使用逻辑地址,而不是以前实模式的地址了
这里还要注意PROT_MODE_CSEG和PROT_MODE_DSEG,这二者分别定义为0x8和0x10,表示代码段和数据段的选择子。
根据段选择子的格式定义,0x8就翻译成:
INDEX TI CPL
注意这里创建堆栈,ebp寄存器按理来讲是栈帧的,可是这里并不须要把它设置为0x7c00,由于这里0x7c00是栈的最高地址,它上面没有有效内容,而以后由于调用,ebp会被设置为被调用的那个函数的栈的起始地址,这里就不用管它了。
Bootload的启动过程能够归纳以下:
首先,BIOS将第一块扇区(存着bootloader)读到内存中物理地址为0x7c00的位置,同时段寄存器CS值为0x0000,IP值为0x7c00,以后开始执行bootloader程序。CLI屏蔽中断(屏蔽全部的中断:为中断提供服务一般是操做系统设备驱动程序的责任,所以在bootloader的执行全过程当中能够没必要相应任何中断,中断屏蔽是经过写CPU提供的中断屏蔽寄存器来完成的);CLD使DF复位,即DF=0,经过执行cld指令能够控制方向标志DF,决定内存地址是增大(DF=0,向高地址增长)仍是减少(DF=1,向地地址减少)。设置寄存器 ax,ds,es,ss寄存器值为0;A20门被关闭,高于1MB的地址都默认回卷到0,因此要激活A20,给8042发命令激活A20,8042有两个IO端口:0x60和0x64, 激活流程: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60,打开A20门。从实模式转换到保护模式(实模式将整个物理内存当作一块区域,程序代码和数据位于不一样区域,操做系统和用户程序并无区别对待,并且每个指针都是指向实际的物理地址,地址就是IP值。这样,用户程序的一个指针若是指向了操做系统区域或其余用户程序区域,并修改了内容,那么其后果就极可能是灾难性的),因此就初始化全局描述符表使得虚拟地址和物理地址匹配能够相互转换;lgdt汇编指令把经过gdt处理后的(asm.h头文件中处理函数)描述符表的起始位置和大小存入gdtr寄存器中;将CR0的第0号位设置为1,进入保护模式;指令跳转由代码段跳到protcseg的起始位置。设置保护模式下数据段寄存器;设置堆栈寄存器并调用bootmain函数;
汇编基本语法简介
清华大学教学内核ucore学习系列(1) bootloader
ucore-lab1-练习3report
学习xv6从实模式到保护模式
ucore练习三