本章内存规划

0

内核的结构、功能和加载

内核的结构和功能

  • 头部:记录各个段的汇编位置,这些统计数据用于告诉初始化代码如何加载内核
         ;以下常量定义部分。内核的大部分内容都应当固定 
         core_code_seg_sel     equ  0x38    ;内核代码段选择子
         core_data_seg_sel     equ  0x30    ;内核数据段选择子 
         sys_routine_seg_sel   equ  0x28    ;系统公共例程代码段的选择子 
         video_ram_seg_sel     equ  0x20    ;视频显示缓冲区的段选择子
         core_stack_seg_sel    equ  0x18    ;内核堆栈段选择子
         mem_0_4_gb_seg_sel    equ  0x08    ;整个0-4GB内存的段的选择子

;-------------------------------------------------------------------------------
         ;以下是系统核心的头部,用于加载核心程序 
         core_length      dd core_end       ;核心程序总长度#00

         sys_routine_seg  dd section.sys_routine.start
                                            ;系统公用例程段位置#04

         core_data_seg    dd section.core_data.start
                                            ;核心数据段位置#08

         core_code_seg    dd section.core_code.start
                                            ;核心代码段位置#0c


         core_entry       dd start          ;核心代码段入口点#10
                          dw core_code_seg_sel

  • 初始化代码:基本就是 MBRLoader(或者叫 BootLoader)了,从BIOS那里接力,安装最基本的段描述符,初始化最初的执行环境,然后从硬盘上读取和加载内核的剩余部分

  • 内核代码段:用于分配内存,读取和加载用户程序,控制用户程序的执行

  • 内核数据段:提供一段可读写的内存空间供内核使用

  • 公共例程段: 就是一些 例程或者叫子程序,供内核或者用户程序调用
    1

内核的加载

这部分的内容其实就是关于 MBR 的,所以以下讨论的是 MBR 的编写

  • 声明内核的位置,包括 物理地址和逻辑扇区号

       core_base_address equ 0x00040000   ;常数,内核加载的起始内存地址 
       core_start_sector equ 0x00000001   ;常数,内核的起始逻辑扇区号 
    
  • 进入保护模式
    这里就简单说一下步骤就好了

    以下设置了存储GDT信息的结构--- 界限+基地址

       pgdt             dw 0
                        dd 0x00007e00      ;GDT的物理地址
    
    • 设置堆栈,结合我们的内存规划图,堆栈段和MBR以 0x7c00为界线

    • 计算GDT所在的逻辑段地址

    • 安装段描述符

      • 空描述符
      • 0-4GB的数据段描述符,方便我们修改访问数据
      • 代码段描述符
      • 堆栈段描述符
      • 显示缓冲区描述符
        2
    • 加载 GDTR 寄存器

    • 打开A20地址线

    • 关中断

    • 设置 cr0 寄存器的 PE 位

    • 跳转到保护模式下的代码

  • 加载内核

    • 初始化段寄存器以访问相应的内存段
       ; 代码段,我们进入保护模式的跳转语句就已经设置好了
    
       mov eax,0x0008                     ;加载数据段(0..4GB)选择子
       mov ds,eax
    
       mov eax,0x0018                     ;加载堆栈段选择子 
       mov ss,eax
       xor esp,esp                        ;堆栈指针 <- 0 ,回滚
    
    • 从硬盘读入内核程序到内存,先读一个扇区,包含内核的头部数据,然后就知道总共有多少个扇区要读了

    设置好例程 read_hard_disk_0 需要的参数后,读取内核的第一个扇区数据

       ;以下加载系统核心程序 
       mov edi,core_base_address 
    
       mov eax,core_start_sector
       mov ebx,edi                        ;起始地址 
       call read_hard_disk_0              ;以下读取程序的起始部分(一个扇区) 
    

    然后是判断程序还需要读多少个扇区,这里就不谈了,直接看代码就好了

    最后循环读完整个内核就行

    • 安装 内核 的段描述符

    之前我们已经创建了几个保护模式下的初始的段描述符,然后我们现在要 为内核的各个段创建描述符,简单来说,就是往 GDT 里添加新的描述符

    这里需要注意的是,我们需要访问修改 pgdt,但这个标号是在代码段中,代码段是不可写的,所以我们这里使用的是上一章介绍的别名技术,用0-4GB的数据段来访问修改

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

    3

    我们的思路是给出一个段的 段基址、段界限和段的属性, 通过例程拼凑出段描述符,然后安装进 GDT 中

    段基址由 标号的汇编地址+内核的加载地址 得出
    段界限由 后一个段的标号-当前段的标号-1 得出
    段属性直接给出就行

    下面给个例子:

       ;建立公用例程段描述符
       mov eax,[edi+0x04]                 ;公用例程代码段起始汇编地址
       mov ebx,[edi+0x08]                 ;核心数据段汇编地址
       sub ebx,eax
       dec ebx                            ;公用例程段界限 
       add eax,edi                        ;公用例程段基地址
       mov ecx,0x00409800                 ;字节粒度的代码段描述符
       call make_gdt_descriptor           ;调用例程拼凑段描述符
       mov [esi+0x28],eax                 ;安装段描述符 低32位
       mov [esi+0x2c],edx                 ;安装段描述符 高32位  
    

    安装完后的GDT如下所示
    4

    安装完各个段的描述符后,还需要修改 pgdt 的内容(界限),然后加载 GDTR 寄存器使修改生效,最后将控制权交给内核

       mov word [0x7c00+pgdt],63          ;描述符表的界限
                                      
       lgdt [0x7c00+pgdt]                 ;加载 GDTR 寄存器
    
       jmp far [edi+0x10]                 ;跳转到内核的入口点,结合内核的头部组成图来看
    

