《操作系统真象还原》第三章

《操作系统真象还原》第三章

vstart

vstart=xxxxorg xxxx这两个关键字是同一功能,它们并不是告诉编译器程序加载到地址xxxx,它们的功能是告诉编译器把后面所有数据(指令和变量)的地址以xxxx为起始开始编吧

mov [xxx], ax ; 原来[xxx]指段内基地址加上偏移地址xxx
vstart=0x7c00

mov [xxx], ax ; [xxx]指段内基地址加上偏移地址xxx再加上0x7c00

mbr用vstart=0x7c00来修饰的原因,是因为开发人员知道mbr要被加载器(BIOS)加载到物理地址0x7c00,mbr中后续的物理地址都是0x7c00+。另外,因为BIOS进入mbr是通过jmp0:7c00来实现的,故此时cs已经变成0,相当于“平坦模型”了,只不过此“平坦模型”大小只是65536字节,而不是4GB,所以mbr中各数据编译出来的地址(大于等于0x7c00)实际上都成了偏移地址,这样“0*16:偏移地址0x7c00+”来访问被加载到0x7c00的mbr是正确无误的。所以说,用vstart的时机是:我预先知道我的程序将来被加载到某地址处。程序只有加载到非0地址时vstart才是有用的,程序默认起始地址是0。

通用寄存器

在这里插入图片描述

操作显卡

在这里插入图片描述

显卡的文本模式也是分为多种模式的,用“列数行数”来表示,如8025,4025,8043或者8050,它们的乘积是整个屏幕上可以容纳的字符数。不同的模式可容纳的字符数不同,如8025表示一行80个字符,共25行。显卡在加电后,默认就置为模式80*25,也就是一屏可以打印2000个字符。我们也在这个默认模式下工作了。

屏幕上每个字符的低字节是字符的ASCII码,高字节是字符属性元信息。在高字节中,低4位是字符前景色,高4位是字符的背景色。颜色用RGB红绿蓝三种基色调和,第4位用来控制亮度,若置1则呈高亮,若为0则为一般正常亮度值。第7位用来控制字符是否闪烁(不是背景闪烁)。

在这里插入图片描述

我们将之前的MBR改造一下,保留滚屏的操作,只修改有关输出的部分,即把通过BIOS的输出改为通过显存

;主引导程序 
;
;LOADER_BASE_ADDR equ 0xA000 
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax

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

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

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

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

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

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

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

   times 510-($-$$) db 0
   db 0x55,0xaa

在这里插入图片描述

硬盘读写

在这里插入图片描述

在这里插入图片描述

左边PATA接口的线缆也称为IDE线,一个IDE线上可以挂两块硬盘,一个是主盘(Master),一个是从盘(Slave)。标有“主板插口”的那一头要插在图中PATA接口处。在之前,主盘从盘的分工很明显,很多工作都要以主盘为主,比如系统就要装在主盘上。到后来系统兼容性越来越好,以至区别不明显了。如果一个主板支持这样的4块IDE(PATA)硬盘,所以主板上提供两个IDE插槽。这两个接口也是以0为起始编号的,一个称为IDE0,另一个称为IDE1。不过按ATA的说法,这两个插槽称为通道,IDE0叫作Primary通道,IDE1叫作Secondary通道。即使主板上安装的是SATA硬盘,它也兼容PATA的编程接口,向上兼容是计算机能源源不断向前发展的根基。所以,后面给出的端口号也将按照PATA这两个通道来分组给出。

这里所说的主盘master、从盘slave别和 Primary通道、Secondary通道搞混了,通道是channel,不是disk,每个通道上分别有主盘和从盘。

硬盘控制器端口

在这里插入图片描述

端口可以被分为两组,Command Block registersControl Block registersCommand Block registers用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers用于控制硬盘工作状态。在Control Block registers组中的寄存器已经精减了,而且咱们基本上用不到,不说它们了,下面重点介绍Command Block registers组中的寄存器。

