操作系统学习:启动进入实模式

本文参考书籍

操作系统真相还原
Linux内核完全剖析:基于0.12内核
x86汇编语言  从实模式到保护模式
ps:基于x86硬件的pc系统

实模式相关介绍

实模式在上文已经做了简要的介绍,实模式的寄存器都是16位,实模式的1MB的寻址能力是通过段基址左移四位加上段内偏移实现的,由于BIOS启动的过程中就会被cpu执行,所以当bios加载完成时,1MB的内存布局如下图所示(图片来源操作系统真相还原);
这里写图片描述
通过如图可以看到,当bios加载完成后启动扇区代码后,启动代码可以通过在对应的内存位置上传入数据,就可以在屏幕上显示相应数据,此时的bios也有相应的中断向量表,可以调用,例如读写硬盘、向显示屏显示数据等都是通过该中断向量表实现。

此时可以根据相关bios的属性,构成如下主引导程序代码;

;主引导程序
;------------------------------------------------------------
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00

; 清屏利用0x06号功能,上卷全部行,则可清屏
; -----------------------------------------------------------
;INT 0x10   功能号:0x06       功能号:上卷窗口
;------------------------------------------------------
;输入
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角(X,Y)位置
;无返回值
   mov     ax, 0x600
   mov     bx, 0x700
   mov     cx, 0           ; 左上角: (0, 0)
   mov     dx, 0x184f      ; 右下角: (80,25),
               ; VGA文本模式下,一行只能容纳80个字符,共25行
               ; 下标从0开始,所以x18=24,0x4f=79
   int     0x10            ; int 0x10

;;;;;;;;;    下面这三行代码获取光标的位置    ;;;;;;;;;
;.get_cursor 获取当前光标位置,在光标位置处打印字符
   mov ah, 3        ; 输入:3号子功能获取光标位置,需要存入ah寄存器
   mov bh, 0        ; bh寄存器存储的是待获取光标的页号

   int 0x10     ; 输出: ch=光标开始行,cl=光标结束行
            ; dh=光标所在行号,dl=光标所在列号

;;;;;;;;;    获取光标位置结束   ;;;;;;;;;;;;;;;;

;;;;;;;;;     打印字符串   ;;;;;;;;;;;
   ;
   mov ax, message 
   mov bp, ax       ; es:bp 为串首地址,es此时同cs一致
            ; 开头时已经为sreg初始化

   ; 光标位置要用到dx寄存器中内容,cs中的光标位置可忽略
   mov cx, 5        ; cx 为串长度,不包括结束符0的字符个数
   mov ax, 0x1301   ; 子功能号13显示字符及属性,要存入ah寄存器
            ; al设置写字符方式ah=01;显示字符串,光标跟随移动
   mov bx, 0x2      ; bh村粗要显示的页号,此处是第0页,
            ; bl中是字符属性,属性黑底绿字bl = 02h)
   int 0x10     ; 执行BIOS 0x10 中断
;;;;;;;;;      ´打印字符串结束 ;;;;;;;;;;;;;;;

   jmp $       ; 进入死循环,使程序悬停在此

   message db "1 MBR"
   times 510-($-$$) db 0
   db 0x55,0xaa

该段代码主要就是在bios下调用了向屏幕打印输出的功能,可查bios手册对有关内容进一步了解,最后由于mbr的固定结束时0x55aa,并且要将剩余的扇区内容填满,由此便是该段代码所做的工作。

此处的程序只是简单的在启动之后就死循环,由于该扇区的大小只有512个字节,操作系统内核一般会超过该大小,并且操作系统一般存放在软盘或者硬盘上,此时还需要将操作系统加载到内存中,此时就需要使用Loader加操作系统加载到内存中。

读取硬盘数据并加载代码如下;

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:      
;-------------------------------------------------------------------------------
                       ; eax=LBA扇区号
                       ; ebx=将数据写入的内存地址
                       ; ecx=读入的扇区数
      mov esi,eax     ;备份eax
      mov di,cx       ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi      ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f      ;lba第24~27位
      or al,0xe0       ; 设置7到4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步:向0x1f7端口写入读命令,¬0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;第4步:检测硬盘状态
  .not_ready:
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88      ;第4位为1表示硬盘控制器已准备好数据传输
      cmp al,0x08
      jnz .not_ready       ;若未准备好,继续等

;第5步,从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax       ; di为要读取的扇区数,一个扇区有512个字节,每次读入一个字
               ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2        
      loop .go_on_read
      ret

当从硬盘读取数据到内存,只需要跳转到当前加载好的内存地址处去执行相应代码就可以了,如下;

