实模式到保护模式:第13章读书笔记

497 阅读10分钟

这一章的内容是加载内核程序和用户程序的基本流程

关于程序的相关信息一般是放在文件的开头,程序本来就是从最开头的读取,所以放在开头也就符合一般的思维习惯,这本章中,作者创建了一个简单的文件头,用于存储该程序的信息,用于加载程序的使用

在这一章中,作者代码演示的加载内核和用户程序的方式可能与真正的操作系统不同,但是这个过程大体是相似的,只是真实操作系统的头部更为复杂

  1. 接下来我来读一下内核加载的程序代码:

     ;代码清单13-2
      ;文件名:c13_core.asm
      ;文件说明:保护模式微型核心程序 
      ;创建日期:2011-10-26 12:11
    
      ;以下常量定义部分。内核的大部分内容都应当固定 
      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
    
      ;==============================================================================
    

    上面代码中内核程序的头部声明了段选择子其对应的索引,所以引导程序在读取内核,创建描述符时,便应该在GDT表对应的位置创建对应的描述符

     [bits 32]               
     flush:                                  
      mov eax,0x0008                     ;加载数据段(0..4GB)选择子
      mov ds,eax
    
      mov eax,0x0018                     ;加载堆栈段选择子 
      mov ss,eax
      xor esp,esp                        ;堆栈指针 <- 0 
    

    初始化ss和ds的值,将其初始化对应的段选择子

      mov edi,core_base_address          ;读取出来的扇区将存储于何处
      mov eax,core_start_sector          ;内核的起始扇区号
      mov ebx,edi                        ;起始地址 
      call read_hard_disk_0              ;以下读取程序的起始部分(一个扇区) 
    
      
      mov eax,[edi]                     ;核心程序尺寸
    

首先,将内核将存入的内存的位置加载到edi寄存器中,将待读取扇区加载eax中去,调用read_hard_disk_0例程读取内核的第一个扇区,读取完毕之后,因为内核的首地址便存储了内核程序的长度,所以通过mov eax,[edi]能够得到内核程序的长度,并存放到eax中

     xor edx,edx 
     mov ecx,512                        ;512字节每扇区
     div ecx

     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                            ;循环读,直到读完整个内核 
	 
div 除数:指令默认被除数为edx:eax,商存储在eax中,edx存储余数
上面首先将edx的值清零,将512存入ecx中,作为除数,因为一个扇区存储512个字节
使用div edx得到大概的扇区数目,再通过一些逻辑的判断,最终得出扇区的总数目,并且调用read_hard_disk_0将内核的全部内容读取出来
	 
 ;到此处便将内核程序全部从硬盘中读取出来了,是位于内存0x40000处的内存开始位置处

 setup:
     mov esi,[0x7c00+pgdt+0x02]         ;不可以在代码段内寻址pgdt,但可以
                                        ;通过4GB的段来访问
     ;建立公用例程段描述符
     mov eax,[edi+0x04]                 ;公用例程代码段起始汇编地址
     mov ebx,[edi+0x08]                 ;核心数据段汇编地址
     sub ebx,eax
     dec ebx                            ;公用例程段界限
     add eax,edi                        ;公用例程段基地址,edi是内核加载的位置,而eax是公用例程段定义的基地址
     mov ecx,0x00409800                 ;字节粒度的代码段描述符
     call make_gdt_descriptor
     mov [esi+0x28],eax
     mov [esi+0x2c],edx
   
     ;建立核心数据段描述符
     mov eax,[edi+0x08]                 ;核心数据段起始汇编地址
     mov ebx,[edi+0x0c]                 ;核心代码段汇编地址 
     sub ebx,eax
     dec ebx                            ;核心数据段界限
     add eax,edi                        ;核心数据段基地址
     mov ecx,0x00409200                 ;字节粒度的数据段描述符 
     call make_gdt_descriptor
     mov [esi+0x30],eax
     mov [esi+0x34],edx 
  
     ;建立核心代码段描述符
     mov eax,[edi+0x0c]                 ;核心代码段起始汇编地址
     mov ebx,[edi+0x00]                 ;程序总长度
     sub ebx,eax
     dec ebx                            ;核心代码段界限
     add eax,edi                        ;核心代码段基地址
     mov ecx,0x00409800                 ;字节粒度的代码段描述符
     call make_gdt_descriptor
     mov [esi+0x38],eax
     mov [esi+0x3c],edx

     mov word [0x7c00+pgdt],63          ;描述符表的界限
     lgdt [0x7c00+pgdt]                  