用户程序的加载和重定位

内核中执行的代码其实挺短的,就本章节的例子而言

内核加载用户程序的流程

       mov esi,50                          ;用户程序位于逻辑50扇区 
       call load_relocate_program
    
       mov [esp_pointer],esp               ;临时保存堆栈指针
     
       mov ds,ax
    
       jmp far [0x10]                      ;控制权交给用户程序(入口点)
                                           ;堆栈可能切换 

  return_point:                            ;用户程序返回点
       mov eax,core_data_seg_sel           ;使ds指向核心数据段
       mov ds,eax

       mov eax,core_stack_seg_sel          ;切换回内核自己的堆栈
       mov ss,eax 
       mov esp,[esp_pointer]

       ;这里可以放置清除用户程序各种描述符的指令
       ;也可以加载并启动其它程序
     
       hlt        

代码段的主要内容就是上面这些。让我们看看内核做了什么
1.首先,我们给出用户程序的逻辑扇区号,然后调用例程加载和重定位用户程序
2.然后,我们将内核的堆栈指针保存到指定位置
3.然后,让ds指向用户程序头部,跳转到用户程序的入口点,移交控制权
4.用户程序返回(结束)后,内核切换ds和ss到自己的数据段和堆栈段
然后内核再重复以上或者做其他事情

用户程序的结构

主要是头部的结构,基本上和之前的差不多,唯一不同的就是,因为我们的用户程序能够调用内核提供的例程,所以我们有了这么一个 “符号地址检索表”

     ;符号地址检索表
     salt_items       dd (header_end-salt)/256 ;#0x24
     
     salt:                                     ;#0x28
     PrintString      db  '@PrintString'
                 times 256-($-PrintString) db 0
                 
     TerminateProgram db  '@TerminateProgram'
                 times 256-($-TerminateProgram) db 0
                 
     ReadDiskData     db  '@ReadDiskData'
                 times 256-($-ReadDiskData) db 0

我们要做的就是将各个标号处的字符串替换成对应例程的地址,让我们能够在用户程序中调用例程,后面会详细介绍,这里就不过多介绍了。暂时只需要知道这一段内容是为了让用户程序能够调用内核的例程

计算用户程序占用的扇区数

预读一个扇区的内容,这个扇区包含用户程序头部的数据,然后在循环读取就行了

已经挺熟练了,就不多讲了

简单的动态内存分配

其实相当简陋,主要由 例程 allocate_memory 实现