data寄存器在名字上我们就知道它是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16位(已经很不错了,表中其他寄存器都是8位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。

​ 读硬盘时,端口0x1710x1F1的寄存器名字叫Error寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫Feature寄存器。有些命令需要指定额外参数,这些参数就写在Feature寄存器中。强调一下,error和 feature这两个名字指的是同一个寄存器,只是因为不同环境下有不同的用途,为了区别这两种用途,所以在相应环境下有不同的名字。这两个寄存器都是8位宽度。

Sector count寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是8位寄存器,最大值为255,若指定为0,则表示要操作256个扇区。

​ 硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称为CHS,但每次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,这对于磁头来说很直观,它就是根据这些信息来定位扇区的。可是咱们还是希望有一套对人来说较直观的寻址方法,我们希望磁盘中扇区从О开始依次递增编号,不用考虑扇区所在的物理结构。其实我在描述需求时已经说出了LBA的定义,这是一种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。

​ LBA有两种,一种是LBA28,用28位比特来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,每个扇区是512字节,最大支持 128GB。我们这里为图简单,采用LBA28模式。由于128GB已经不能满足日益增长的存储需求,硬盘越来越大了,得有相匹配的寻址方法与之配套,于是要介绍的另外一种是LBA48,用48位比特来描述一个扇区的地址,最大可寻址范围是2的48次方,等于281474976710656个扇区,乘以512字节后,最大支持131072TB,即 128PB

​ 介绍完了LBA,现在可以说LBA寄存器了,这里有LBA low、LBA mid、LBA high三个,它们三个都是8位宽度的。LBA low寄存器用来存储28位地址的第0~~7位, LBA mid寄存器用来存储第8~15位,LBA high寄存器存储第16~23位。哎?三个8位的加起来才24位,连LBA28都不够,咱们怎么用呢?有问题就有解决方案,这就引出了下一个寄存器,device寄存器

device寄存器是个杂项,它的宽度是8位。在此寄存器的低4位用来存储LBA地址的第24~27位。结合上面的三个LBA寄存器。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘第6位用来设置是否启用LBA 方式,1代表启用LBA模式,0代表启用CHS模式,另外的两位:第5位和第7位是固定为1的,称为MBS位,不用关注。

​ 在读硬盘时,端口0x1F7或0x177的寄存器名称是Status,它是8位宽度的寄存器,用来给出硬盘的状态信息。第0位是ERR位,如果此位为1,表示命令出错了,具体原因可见 error寄存器。第3位是datarequest位,如果此位为1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第6位是DRDY,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。第7位是BSY位,表示硬盘是否繁忙,如果为1表示硬盘正忙着,此寄存器中的其他位都无效。另外的4位暂不关注。

​ 在写硬盘时,端口0x1F7或0x177的寄存器名称是command,和上面说过的error和 feature寄存器情况一样,只是用途变了,所以换了个名字表示新的用途,它和status 寄存器是同一个。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令。

​ (1) identify: 0xEC,即硬盘识别。

​ (2) read sector: 0x20,即读扇区。

​ (3) write sector:0x30,即写扇区。

更多指令参考ATA手册

在这里插入图片描述

操作硬盘一般步骤

(1)先选择通道,往该通道的sector count寄存器中写入待操作的扇区数。

(2)往该通道上的三个LBA寄存器写入扇区起始地址的低24位。

(3) 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位,选择操作的硬盘(master硬盘或slave硬 盘)。

(4)往该通道上的command寄存器写入操作命令。

(5)读取该通道上的status寄存器,判断硬盘工作是否完成。

(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。

(7)将硬盘数据读出。

硬盘数据传输方式

(1)无条件传送方式。

(2)查询传送方式。

(3)中断传送方式。

(4)直接存储器存取方式(DMA)。

(5) I/O处理机传送方式。

改进MBR 完成从磁盘读入Loader加载器的使命

nop--空语句的作用:
1、nop指令执行时花去一个时钟周期,所以可用把它用在延时程序中作为循环体,用以估计一个延时时间;
2、用于程序指令的对齐,比如字对齐--以便于反汇编时容易读懂。

boot.inc:

;-------------	 loader和kernel   ----------
; loader 在内存中的位置
LOADER_BASE_ADDR equ 0x900
; loader 在硬盘上的逻辑扇区地址(LBA)
LOADER_START_SECTOR equ 0x2

​ boot.inc是我们的配置文件,我们目前关于加载器的配置信息就写在里面,今后还会在此添加更多的配置信息。大家看到的这两句也是预处理命令,是nasm提供的宏,和C语言中的宏是一回事。只不过nasm中的语法是:宏名equ值,而C语言中的宏是由#define 指令来实现的。所以LOADER_BASE_ADDR和LOADER_START_SECTOR是两个宏名。

LOADER_BASE_ADDR定义了loader 在内存中的位置,MBR要把 loader 从硬盘读入后放到此处。如前所述,它的值是0x900,说明将来loader会在内存地址0x900处。

LOADER_START_SECTOR定义了loader在硬盘上的逻辑扇区地址,即LBA地址。我们自己决定使用第2快扇区,它等于0x2,说明loader放在了第2块扇区。

mbr.asm:

; 主引导程序
;-------------------------------------------------------
%include "boot.inc"

section MBR vstart=0x7c00

    ; 初始化段寄存器
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov fs, ax
    mov sp, 0x7c00 ;这个时候 ds = es = ss = 0 栈指针指向MBR开始位置
    mov ax, 0xb800
    mov gs, ax     ; 将gs设置为0xb800,之后操作显存 
    ; 初始化段寄存器结束


    ; 清屏 利用0x06号功能,上卷全部行,则可清屏。
    ; -----------------------------------------------------------
    ;INT 0x10   功能号:0x06	   功能描述:上卷窗口
    ;------------------------------------------------------
    ;输入:
    ;AH 功能号= 0x06
    ;AL = 上卷的行数(如果为0,表示全部)
    ;BH = 上卷行属性
    ;(CL,CH) = 窗口左上角的(X,Y)位置
    ;(DL,DH) = 窗口右下角的(X,Y)位置
    ;无返回值:
    mov ax, 0x600 ; 设置AH为06, 即功能号= 0x06, 上卷全部行数
    mov bx, 0x700
    mov cx, 0     ; 窗口左上角的(0,0)位置
    mov dx, 0x184f ;窗口右下角的(24,79)位置, 因为默认为80x25的VGA文本模式, 0x18=24, 0x4f=79
    int 0x10
    ; 清屏结束


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

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

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

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

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

    mov eax, LOADER_START_SECTOR    ; 起始扇区LBA地址
    mov bx, LOADER_BASE_ADDR        ; 写入的地址
    mov cx, 1                       ; 待读入扇区数
    call rd_disk_m_16               ; 调用函数读取硬盘

    jmp LOADER_BASE_ADDR            ; 移交控制权给loader


    ;-------------------------------------------------------------------------------
    ; 功能:读取硬盘n个扇区
    ; eax=LBA扇区号
	; ebx=将数据写入的内存地址
	; ecx=读入的扇区数  
    ; 1. 写入待操作磁盘数
    ; 2. 写入LBA 低24位寄存器 确认扇区
    ; 3. device 寄存器 第4位主次盘 第6位LBA模式 改为1
    ; 4. command 写指令
    ; 5. 读取status状态寄存器 判断是否完成工作
    ; 6. 完成工作 取出数据 
    ;-------------------------------------------------------------------------------
	rd_disk_m_16:

        mov esi, eax    ; 备份eax
        mov di, cx      ; 备份cx

        ;读写硬盘:
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;第1步:设置要读取的扇区数, 0x1f2端口
        mov dx, 0x1f2
        mov al, cl
        out dx, al      ; 读取扇区数
        mov eax, esi    ; 恢复 ax
        ; 第1步结束
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;第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   ; 右移8位,获取15-8位
        out dx, al

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

        ; device 寄存器(0x1f6) 第4位主次盘(0主盘,1次盘) 
        ; 第6位LBA模式 改为1, 5、7位固定为7
        shr eax, cl     ; 把除了最后四位的其他位置设置成0
        and al, 0x0f    ;LBA第24~27位 0x0f=1111, 
        or al, 0xe0     ;0xe0=1110 0000, 把第7-4位设置成1110 转换为LBA模式
        mov dx, 0x1f6
        out dx, al
        ; 第2步结束
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


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


        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;第4步:检测硬盘状态
        ;设置不断读取重复 如果不为1则一直循环
        .not_ready:
            nop             ; 空跳转指令 在循环中达到延时目的(跳转过来之后等一下)
            in al, dx       ; 把寄存器中的信息返还出来
            and al, 0x88    ; 0x88=1000 1000, 保留第3和第7位 ,第3位为1表示硬盘控制器已经准备好数据, 第7位为1表示硬盘很忙
            cmp al, 0x08    ; 0x08=1000, 判断第3位是否为1, 不跳转结果为 al=0000 1000, 即硬盘已经准备好了
            jnz .not_ready  ; 若未准备好,继续等

        ; 第4步结束
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        ;第5步:从0x1f0端口读数据
        mov ax, di      ; 把 di 储存的cx 取出来 即读入的扇区数  
        mov dx, 256     ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
        mul dx          ; 被乘数在ax, 即 ax*256, 得到结果高16位在dx, 低16位在ax
        mov cx, ax      ; 得到要执行的in指令次数
        mov dx, 0x1f0

        .go_on_read:
            in ax, dx           ;两字节dx 一次读2byte
            mov [bx], ax        ; ebx=将数据写入的内存地址
            add bx, 2           ; 一次读2byte,所以将地址加2
            loop .go_on_read    ; 循环读取

            ret                 ; 函数调用结束

        ; 第5步结束, 函数调用结束
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    times 510-($-$$) db 0
    db 0x55, 0xaa

在这里插入图片描述

接下来的工作是编译,本次的编译较之前相比,多加了一个参数-I。此参数的意思还是参见nasm 帮助,nasm -h回车,找到-I的说明。

“-I<path>adds a pathname to the include file path”

大概意思是添加一个包含文件的路径,其实就是添加个库目录。为了目录整洁一些,我在 boot目录下建立了一个子目录include,并把boot.inc放到了include目录下。nasm要用-I指定库目录,所以在 boot目录下输入:

nasm -I include/ -o mbr.bin mbr.asm

dd if=/home/steven/source/os/bochs/code/mbr.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=1 conv=notrunc

编写简易Loader

loader.asm

%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR

    ; 输出背景色绿色,前景色红色,并且跳动的字符串"2 LOADER"
    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 $		       ; 通过死循环使程序悬停在此

在这里插入图片描述

输入以下代码

nasm -I include/ -o loader.bin loader.asm

向磁盘2写入 seek = 2跳过两个块

dd if=/home/steven/source/os/bochs/code/loader.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc

进入编译器bochs一探究竟
bin/bochs -f bochsrc.disk

在这里插入图片描述

测试成功😀

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值