• x86架构 --- 内核组成


    内核的内存分布

    1. 初始化代码
      从BIOS 那里接管处理器和计算机硬件的控制权,安装最基本的段描述符,初始化最初的执行环境。然后,从硬盘上读取和加载内核的剩余部分,创建组成内核的各个内存段。
    2. 内核代码段
      用于分配内存,读取和加载用户程序,控制用户程序的执行。
    3. 内核数据段
      提供了一段可读写的内存空间,供内核自己使用。
    4. 公共例程段
      用于提供各种用途和功能的子过程以简化代码的编写。这些例程既可以用于内核,也供用户程序调用。
    5. 头部段
      用于记录每个段的汇编位置以及内核的入口信息,告诉初始化代码如何加载内核
      在这里插入图片描述

    硬盘主引导扇区代码

    1

    加载之前,主引导扇区的代码位与硬盘上,内核的起始逻辑扇区号为1(约定 写入虚拟硬盘时就写入到1号扇区)。需要将内核加载到内存0x00040000的位置上。

    core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址
    core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号
    
    • 1
    • 2

    2 进入保护模式

    加载内核之前需要进入保护模式,访问全部的内存空间,并且为内核执行作准备(内核工作在保护模式下)
    主要是设置gdt的base和size信息写到gdtr寄存器,然后安装:

    mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
    	...
    	...
    pgdt dw 0
         dd 0x00007e00 ;GDT的物理地址
    lgdt [cs:pgdt+0x7c00]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    由于0x00007e00是32位物理地址,目前仍为实模式,所以要转为逻辑段地址和偏移地址
    方法是除16,商为段地址,余数为偏移地址。

    ;计算GDT所在的逻辑段地址
    mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址
    xor edx,edx
    mov ebx,16
    div ebx ;分解成16位逻辑地址
    
    mov ds,eax ;令DS指向该段以进行操作
    mov ebx,edx ;段内起始偏移地址
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    创建数据段描述符,初始代码段描述符,堆栈段描述符,显示缓冲区描述符:
    注意这里数据段是整个4GB的内存空间,代码段重叠在其上,开始于0x00007c00,代码段不能直接修改,但可以通过对数据段的修改来到达这个目的:

    ;跳过0#号描述符的槽位
    ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
    mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF
    mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
    
    ;创建保护模式下初始代码段描述符
    mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF
    mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
    
    ;建立保护模式下的堆栈段描述符 ;基地址为0x00007C00,界限0xFFFFE
    mov dword [ebx+0x18],0x7c00fffe ;粒度为4KB
    mov dword [ebx+0x1c],0x00cf9600
    
    ;建立保护模式下的显示缓冲区描述符
    mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
    mov dword [ebx+0x24],0x0040920b ;粒度为字节
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    打开A20,兼容8086程序:

    in al,0x92 ;南桥芯片内的端口
    or al,0000_0010B
    out 0x92,al ;打开A20
    
    • 1
    • 2
    • 3

    然后关中断,设置CR0的Protect Enable PE位进入保护模式

    cli ;中断机制尚未工作
    
    mov eax,cr0
    or eax,1
    mov cr0,eax ;设置PE位
    
    • 1
    • 2
    • 3
    • 4
    • 5

    很重要的,使用jmp清空流水线:
    0x0010时代码段描述符选择子的“索引”位置,,flush则是flush代码块在代码段的32位偏移

    jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移 清流水线并串行化处理器
    
    • 1

    完成!!!
    现在内存分布应该是这个样子:
    在这里插入图片描述

    3 加载内核

    加载内核就需要从指定的硬盘扇区读取数据到指定的内存空间里,这两个”指定“前面第一步就已经设置好了
    (core_base_address 和 core_start_sector)
    读取数据需要访问硬盘,操作I/O接口中端口寄存器。数据读取出来要放到内存中数据段,需要段寄存器预先设置到指定位置

             mov eax,0x0008                     ;加载数据段(0..4GB)选择子
             mov ds,eax
          
             mov eax,0x0018                     ;加载堆栈段选择子 
             mov ss,eax
             xor esp,esp                        ;堆栈指针 <- 0 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    还记得之前章节中所说的引导扇区代码和用户程序之间的约定吗?这里操作系统代码就是之前的用户程序,同样需要和引导代码之间做一个约定!这样引导扇区代码才知道操作系统到底有多大。根据约定,操作系统代码头部第一个扇区内应该记录一些信息,包括:

    1. 核心程序总长度
    2. 系统公用例程段位置
    3. 核心数据段位置
    4. 核心代码段位置
    5. 核心代码段入口点

    现在让引导代码开始从硬盘读取这至关重要的第一扇区。
    读硬盘的代码封装成一个过程调用:
    参数:
    逻辑扇区号,用EAX传送
    目标缓冲区地址,DS:EBX传送,DS表示描述符索引,EBX作为逻辑偏移量
    返回值:
    EBX,让偏移直接+512,以便读取下一个扇区。

    read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                             ;EAX=逻辑扇区号
                                             ;DS:EBX=目标缓冲区地址
                                             ;返回:EBX=EBX+512 
             push eax 
             push ecx
             push edx
          
             push eax
             
             mov dx,0x1f2
             mov al,1
             out dx,al                       ;读取的扇区数
    
             inc dx                          ;0x1f3
             pop eax
             out dx,al                       ;LBA地址7~0
    
             inc dx                          ;0x1f4
             mov cl,8
             shr eax,cl
             out dx,al                       ;LBA地址15~8
    
             inc dx                          ;0x1f5
             shr eax,cl
             out dx,al                       ;LBA地址23~16
    
             inc dx                          ;0x1f6
             shr eax,cl
             or al,0xe0                      ;第一硬盘  LBA地址27~24
             out dx,al
    
             inc dx                          ;0x1f7
             mov al,0x20                     ;读命令
             out dx,al
    
      .waits:
             in al,dx
             and al,0x88
             cmp al,0x08
             jnz .waits                      ;不忙,且硬盘已准备好数据传输 
    
             mov ecx,256                     ;总共要读取的字数
             mov dx,0x1f0
      .readw:
             in ax,dx
             mov [ebx],ax
             add ebx,2
             loop .readw
    
             pop edx
             pop ecx
             pop eax
          
             ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    接下来就需要调用这个过程来读取第一扇区,别忘了传参:
    逻辑扇区号,用EAX传送,EAX里要送入core_start_sector
    目标缓冲区地址,DS:EBX传送,DS表示描述符索引,EBX作为逻辑偏移量。DS指向数据段的初始化前面已经做过了,EBX要送入core_base_address。
    ok,万事俱备,开始调用

     call read_hard_disk_0
    
    • 1

    等这次调用返回,第一扇区中的内容就copy到core_base_address开始内存位置上了,所以core_base_address起始的一个double word即四字节就是内核程序的总长度,取到这个总长度,除以512 从而算出我们还有多少个扇区没有读取。

    		 ;edi事先设置为core_base_address
             ;以下判断整个程序有多大
             mov eax,[edi]                      ;核心程序尺寸
             xor edx,edx 
             mov ecx,512                        ;512字节每扇区
             div ecx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果除数是0,那就说明这内核很小,还没到512字节(事实上os不会这么小)
    如果除数不为0,那么商就等于总扇区数-1,但是我们已经读取一个扇区了,也就是说商就是未读扇区数
    知道这些后面就开分支根据不同情况读取就行:

             or edx,edx
             jnz @1                             ;未除尽,因此结果比实际扇区数少1 
             dec eax                            ;已经读了一个扇区,扇区总数减1 
       @1:
             or eax,eax                         ;考虑实际长度≤512个字节的情况 
             jz setup                           ;EAX=0 ?
    
             ;读取剩余的扇区
             mov ecx,eax                        ;32位模式下的LOOP使用ECX
             mov eax,core_start_sector
             inc eax                            ;从下一个逻辑扇区接着读
       @2:
             call read_hard_disk_0
             inc eax
             loop @2  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    内核已经全部读到了内存中,老话说入乡随俗,内核要被执行,读取,操作数据,也是要到GDT表中登记自己的分段信息的。
    所以现在首要任务为它的各个段创建描述符。
    创建描述符并加载这种事情之前已经做过了

    lgdt [cs: pgdt+0x7c00]
    
    • 1

    现在就像交了作业却发现还有几道题没写的你,需要把几道题写上再重提交一次。
    我们的任务是重新从标号pgdt 处取得GDT 的基地址,为其添加描述符,并修改它的大小,然后用lgdt 指令重新加载一遍GDTR 寄存器,使修改生效。
    之前做这件事时我们是在实模式下,可以对数据进行任意的修改。现在我们已经处于保护模式,代码段无法修改。但是前面说过代码段和数据段重叠在一起,可以通过对数据段的修改来达到对代码段进行修改的目的

    setup:
    	mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以通过4GB的段来访问
    
    • 1
    • 2

    经过上面的设置,现在edi指向内核程序的起始地址,内核程序head部分给出了其中各个部分所在的位置。
    刚才计算内核程序尺寸已经用到过core_length,现在要使用下面几个,光有段所在的位置还不够,还需要计算每个段的界限,这就需要将相邻段位置之间相减,方能算出每个段的段长,再减去一则是段界限大小。

    15 ;以下是系统核心的头部,用于加载核心程序
    16 core_length dd core_end ;核心程序总长度#00
    17
    18 sys_routine_seg dd section.sys_routine.start
    19 ;系统公用例程段位置#04
    20
    21 core_data_seg dd section.core_data.start
    22 ;核心数据段位置#08
    23
    24 core_code_seg dd section.core_code.start
    25 ;核心代码段位置#0c
    26
    27
    28 core_entry dd start ;核心代码段入口点#10
    29 dw core_code_seg_sel
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有了段基址和段界限还不够,描述符还需要高32位中间那几个配置位,具体的描述符配置阶段就不过多讲述了,核心代码封装成了一个过程:make_gdt_descriptor

    98 ;建立公用例程段描述符
    99 mov eax,[edi+0x04] ;公用例程代码段起始汇编地址
    100 mov ebx,[edi+0x08] ;核心数据段汇编地址
    101 sub ebx,eax
    102 dec ebx ;公用例程段界限
    103 add eax,edi ;公用例程段基地址
    104 mov ecx,0x00409800 ;字节粒度的代码段描述符
    105 call make_gdt_descriptor
    106 mov [esi+0x28],eax
    107 mov [esi+0x2c],edx
    108
    109 ;建立核心数据段描述符
    110 mov eax,[edi+0x08] ;核心数据段起始汇编地址
    111 mov ebx,[edi+0x0c] ;核心代码段汇编地址
    112 sub ebx,eax
    113 dec ebx ;核心数据段界限
    114 add eax,edi ;核心数据段基地址
    115 mov ecx,0x00409200 ;字节粒度的数据段描述符
    116 call make_gdt_descriptor
    117 mov [esi+0x30],eax
    118 mov [esi+0x34],edx
    119
    120 ;建立核心代码段描述符
    121 mov eax,[edi+0x0c] ;核心代码段起始汇编地址
    122 mov ebx,[edi+0x00] ;程序总长度
    123 sub ebx,eax
    124 dec ebx ;核心代码段界限
    125 add eax,edi ;核心代码段基地址
    126 mov ecx,0x00409800 ;字节粒度的代码段描述符
    127 call make_gdt_descriptor
    128 mov [esi+0x38],eax
    129 mov [esi+0x3c],edx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    自此新来的描述符全都装配完成,描述符表的界限就需要更新,之前的 四个,加上新来的三个,还有一个空描述符,一共八个,所以界限值为8×8-1=63
    算出界限值后放入pgdt的低16bit上,然后再次使用ldgt安装新的描述符表;

    130
    131 mov word [0x7c00+pgdt],63 ;描述符表的界限
    132
    133 lgdt [0x7c00+pgdt]
    
    • 1
    • 2
    • 3
    • 4

    最后只需要跳到内核程序的入口处去执行:
    内核代码段入口点在偏移量为0x10的地方(core_entry位置)

    jmp far [edi+0x10]
    
    • 1

    至此 引导扇区代码的使命就完成了

  • 相关阅读:
    【附源码】计算机毕业设计SSM铜仁学院毕业就业管理系统
    在vue中父组件更新,子组件也会更新吗
    wpf中的StaticResource和DynamicResource
    SH-SSS丨《ISSD: 基于迭代式语音分离的说话人日志系统》论文线上分享
    Oracle Net Configuration Assistant 配置步骤
    小样本学习——匹配网络
    弹性云如何修改账号密码?以我购买的弹性云举例
    基于SSM的飞机航班管理系统
    【Ajax】全面详细了解git的基础操作【万字教学+面试常客】
    《Java 核心技术卷1 基础知识》第二章 Java 程序设计环境 笔记
  • 原文地址:https://blog.csdn.net/weixin_43604927/article/details/127426133