上面的代码创建了属于内核的数据段,代码段,公共例程段的值,放入gdt表的位置与内核声明的常数的值一致,在最后将新的gdt表的界限写入对应的内存区域中,lgdt指令将新的gdt表的基地址和表的大小加载到GDT寄存器中去

     jmp far [edi+0x10]  

可以看一下上面内核头部的定义,内核中偏移量为0x10中存储的值为

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

此时edi的值为内核加载的起始位置,ds指向0-4GB数据段,因此通过远跳转指令jmp far [edi+0x10],最终跳转到内核标号为start的位置继续执行

  1. 进入内核程序的start开始运行,接下来进入内核加载用户程序的步骤

      mov esi,50                          ;用户程序位于逻辑50扇区 
      call load_relocate_program			;加载用户程序
    

将用户程序所在扇区放入esi中,开始加载用户程序,接下来调用load_relocate_program来执行加载用户程序

load_relocate_program:

     push ebx
     push ecx
     push edx
     push esi
     push edi
  
     push ds
     push es
  
     mov eax,core_data_seg_sel
     mov ds,eax                         ;切换DS到内核数据段,可能是到时候能够直接添加选择子
     mov eax,esi                        ;读取程序头部数据 
     mov ebx,core_buf                        
     call sys_routine_seg_sel:read_hard_disk_0  ;读取出来的将会被放到core_buf缓冲区中,读第一个扇区

     ;以下判断整个程序有多大
     mov eax,[core_buf]                 ;程序尺寸,用户程序的第一个字节记录了该程序的大小
     mov ebx,eax
     and ebx,0xfffffe00                 ;使之512字节对齐(能被512整除的数, 
     add ebx,512                        ;低9位都为0 
     test eax,0x000001ff                ;程序的大小正好是512的倍数吗? 
     cmovnz eax,ebx                     ;不是。使用凑整的结果 

首先,ds指向内核数据段的选择子,加载用户程序的第一个扇区到内核数据段中的缓冲区中,通过读取第一个字节,得到该用户程序的长度,其中用到了cmovnz指令,避免了跳转指令,接下来就是分配内存了

     mov ecx,eax                        ;实际需要申请的内存数量,eax存储了该程序大小,而且该大小是512的整数倍
     call sys_routine_seg_sel:allocate_memory	;返回时,ecx返回了分配内存的首地址
     mov ebx,ecx                        ;ebx -> 申请到的内存首地址
     push ebx                           ;保存该首地址,被分配内存的首地址
     xor edx,edx
     mov ecx,512
     div ecx
     mov ecx,eax                        ;总扇区数 
  
     mov eax,mem_0_4_gb_seg_sel         ;切换DS到0-4GB的段
     mov ds,eax

上面得到的eax是用户程序的长度,并且是512字节的倍数,将其除以512,得到用户程序所占据的扇区数,而通过allocate_memory分配的内存是线性地址,所以将ds切换到0-4GB段,接着便开始读取硬盘中的数据到为该程序分配的内存中去

     mov eax,esi                        ;起始扇区号,那么第一个扇区将会被连续读取两次,eax指向要被读取的扇区号
  .b1:
     call sys_routine_seg_sel:read_hard_disk_0
     inc eax
     loop .b1                           ;循环读,直到读完整个用户程序

读取所有的扇区到为它分配的内存处,接下来就可以读取用户程序的头部段,创建相对应的描述符,为用户程序使用的例程进行重定位到正确的位置

     ;建立程序头部段描述符
     pop edi                            ;恢复程序装载的首地址 
     mov eax,edi                        ;程序头部起始线性地址
     mov ebx,[edi+0x04]                 ;段长度
     dec ebx                            ;段界限 
     mov ecx,0x00409200                 ;字节粒度的数据段描述符
     call sys_routine_seg_sel:make_seg_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+0x04],cx                   

	
     ;建立程序代码段描述符
     mov eax,edi
     add eax,[edi+0x14]                 ;代码起始线性地址
     mov ebx,[edi+0x18]                 ;段长度
     dec ebx                            ;段界限
     mov ecx,0x00409800                 ;字节粒度的代码段描述符
     call sys_routine_seg_sel:make_seg_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+0x14],cx

     ;建立程序数据段描述符
     mov eax,edi
     add eax,[edi+0x1c]                 ;数据段起始线性地址
     mov ebx,[edi+0x20]                 ;段长度
     dec ebx                            ;段界限
     mov ecx,0x00409200                 ;字节粒度的数据段描述符
     call sys_routine_seg_sel:make_seg_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+0x1c],cx

     ;建立程序堆栈段描述符
     mov eax,edi            ;得到程序加载的基地址
     add eax,[edi+0x08]     ;得到栈段的基地址,可是对于栈是要得到其高端地址作为基地址				;
     add eax,[edi+0x0c]	   ;得到堆栈的高端地址
     mov ebx,0xffffffff	   
     sub ebx,[edi+0x0c]	   ;两者相减得出栈的界限值,esp > ebx
     mov ecx,0x00409600 
     call sys_routine_seg_sel:make_seg_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+0x08],cx

