注:本程序为原创,若发现bug,万望指出,若有问题,欢迎交流,转载请指明出处。若能有助于一二访客,幸甚。
以下为结果截图,显示的LOGO为小篆字体的欢迎 baby os 加载完成...几个字。
保护模式
参考资料:
《Intel 64 and IA-32 Architectures Software Developer's Manual》
《Orange's 一个操作系统的实现'》
《X86/X64 体系探测及编程》
《30天自制操作系统》
《Linux 内核完全剖析》
0.概述
Intel IA 32下,CUP有两种工作模式:实模式和保护模式。打开PC,开始时CPU工作在实模式下,即此前几篇东西写的代码都是在实模式下的。
实模式下有16位的寄存器、16位的数据总线、及20位的地址总线,1MB的寻址能力。物理地址的计算方法:
物理地址(Physical Address) = 段值(Segment)* 16 + 偏移值(Offset) 其中段和偏移都是16位的。
从80386开始,Intel的CPU 进入32位时代,80386有32位地址总线,寻址能力达到4GB.
保护模式保护处理器的某些资源不能被随意访问,如处理器的硬件资源和系统的软件资源,如CR0等控制寄存器,GDT、IDT等系统级的数据结构,OS kernel的代码和数据等。
x86的segmentation和paging即分段和分页机制是实施保护措施的手段。分段和分页实行了不同的内存管理模式和访问控制。
1.权限和环境
4个权限级别:0~3,0为最高级别。
3种权限类型:CPL、DPL、RPL:
1)CPL(current privilege level):当前的权限级别,指示当前代码在哪个权限级别,CPL的值存放在CS寄存器Selector域的RPL。(另外,SS寄存器的Selector的RPL总等于CPL)。
2)DPL(Descriptor Privilege Level):DPL存放在描述符Descriptor(包括段描述符Segment Descriptor和门描述符Gate Descriptor)里的DPL域,它指示访问这些segment所需要的权限级别
3)RPL(Requested Privilege Level):存放在访问者所使用的选择子Selector的Bit0和Bit1,指示发起访问的访问者使用什么样的权限对目标进行访问。
若CPL > DPL表示当前运行的代码的权限级别不足,不能对segment或gate进行访问。
从实模式进入保护模式,段式管理机制必须建立,分页机制是可选的,当分页机制关闭时,从段式内存管理中得到的线性地址(linear address)就是物理地址。
2.段式管理所使用的资源
硬件资源:
1)CR0、CR4
2)GDTR、LDTR(可选)、IDTR、TR
3)段选择子寄存器:ES、CS、SS、DS、FS、GS寄存器
数据结构:
1)GDT、LDT(可选)、IDT
2)TSS
3)段描述符(Segment Descriptor):系统(System)段描述符、代码(Code)/数据(Data)段描述符
4)门描述符(Gate Descriptor):包括调用门(Call-gate),中断/陷阱门(Interrupt/Trap-gate)和任务门(Task-gate)
5)选择子(Selector):存放在段寄存器里。
分段机制的内存管理职责:从逻辑地址(Logic address)转换为处理器的线性地址(Linear address).
3.分页机制使用的资源:
1)控制寄存器:CR0、CR2、CR3、CR4
2)IA32_EFER
页转换表:
1)PDPT(Page Directory Pointer Table)
2)PDT(Page Directory Table)
3)PT(Page Table)
分页机制内存管理职责:从处理器的线性地址(即virtual address)映射到物理地址。
read/write的内存设备RAM(DRAM)、read-only的内存设备ROM(EPROM),及memory mapped I/O设备都可以映射到物理地址空间上。
典型的ROM设备映射到物理地址空间的高端和低端,Video和IGD设备的buffer映射到A0000H到BFFFFH的物理地址空间,PCIe等设备映射到物理地址空间的E0000000位置上,I/O APIC 设备映射到FEC00000以上的位置,等等。
经过页式转换形成的物理地址,可以映射到DRAM或外部存储设备Disk上。
4.段式内存管理
两方面的管理:
1)内存管理:为地址的转换提供基础
Linear Address = base + offset
与实模式下的原理是一致的,实模式下段的base = selector << 4; 保护模式下,base从segment descriptor里加载而来。
2)保护措施:对访问行为的控制
对段的limit、type、privilege检查
5.段式管理的数据结构
1)段选择子(Segment Selector)
RPL:bit0~bit1,请求访问者所使用的权限级别
TI:Table Indicator,描述符表索引位,bit2, TI=0表示GDT,TI=1表示LDT。
Index:Descriptor Index,它是Descriptor在GDT/LDT中的序号。bit3~bit15,13位,范围0~8191,即可寻址8192个descriptor。
2)描述符表(Descriptor Table)
Segment Selector用于在Descriptor Table中查找descriptor。
描述符表由描述符表寄存器进行定位,对应GDT,LDT,IDT有GDTR,LDTR,IDTR。在IA32中,这三个寄存器都是48位,包括低16位为Limit和髙32位为Base,加载描述符表方法为lgdt, lldt, lidt。
其中Limit用于检查Selector是否超出GDT的limit,如同数组的长度一样,判断数组是否越界。
3)段描述符(Segment Descriptor)
段描述符要么存放在描述符表里,要么被加载到段寄存器里。被加载到段寄存器后,它所描述的段变成了active状态。
描述符有两大类:段描述符和门描述符。
6.切换到保护模式
Intel推荐的步骤:
1)关中断,包括可屏蔽中断和不可屏蔽中断
2)使用lgdt加载GDTR
3)置cr0 的PE位,切换到保护模式
4)使用far jmp/call,提供一个同级权限的CS Selector更新CS寄存器
5)若需要使用LDT,用lldt加载LDTR
6)使用ltr加载TR
7)更新SS、DS寄存器
8)使用lidt加载IDTR
9)开中断
程序源码:
boot.s:
[cpp] view plaincopyprint?
#-------------------------------------------------------------- # 文件:boot.s # 描述:1.清屏 # 2.设置显示模式为0x103(800*600,256色) # 3.读取软盘,将内核加载到内存 # 4.将内核第一个扇区(load.s)移动到内存0x0000位置 # 5.将引导扇区中的GDT及新显示模式的一些参数移动到指定位置 # 6.开启A20总线,置位cr0寄存器的PE位,进入保护模式 # 时间:2012-12-29 21:47:12 # 作者:guzhoudiaoke@126.com #-------------------------------------------------------------- .include "include/kernel.inc" .section .text .global _start .code16 _start: jmp main #--------------------------------------------------------------- # 清屏: # 设置屏幕背景色,调色板的索引0指代的颜色为背景色 # 先不考虑效率,只考虑可读性,故ah,al分开赋值 #--------------------------------------------------------------- clear_screen: # 清屏函数 movb $0x06, %ah # 功能号0x06 movb $0, %al # 上卷全部行,即清屏 movb $0, %ch # 左上角行 movb $0, %ch # 左上角列 movb $24, %dh # 右下角行 movb $79, %dl # 右下角列 movb $0x07, %bh # 空白区域属性 int $0x10 ret #-------------------------------------------------------------------- # 设置显示模式: # 1.检查VBE是否存在,即显卡是否支持VESA BIOS EXTENSION # 2.检查VBE版本,是否为2.0以上 # 3.检查要设置的mode的一些参数,看是否符合要求 # 4.设置显示模式为VBE 0x103(800*600,256色) # 5.记录新显示模式的一些参数 # 6.若上面检查或设置失败,则设置显示模式为VGA 0x13(320*200,256色) #-------------------------------------------------------------------- set_video_mode: movw $0x800, %ax movw %ax, %es movw %ax, %ds xorw %di, %di check_vbe: movb $0x4f, %ah # 表示使用VBE标准 movb $0x00, %al # 功能号 int $0x10 cmp $0x004f, %ax # 若有VBE,AX应该为0x004f jne set_mode_vga_0x13 movw 0x04(%di), %ax cmp $0x0200, %ax # 若VBE版本不是2.0以上 jb set_mode_vga_0x13 check_vbe_mode: # 检查MODE_VBE_0x13的参数 movw $VIDEO_MODE_0x103, %cx movb $0x4f, %ah # 表明VBE标准 movb $0x01, %al # 子功能号 int $0x10 cmpb $0x00, %ah # 是否调用成功 jne set_mode_vga_0x13 cmpb $0x4f, %al # 是否支持该模式 jne set_mode_vga_0x13 cmpb $8, 0x19(%di) # 颜色是否占8bit jne set_mode_vga_0x13 cmpb $4, 0x1b(%di) # 颜色的指定方法为4(调色板方式) jne set_mode_vga_0x13 movw (%di), %ax andw $0x0080, %ax jz set_mode_vga_0x13 # AX第bit7是否为1(线性帧缓存是否有效) set_mode_vbe: # 下面设置模式 movw $VIDEO_MODE_0x103, %bx addw $0x4000, %bx # BX第14个比特表示是否使用大的线性缓存区 movb $0x4f, %ah # 表示使用VBE标准 movb $0x02, %al # 功能号,表示设置模式 int $0x10 save_video_mode_info: # 记录切换到的模式的一些参数信息 movw $VIDEO_MODE_0x103, video_mode movw 0x12(%di), %ax movw %ax, screen_x movw 0x14(%di), %ax movw %ax, screen_y movl 0x28(%di), %eax movl %eax, video_ram movw $1, %ax ret set_mode_vga_0x13: # 若不支持VBE则设置为VGA 0x13 mode movb $0, %ah # 功能号0x0 movb $VIDEO_MODE_0x13, %al # 显示模式 int $0x10 movw $0x13, video_mode movw $320, screen_x movw $200, screen_y movl $0xb8000, video_ram ret #---------------------------------------------------------------- # 读取软盘一个扇区: # 使用BIOS INT 0x13中断读软盘,使用前需要设置ES:BX作为缓冲区 # AX为相对扇区号,基于相对扇区号,为学习软盘的知识,使用了由 # 相对扇区号来读软盘的方式,也可以直接设置读取扇区数而读连续的 # 多个扇区。但好像有不能跨越磁道、不能超过64KB等限制,要小心。 # 柱面号、磁头号、扇区号计算公式如下: # 柱面号CH = N / 36,令x = N % 36 # 磁头号DH = x / 18,扇区号CL = x % 18 + 1(因为从1开始,故加1) #----------------------------------------------------------------- read_a_sect: movb $36, %dl divb %dl movb %al, %ch # 柱面号=N / 36, 假设x = N % 36 movb %ah, %al # AL = N % 36 xorb %ah, %ah # AH = 0, 则AX = AL = N % 36 movb $18, %dl divb %dl movb %al, %dh # 磁头号DH = x / 18 movb %ah, %cl # CL = x % 18 incb %cl # 扇区号CL = x % 18 + 1 movb $0x00, %dl # 驱动器号DL = 0,表示第一个软盘即floppya movb $0x02, %ah # 功能号0x02表示读软盘 movb $0x01, %al # 读取一个扇区数 re_read: # 若调用失败(可能是软盘忙损坏等)则重新调用 int $0x13 jc re_read # 若进位位(CF)被置位,表示调用失败 ret #------------------------------------------------------------------- # 读取内核到内存 # 该函数读取baby OS 的内核到内存,第一个扇区为引导扇区,需要读取 # 的是从第二个扇区(相对扇区号1)开始的KERNEL_SECT_NUM个扇区 # ES:BX为缓冲区,为读取内核的临时位置0x10000 #------------------------------------------------------------------- read_kernel: movw $0x1000, %ax movw %ax, %es # ES:BX 为缓冲区地址 xorw %bx, %bx movw $0x00, %si # 已经读取的扇区数 movw $0x01, %di # 相对扇区号 1: movw %di, %ax # 将相对扇区号传给AX作为参数 call read_a_sect incw %si incw %di addw $512, %bx cmpw $KERNEL_SECT_NUM, %si jne 1b ret #-------------------------------------------------------------------- # 移动内核第一个扇区: # 内核从软盘读取到内存的一个临时位置,现在将第一个扇区移动到内存 # 0x0000处,第一个扇区即load.s,它将会把内核剩余部分移动到它的后面, # 之所以分两次移动,是因为若内核较大,一次移动会覆盖0x7c00处的代码, # 即引导扇区的代码,导致运行出错。 #-------------------------------------------------------------------- move_first_sect_of_kernel: cli # 指明SI,DI递增 movw $0x1000, %ax movw %ax, %ds # DS:SI 为源地址 xorw %si, %si movw $0x00, %ax movw %ax, %es # ES:DI 为目标地址 xorw %di, %di movw $512 >> 2, %cx # 移动512/4 次 rep movsl # 每次移动4个byte ret #-------------------------------------------------------------------- # 移动GDT及新显示模式的参数信息到指定位置 # 该函数把GDT及参数信息移动到指定的位置,以便于以后使用 #-------------------------------------------------------------------- move_gdt_and_video_info: xorw %ax, %ax movw %ax, %ds # DS:SI 为源地址 leaw gdt, %si movw $GDT_ADDR >> 4, %ax # 由要保存的地址来计算段基址 movw %ax, %es # ES:DI 为目的地址 xorw %di, %di movw $GDT_SIZE+VIDEO_INFO_SIZE, %cx # 移动的双字个数 rep movsb ret #-------------------------------------------------------------------- # 开启保护模式: # 1.关中断 # 2.加载GDT # 3.开启A20总线,置cr0的PE位,切换到保护模式 # 4.far jmp/call,用一个CS Selector 更新CS 寄存器,开始执行新代码 #-------------------------------------------------------------------- enter_protected_mode: cli # 关中断 lgdt gdt_ptr # 加载GDT enable_a20: inb $0x64, %al # 从端口0x64读取数据 testb $0x02, %al # 测试读取数据第二个bit jnz enable_a20 # 忙等待 movb $0xdf, %al outb %al, $0x64 # 将0xdf写入端口0x60 movl %cr0, %eax # 读取cr0寄存器 orl $0x01, %eax # 置位最后以为即PE位 movl %eax, %cr0 # 写cr0寄存器 ljmp $CODE_SELECTOR, $0x00 # 跳转到代码段,即load.s处开始执行 ret #-------------------------------------------------------------------- # 开始执行后,会跳转到此处开始执行 #-------------------------------------------------------------------- main: movw %cx, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss movw $0x1000, %sp call clear_screen # 清屏 call set_video_mode # 设置显示模式 call read_kernel # 从软盘读取内核 call move_first_sect_of_kernel # 将内核第一个扇区load.s移动到0x0000 call move_gdt_and_video_info # 将GDT和显示模式信息保存起来 call enter_protected_mode # 进入包含模式 1: jmp 1b gdt: .quad 0x0000000000000000 # 空描述符 .quad 0x00cf9a000000ffff # 代码段描述符 .quad 0x00cf92000000ffff # 数据段描述符 .quad 000000000000000000 # 留待以后使用 .quad 000000000000000000 # 留待以后使用 video_mode: # 显示模式 .short 0 screen_x: # 水平分辨率 .short 0 screen_y: # 垂直分辨率 .short 0 video_ram: # video_ram地址 .long 0 gdt_ptr: # 用与lgdt 加载GDT .word screen_x - gdt - 1 # GDT段限长 .long GDT_ADDR # GDT基地址 .org 0x1fe, 0x90 # 用nop 指令填充 .word 0xaa55 # 引导扇区标志 load.s:
[cpp] view plaincopyprint?
#************************************************************************* # > File: load.s # > Desc: 1.设置新的数据段等 # 2.将内核剩余部分移动到load.s后面 # 3.显示babyos 加载成功的Logo # > Author: 孤舟钓客 # > Mail: guzhoudiaoke@126.com # > Time: 2012年12月30日 星期日 22时23分55秒 #************************************************************************* .include "include/kernel.inc" .section .text .global _start .org 0 _start: movl $DATA_SELECTOR, %eax movw %ax, %ds movw %ax, %es movw %ax, %fs movw %ax, %gs movw %ax, %ss movl $STACK_BOTTOM, %esp load_lefted_kernel: cld movl $0x10200, %esi movl $0x200, %edi movl $(KERNEL_SECT_NUM-1)<<7,%ecx rep movsl show_logo: movl $0xe0000000, %edi addl $272 + 800*10, %edi movl $0x400, %esi movl $128, %ebx movl $1, %eax 1: movl $256, %ecx set_line_mem: cmpb $255, (%esi) je 2f movb %al, (%edi) 2: inc %esi inc %edi loop set_line_mem addl $800-256, %edi decl %ebx jnz 1b 3: jmp 3b .org 512, 0x90 baby os 暂时使用下面的简单logo:
o(∩∩)o...哈哈,这个logo 使用小篆字体,还是很有中国特色的呦~
总结
以上是生活随笔为你收集整理的跳转到保护模式并显示一个LOGO的全部内容,希望文章能够帮你解决所遇到的问题。
如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。