丹阳网站建设案例/网页设计参考网站

1.A20地址线:
A20地址线是指第21根地址线(因为地址线是从A0开始的)。
我们前边提到过,在支持32位保护模式的计算机中运行实模式时,为了适应实模式的寻址,我们的高12位地址线是默认输出0的。
举个例子:对于实模式下的0xFFFFF地址,实际上传入内存控制器的地址是0x000FFFFF,使用这种默认高位置0的方式可以增强寻址的兼容性,这样,不管是20位地址还是32位地址,我们只需要内存控制器能够识别32位地址即可。

但是,这样也会带来一个问题,我们如果在实模式下使用段:偏移的方式寻址,可能会产生21位的物理地址。如果我们使用0xFFFF:0x0010地址,当这个地址传入MMU时,MMU会产生21位地址100000000000000000000B,当A20打开时,就会寻址到1M以上的空间,这在实模式下是不允许的,所以在计算机开机时,A20是默认关闭的。这个时候,20位地址的部分就会被舍弃,超过1M的空间就会由于这个舍去的地址位而顺序地映射回0开始的物理地址中。

所以,在保护模式中,由于我们需要使用到第21个地址位,进入保护模式的第一步就是打开A20地址线。
2.加载GDT
在保护模式中,我们仍然需要地址分段,但是我们以后用到更多的内存管理是建立在分页上的。分页是可选的,但分段是必须的!
在保护模式分段中,由于我们要控制段的一些权限以及特权级问题(主要还是突出了“保护”二字,这方面后边会讲解),我们需要更多的数据来描述段的信息,所以我们使用了段描述符结构来存放这些信息,并且使用类似于指针的选择子来执行这些结构,我们只需要在段寄存器中存放选择子即可。当我们访问一个段时,计算机会取出段寄存器中的“指针”,获取到指向的段描述符就可以得到段的基址以及一些特权信息了。
为了方便管理,我们将段描述符放在一起,形成一张段描述符表,用的最多的段描述符表就是GDT(LDT我们不使用)。
(1)selector结构

selector的高13位是本描述符在GDT中的索引值,RPL是请求特权级(0~3 越小特权级别越高)TI指本描述符在LDT还是GDT中,我们默认设置为0(在GDT中)
我们重点关注的是描述符索引,由于每个描述符的大小位8字节,所以访问某个描述符地址为:GDT基址+8*描述符索引。
(2)段描述符