内核数据段中 ram_alloc dd 0x00100000 ;下次分配内存时的起始地址,这个ram_alloc标号处就存储着 内核下次分配内存时的起始地址,就是由allocate_memory计算出来的

不多提及了,看例程的具体实现就行

段的重定位和描述符的创建

根据前面读取到的用户程序头部信息,创建段的描述符(已经在保护模式下,段的重定位和创建描述符算是重合了吧),主要由例程 make_seg_descriptor 创建,set_up_gdt_descriptor 安装

重定位用户程序内的符号地址

前面几个步骤虽然讲的是 内核加载用户程序,但其实和 MBR加载内核 还是很相似的,这个步骤是为了用户程序能够使用内核创建的例程

在内核数据段中也有一个“符号地址检索表”

   ;符号地址检索表
   salt:
   salt_1           db  '@PrintString'
               times 256-($-salt_1) db 0
                    dd  put_string
                    dw  sys_routine_seg_sel

   salt_2           db  '@ReadDiskData'
               times 256-($-salt_2) db 0
                    dd  read_hard_disk_0
                    dw  sys_routine_seg_sel

   salt_3           db  '@PrintDwordAsHexString'
               times 256-($-salt_3) db 0
                    dd  put_hex_dword
                    dw  sys_routine_seg_sel

   salt_4           db  '@TerminateProgram'
               times 256-($-salt_4) db 0
                    dd  return_point
                    dw  core_code_seg_sel

   salt_item_len   equ $-salt_4
   salt_items      equ ($-salt)/salt_item_len

不过和用户程序中的稍有区别------内核中的每一项都有具体的入口地址,用户程序中的是没有的,我们要做的就是用具体地址替换掉用户程序中的字符串
6

算法实现也很简单,就是用循环,我们从用户程序的表中取出一项,然后循环对比内核中的表(比较的是字符串是否相等), 相等,将地址填入,不相等就继续找,也就是说外循环是用户程序,内循环是内核
7

执行用户程序

  • 初始化各个寄存器
     mov eax,ds
     mov fs,eax     ;fs指向的是用户程序的头部,后续用fs来调用内核的例程
 
     mov eax,[stack_seg]
     mov ss,eax
     mov esp,0
 
     mov eax,[data_seg]
     mov ds,eax
  • 调用内核例程
     mov ebx,message_1
     call far [fs:PrintString] ;fs指向用户程序头部,标号PrintString处有具体的例程入口地址
  • 程序结束,控制权返回给系统
     jmp far [fs:TerminateProgram]       ;将控制权返回到系统 
     ;TerminateProgram例程是返回内核中的 return_point 的位置

例程

read_hard_disk_0

read_hard_disk_0:          ;从硬盘读取一个逻辑扇区
                           ;EAX=逻辑扇区号
                           ;DS:EBX=目标缓冲区地址
                           ;返回:EBX=EBX+512 

make_gdt_descriptor

make_gdt_descriptor:       ;构造描述符
                           ;输入:EAX=线性基地址
                           ;      EBX=段界限
                           ;      ECX=属性(各属性位都在原始
                           ;      位置,其它没用到的位置0) 
                           ;返回:EDX:EAX=完整的描述符

load_relocate_program

load_relocate_program:    ;加载并重定位用户程序
                          ;输入:ESI=起始逻辑扇区号
                          ;返回:AX=指向用户程序头部的选择子 

allocate_memory

allocate_memory:          ;分配内存
                          ;输入:ECX=希望分配的字节数
                          ;输出:ECX=起始线性地址 

make_seg_descriptor

make_seg_descriptor:      ;构造存储器和系统的段描述符
                          ;输入:EAX=线性基地址
                          ;      EBX=段界限
                          ;      ECX=属性。各属性位都在原始
                          ;          位置,无关的位清零 
                          ;返回:EDX:EAX=描述符

set_up_gdt_descriptor

set_up_gdt_descriptor:    ;在GDT内安装一个新的描述符
                          ;输入:EDX:EAX=描述符 
                          ;输出:CX=描述符的选择子
 posted on 2024-08-18 20:24  Dylaris  阅读(30)  评论(0)    收藏  举报