section loader vstart=LOADER_BASE_ADDR

; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4     ; A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4   

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $             ; 通过死循环使程序悬停在此

此时就会在屏幕上显示完成后就进入了死循环。接下来就是进入保护模式,然后执行相应的内核代码。

Linux内核相关实模式操作流程

Linux引导启动程序的介绍
当pc的电源打开后,80x86结构的cpu将自动进入实模式,并从地址0xFFFF0开始自动给执行代码,这个地址通常是bios中的地址。pc的bios将执行某些系统检测,并在物理地址0处开始初始化中断向量。此后,它将启动设备的第一个扇区(磁盘引导扇区,512B)读入内存绝对地址0x7c00处,并跳转到这个地方。启动设备通常是软驱或硬盘。

Linux最开始执行的就是用汇编语言编写的boot/bootsect.S汇编文件,它将由bios读入到内存绝对地址0x7c00处,当它被执行时就会把自己移动到内存绝对地址0x90000(576KB)处,并把启动设备盘中后2KB代码(boot/setup.S)读入到内存0x90200处,而内核的其他部分则被读入到从内存地址0x10000(64KB)开始处,由于当时内核模块的长度不会超过0x80000字节大小(即512KB),所以bootsect程序吧内核模块读入物理地址0x10000开始位置处时并不会覆盖从0x90000(576KB)处开始的bootsect和setup模块,后面setup程序会把内核模块移动到物理内存起始位置处,这样内核模块中代码的地址即等于实际的物理地址,便于对内核代码和数据进行操作,可由如图表示;
Linux启动引导时内核内存位置的变化情况

启动部分识别主句的某些特性以及VGA卡的类型,然后将整个系统从地址0x10000移动到0x0000处,进入保护模式并跳转到系统的余下部分,此时所有32位运行方式的设置启动被完成:IDT、GDT以及LDT被加载,分页工作也设置好了,最终调用init/main.c中的main程序。

其中bootsect.S中的部分代码如下(参考Linux内核完全剖析);

!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
!
#include <linux/config.h>
SYSSIZE = DEF_SYSSIZE
!
!   bootsect.s      (C) 1991 Linus Torvalds
!   modified by Drew Eckhardt
!
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts. 
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

SETUPLEN = 4                ! nr of setup-sectors
BOOTSEG  = 0x07c0           ! original address of boot-sector
INITSEG  = DEF_INITSEG          ! we move boot here - out of the way
SETUPSEG = DEF_SETUPSEG         ! setup starts here
SYSSEG   = DEF_SYSSEG           ! system loaded at 0x10000 (65536).
ENDSEG   = SYSSEG + SYSSIZE     ! where to stop loading

! ROOT_DEV & SWAP_DEV are now written by "build".
ROOT_DEV = 0                ! 根文件系统设备使用与系统引导时同样的设备
SWAP_DEV = 0                ! 交换设备使用与系统引导时同样的设备

entry start                 ! 告知链接程序,程序从start标号开始执行
start:
    mov ax,#BOOTSEG         ! 设置ds段寄存器为0x7c0
    mov ds,ax
    mov ax,#INITSEG         ! 设置es段寄存器为0x9000
    mov es,ax
    mov cx,#256             ! 设置移动计数值为256字
    sub si,si               ! 源地址 ds:si = 0x7c00:0x0000
    sub di,di               !  目的地址es:di=0x9000:0x0000
    rep                     ! 重复执行并递减cx的值,直到cx=0为止
    movw                    ! 从内存[si]处移动cx个字到[di]处
    jmpi    go,INITSEG      ! 段间跳转,跳转到INITSEG段,偏移为go

go: mov ax,cs           ! 将ds、es和ss都置成移动后所在的段0x9000
    mov dx,#0xfef4  ! arbitrary value >>512 - disk parm size

    mov ds,ax
    mov es,ax
    push    ax      ! 临时保存段值(0x9000)

    mov ss,ax       ! put stack at 0x9ff00 - 12.
    mov sp,dx