下边来简要概述下每个字段(如果想深入了解,可以去查看inter手册)
段界限基址被截断成了3个部分(主要还是为了兼容80286的非32位保护模式),拼接成了32位的段基址。
S,TYPE两个段共同表示了段的一些属性,比如读写执行属性以及段的类型(区分代码段 堆栈段等等)。
DPL表示段的目标权限等级
P表示段是否存在于内存中,我们暂不考虑,设为1即可
AVL段表示保留位,没啥用,预设为0就好了
段界限段和G共同表示了段的长度,G为0时,段长度=(段界限+1)*1-1 Byte
G为1时,段长度=(段界限+1)*4K+1 Byte,我们预设段界限为0xFFFFF,G为1
这样我们就可以访问 (0xFFFFF+1)*4K-1=0xFFFFFFFF的空间了,一共4个GB
L表示是否为64位段,我们编写的是32位系统,所以预设为0即可
D/B段表示EIP ESP IP ESP的选择,这个字段是为了兼容性考虑的,我们预设为1即可
(3)补充注意
在GDT中的第一个段描述符是不可用的,这是为了防止进入保护模式后,开发人员忘记装载新的段选择子,导致段寄存器中的值还是0,这样就会访问到第零个描述符造成错误的指令执行,所以我们首先要把GDT中的第一个描述符置0,我们使用第1个开始的描述符即可
(4)装载GDTR
我们的selector中存放的是段的索引,所以我们还需要一个寄存器来存放GDT的基址,这就是GDTR寄存器,这样我们就可以使用GDTR中的基址+段选择符索引*8即可访问对应的段描述符,在汇编中提供了 lgdt指令来装载GDTR寄存器。
另一个值的注意的是,每次这样访问地址时都访问一次GDT来寻找段描述符是很浪费时间的,所以,计算机会将段的描述信息放入高速缓存中,这样访问地址前可以直接通过缓存得到段基址。所以我们在修改GDT的时候一定要清空缓存,不然就会访问到更新前的段选择符信息。而清空缓存的方法就是再次使用lgdt来重新加载GDT。
3.动手实践吧
首先要清楚,我们使用的是平坦寻址模式,即每个段都是由0x00000000开始的,并且都能访问到4GB的空间,之所以这样做,是因为这样可以方便编写程序,以及兼容ELF文件,而对于内存管理等问题,我们交给分页即可。
boot.inc
;-------------- gdt描述符属性 -----------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0数据段是不可执行的,向上扩展的,可写的,已访问位a清0.DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
这是一个配置文件,描述了一些段的字段信息
下边是我们GDT的分配以及加载GDT的过程
GDT_BASE: dd 0x00000000 dd 0x00000000CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4 ; 此时dpl为0GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1 times 30 dq 0 ; 此处预留30个描述符的空位SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上 total_mem_bytes dd 0 ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址gdt_ptr dw GDT_LIMIT dd GDT_BASE
;boot开始!
boot_start:cli ;关闭外中断 mov [mutiboot_addr32], ebx ; GRUB加载内核后会将mutiboot信息地址存放在ebx中;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1;----------------- 打开A20 ----------------in al,0x92or al,0000_0010Bout 0x92,al;----------------- 加载GDT ----------------lgdt [gdt_ptr];----------------- cr0第0位置1 ----------------mov eax, cr0or eax, 0x00000001mov cr0, eaxjmp dword SELECTOR_CODE:far_jmp_target ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,;初始化段寄存器以及栈结构far_jmp_target:mov ax,SELECTOR_DATAmov ss,axmov ds,axmov ax,SELECTOR_VIDEOmov gs,axmov esp, STACK_TOP and esp, 0xFFFFFFF0 ;16字节对齐mov ebp, 0
我们定义了三个段描述符,并且按照一定的顺序加载了GDT。但是CS段选择子的加载,我们只能采用远跳转的方法,直接跳转到SELECTOR_CODE段执行,并且可以清空流水线(我们之前提到过)。最后再手动加载SS以及DS段
4.使用GRUB加载内核文件
我之前本来使用的是自己编写加载器来加载内核,但是后来由于安装的bochs工具的一些问题,导致每次只能加载两个扇区,调试了几天无果后,我打算使用现有的加载器来加载内核,同时,这样做我们可以避开ELF文件头的分析过程
为了方便大家的学习,我是配置好了一个1.44M的软盘来作为启动盘
https://pan.baidu.com/s/1AtixGuVUt1nm9qNE1OLhIQpan.baidu.com提取码jjj6
但是如果想自己制作软盘启动器的同学,看以下链接
CSDN-专业IT技术社区-登录blog.csdn.netGRUB会加载配置文件中指定的内核文件,我们要把软盘文件放在开发主目录下。
对于内核编译以及链接过程,我提供了以下两个文件:
Makefile
#Makefile for BHOS
#edit:2020/1/26
#by 不吃香菜的大头怪C_SOURCES = $(shell find . -name "*.c") #.c源文件
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES)) #.c生成.o文件
S_SOURCES = $(shell find . -name "*.s") #.s源文件
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES)) #.s生成.o文件#编译器与链接器参数
nasm_pars = -f elf -g -F stabs
gcc_pars = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include
ld_pars = -T kernel.ld -m elf_i386 -nostdlib#目标软盘
target_floppy = floppy.img#过程
all: $(S_OBJECTS) $(C_OBJECTS) link copykern.c.o:gcc $(gcc_pars) $< -o $@.s.o:nasm $(nasm_pars) $<.PHONY:link
link:ld $(ld_pars) $(S_OBJECTS) $(C_OBJECTS) -o kernel.elf.PHONY:copykern
copykern:sudo mount $(target_floppy) /mnt/kernel/sudo cp kernel.elf /mnt/kernel/sudo umount /mnt/kernel/.PHONY:mt
mt:sudo mount $(target_floppy) /mnt/kernel.PHONY:umt
umt:sudo umount /mnt/kernel.PHONY:run
run:qemu -fda $(target_floppy) -boot a.PHONY:clean
clean:rm $(S_OBJECTS) $(C_OBJECTS) kernel.elf
由于我们再Makefile中使用了链接器脚本参数来控制ld链接器,提供以下脚本
kernel.ld
ENTRY(boot_start)
SECTIONS
{/*内核是加载到1 M空间之上的*/. = 0x100000;.text :{*(.text). = ALIGN(4096);}.data :{*(.data). = ALIGN(4096);}.rodata :{*(.rodata). = ALIGN(4096); }.bss :{*(.bss). = ALIGN(4096);}.stab :{*(.stab). = ALIGN(4096);}.stabstr :{*(.stabstr). = ALIGN(4096);}/DISCARD/ : { *(.comment) *(.eh_frame) }
}
放在主目录下,主要内容是将内核各个section紧挨着链接,并且链接的虚拟起始地址为0x100000,即1M开始处,并且定义了内核的起始执行位置,即boot_start
最后,我们来编写GRUB加载内核后执行的启动内容文件
boot.s
;/boot/boot.s
;edit:2020/1/26
;by:不吃香菜的大头怪%include "./boot/boot.inc"
;这三个双字为GRUB加载器识别MagicNumber 以及配置信息(可以不用理解)
dd 0x1badb002
dd 0x3
dd -(0x1badb002+0x3) [BITS 32] ;由于GRUB在加载内核前进入保护模式,所以要32位编译
section .text
[GLOBAL boot_start]
[GLOBAL mutiboot_addr32]
[EXTERN kern_entry]GDT_BASE: dd 0x00000000 dd 0x00000000CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4 ; 此时dpl为0GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1 times 30 dq 0 ; 此处预留30个描述符的空位SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上 total_mem_bytes dd 0 ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址gdt_ptr dw GDT_LIMIT dd GDT_BASE
;boot开始!
boot_start:cli ;关闭外中断 mov [mutiboot_addr32], ebx ; GRUB加载内核后会将mutiboot信息地址存放在ebx中;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1;----------------- 打开A20 ----------------in al,0x92or al,0000_0010Bout 0x92,al;----------------- 加载GDT ----------------lgdt [gdt_ptr];----------------- cr0第0位置1 ----------------mov eax, cr0or eax, 0x00000001mov cr0, eaxjmp dword SELECTOR_CODE:far_jmp_target ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,;初始化段寄存器以及栈结构far_jmp_target:mov ax,SELECTOR_DATAmov ss,axmov ds,axmov ax,SELECTOR_VIDEOmov gs,axmov esp, STACK_TOP and esp, 0xFFFFFFF0 ;16字节对齐mov ebp, 0
;进入内核主函数 call kern_entry jmp dword $ ;防止意外退出内核section .data
mutiboot_addr32: dd 0x0 section .bss ; 未初始化的数据段从这里开始
stack:resb 0x100000 ; 1M的内核栈 (应该够了吧,不够自己改)
STACK_TOP equ $-1
首先,我们再文件开头填充了三个Magic Number,并且我们再Makefile中把boot.s连接到内核文件头部,这样GRUB识别内核头部的这三个数后会将内核文件识别为系统所在文件。
我们再此定义了boot_start符号,并且使用global将这个符号声明成全局的,这样GRUB就可以访问这个符号,并且我们在ld脚本中定义了boot_start为内核入口函数,GRUB在配置完成后就会jmp到boot_start执行
在boot_start中,我们首先把ebx存放在一个指定地址中,ebx中存放的是GRUB启动中的一些重要数据,以后会使用到。
最后,我们使用call kern_entry执行了内核的入口函数(我们在boot.s中也声明extern这个外部符号,类似于c语言中的函数声明)
下边,我们来编写kern_entry函数所在的c主文件
entry.c
void kern_entry(){char *input = (uint8_t *)0xB8000;char color = (0 << 4) | (15 & 0x0F);*input++ = 'H'; *input++ = color;*input++ = 'e'; *input++ = color;*input++ = 'l'; *input++ = color;*input++ = 'l'; *input++ = color;*input++ = 'o'; *input++ = color;*input++ = ' '; *input++ = color;*input++ = 'B'; *input++ = color;*input++ = 'H'; *input++ = color;*input++ = 'O'; *input++ = color;*input++ = 'S'; *input++ = color;*input++ = '!'; *input++ = color;while(1)asm volatile ("hlt");
}
我们在内核中直接访问VGA显存映射来控制屏幕输出
最后,cd到项目主目录中,使用make编译后再使用make run打开qemu虚拟机运行


最后,我们成功地加载了内核,并且有了屏幕输出
下一节,我们将编写string函数库以及printk等内核级打印函数