上面代码根据用户程序的头部的内容,为用户程序创建了代码段,数据段,堆栈段,将所创建的描述符选择子,并且将其放回用户程序的头部中去,用户程序可以直接引用这些选择子,接下来就重定位salt的内容了

     ;重定位SALT
     mov eax,[edi+0x04]
     mov es,eax                         ;es -> 用户程序头部 
     mov eax,core_data_seg_sel
     mov ds,eax
  
     cld

     mov ecx,[es:0x24]                  ;用户程序的SALT条目数
     mov edi,0x28                       ;用户程序内的SALT位于头部内0x2c处
  .b2: 
     push ecx
     push edi
  
     mov ecx,salt_items
     mov esi,salt
.b3:
     push edi
     push esi
     push ecx

     mov ecx,64                         ;检索表中,每条目的比较次数 
     repe cmpsd                         ;每次比较4字节 
     jnz .b4
     mov eax,[esi]                      ;若匹配,esi恰好指向其后的地址数据
     mov [es:edi-256],eax               ;将字符串改写成偏移地址 
     mov ax,[esi+4]
     mov [es:edi-252],ax                ;以及段选择子 
.b4:
  
     pop ecx
     pop esi
     add esi,salt_item_len
     pop edi                            ;从头比较 
     loop .b3
  
     pop edi
     add edi,256
     pop ecx
     loop .b2

     mov ax,[es:0x04]                   ;将用户头部的选择子放到ax中返回

     pop es                             ;恢复到调用此过程前的es段 
     pop ds                             ;恢复到调用此过程前的ds段
  
     pop edi
     pop esi
     pop edx
     pop ecx
     pop ebx
  
     ret

为用户程序使用到的系统调用进行重定位,将例程段在内核中的选择子和实际例程的偏移量放置到用户程序的salt中去

此时已经成功加载完成了用户程序,返回内核程序

总结:这一章通过作者通过一个实例来说明了用户程序加载和内核程序加载的大致流程。

本文由博客一文多发平台 OpenWrite 发布!