/*
 *  Many BIOS's default disk parameter tables will not 
 *  recognize multi-sector reads beyond the maximum sector number
 *  specified in the default diskette parameter tables - this may
 *  mean 7 sectors in some cases.
 *
 *  Since single sector reads are slow and out of the question,
 *  we must take care of this by creating new parameter tables
 *  (for the first disk) in RAM.  We will set the maximum sector
 *  count to 18 - the most we will encounter on an HD 1.44.  
 *
 *  High doesn't hurt.  Low does.
 *
 *  Segments are as follows: ds=es=ss=cs - INITSEG,
 *      fs = 0, gs = parameter table segment
 */


    push    #0          ! 置段寄存器fs=0
    pop fs
    mov bx,#0x78        ! fs:bx is parameter table address
    seg fs
    lgs si,(bx)         ! gs:si is source

    mov di,dx           ! es:di is destination
    mov cx,#6           ! copy 12 bytes
    cld                 ! 清方向标致,复制时指针递增

    rep                 !复制12字节的软驱参数表到0x9000:0xfef4处
    seg gs 
    movw

    mov di,dx               ! 将es:di指向新表,然后修改表中偏移4处的最大扇区数
    movb    4(di),*18       ! patch sector count

    seg fs                  ! 让中断向量0x1E的值指向新表
    mov (bx),di   
    seg fs
    mov 2(bx),es

    pop ax              !as0x9000
    mov fs,ax           !设置fs=gs=0x9000
    mov gs,ax

    xor ah,ah           ! reset FDC 复位软盘控制器,让其采用新参数
    xor dl,dl           !dl=0,第一个软驱
    int     0x13        ! 系统调用读磁盘

! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.

load_setup:
    xor dx, dx          ! drive 0, head 0
    mov cx,#0x0002      ! sector 2, track 0
    mov bx,#0x0200      ! address = 512, in INITSEG
    mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
    int 0x13            ! read it  设置完磁盘参数后系统调用读磁盘
    jnc ok_load_setup       ! ok - continue  如果读取成功就跳到ok_load_setup执行

    push    ax          ! dump error code 显示错误信息,出错码压入
    call    print_nl    !屏幕光标回车
    mov bp, sp          ! ss:bp指向欲显示的字
    call    print_hex   !显示16进制值
    pop ax  

    xor dl, dl          ! reset FDC 复位磁盘控制器 重新读
    xor ah, ah
    int 0x13
    j   load_setup      !重新执行该段读磁盘程序

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

    xor dl,dl
    mov ah,#0x08        ! AH=8 is get drive parameters
    int 0x13            !系统调用获取磁盘驱动器参数
    xor ch,ch
    seg cs
    mov sectors,cx      
    mov ax,#INITSEG
    mov es,ax           !重置es值

! Print some inane message

    mov ah,#0x03        ! read cursor pos  获取光标位置
    xor bh,bh
    int 0x10            !dh代表行,dl代表列

    mov cx,#9           !显示9个字符
    mov bx,#0x0007      ! page 0, attribute 7 (normal)
    mov bp,#msg1        !将bp指向要显示的字符
    mov ax,#0x1301      ! write string, move cursor
    int 0x10            !写字符串并移动光标到串结束处

! ok, we've written the message, now
! we want to load the system (at 0x10000)

    mov ax,#SYSSEG      !开始在0x10000处加载system模块
    mov es,ax       ! segment of 0x010000
    call    read_it     !读磁盘上system模块,es为输入参数
    call    kill_motor  !关闭驱动器马达
    call    print_nl    !光标回车换行

! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.

    seg cs
    mov ax,root_dev     !取508509字节处的根设备号并判断是否已被定义
    or  ax,ax
    jne root_defined     !获取每磁道扇区数,如果是15则说明是1.2,如果是18则是1.4MB
    seg cs
    mov bx,sectors
    mov ax,#0x0208      ! /dev/ps0 - 1.2Mb
    cmp bx,#15
    je  root_defined
    mov ax,#0x021c      ! /dev/PS0 - 1.44Mb
    cmp bx,#18
    je  root_defined
undef_root:             !如果都不是则循环死机
    jmp undef_root
root_defined:
    seg cs
    mov root_dev,ax     !将检查过的设备号保存到root_dev中

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:

    jmpi    0,SETUPSEG   !至此,所有程序加载完毕,我们就跳转到0x9020:000处去执行

当bootsect.S将setup.S的代码读取完成后跳转到setup.S处代码执行。setup.S代码的主要功能是利用bios中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖了bootsect程序所在的位置),所取得的参数和保留的参数,会在内核中相关程序中使用,例如字符设备驱动程序等;然后setup程序将system模块从0x10000~0x8ffff(当时认为内核系统模块的长度不会大于512KB)整块下移动到内存绝对地址0x00000处,接下来加载中断描述符表寄存器idtr和全局描述符表寄存器gdtr,开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20~0x2f。最后设置cpu的控制寄存器cr0,从而进入32位保护模式运行,并跳转到位于system模块最前面部分的head.s程序继续执行。由此cpu进入到保护模式运行,保护模式相关内容将在下篇文章中进行介绍。

猜你喜欢

转载自blog.csdn.net/qq_33339479/article/details/80375085
今日推荐