自制操作系统笔记-第三章

3.1 制作真正的IPL

; 读磁盘

        MOV        AX,0x0820
        MOV        ES,AX
        MOV        CH,0            ; 柱面0
        MOV        DH,0            ; 磁头0
        MOV        CL,2            ; 扇区2

        MOV        AH,0x02            ; AH=0x02 : 读盘
        MOV        AL,1            ; 1个扇区
        MOV        BX,0
        MOV        DL,0x00            ; A驱动器
        INT        0x13            ; 调用磁盘BIOS
        JC        error

JC是jump if carry的缩写,如果进位标志 (carry flag)是1的话就跳转,

INT 0x13,这个中断的说明参观:https://blog.csdn.net/weixin_37656939/article/details/79684611,书上的网站访问不了。

AH=0x02(读盘)

AH=0x03(写盘)

AH=0x04(校验)

AH=0x0c (寻道)

AL=处理对象的扇区数,(只能同时处理连续的扇区)

CH=柱面号

CL=扇区号 

DH=磁头号

DL=驱动器号

ES:BX=缓冲地址(校验及寻道时不使用)

返回值

  FLACS.CF=0: 没有错误,AH=0    AL=传输的扇区数(这里不知道是不是书上印错了,应该是FLAGS)

  FLAGS.CF=1: 有错误,错误号存入AH内(与重置功能一样)

这里是AH=0X02,所以是读盘

CF(carry flag)是一个只有一位信息的寄存器,这种只有一位的寄存器称为标志(flag),CF本是用来表示有没有进位的,但因为简单易用,所有其它地方也经常用到,这里就是表示函数调用 是否有错。

软盘结构:

80个柱面(0-79),每个柱面18个扇区(sector)(1-18)每个扇区512字节,两个磁头(0 正面和1 背面),所以一张软盘容量是80*18*512*2=1 474 560 字节 = 1440KB

含有IPL的启动区位于柱面0,磁头0,扇区1(C0-H0-S1) ,它的下一个扇区是 C0-H0-S2, 这里要加载的就是这个。

注意:软盘是按扇区读取,但是内存是按字节对地址编号的。

ES:BX=缓冲地址,这个地址就是一个内存地址,表示我们要把从软盘上读出的数据装载到内存的哪个位置,BX是16位寄存器,只能表示0-0xffff的值,也就是0-65535,最大才64K。只用一个寄存器的话就只能用64K的内存,

EBX 是32位,能处理4G内存,0-0xffffffff ( 0 - 4294967295字节)即4194304KB = 4096MB =  4GB

 而早期没有EBX寄存器,所以设计了一个起辅助作用的段寄存器,使用段寄存器时,以[ES:BX]方式表示内存地址,即ES*16+BX ,如果ES取0xffff,BX也取0xffff,则为 0xffff * 16 + 0xffff = 65535*16 + 65535 = 1114095Byte = 1087KB,这样就可以访问1MB内存。

这里ES=0X0820, BX=0,所以软盘的第二个扇区的数据被装载到内存中的0x8200到0x83ff(512字节,一个扇区)的地方。0x8000 -  0x81FF这512字节是留给(copy)启动区的,要将启动区的内容读到那里。0x7c00 - 0x7DFF(512字节)用于启动区

内存分布图:

0x8000 -  0x81FF是留给启动区的,0x7C00 - 0x7DFF也是启动区,这两个的关系是什么?我有点没看懂。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

不管要指定内存的什么地址,都必须同时指定段寄存器,如省略会把“DS”作为默认的段寄存器。

MOV CX, [1234] 其实是MOV CX, [DS: 1234]的意思。 

MOV AL, [SI] 就是 MOV AL, [DS: SI] 的意思

所以DS必须预先指定为0,否则地址的值就要加上这个数的16倍,就会读写到其它地方,引起混乱。

 执行make run ,如果看到下面的画面就是成功了。如果失败会显示 load error

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 3.2 试错

; 读磁盘

        MOV        AX,0x0820
        MOV        ES,AX
        MOV        CH,0            ; 柱面0
        MOV        DH,0            ; 磁头0
        MOV        CL,2            ; 扇区2

        MOV       SI, 0             ; 记录失败次数的寄存器

retry:
        MOV        AH,0x02            ; AH=0x02 : 读入磁盘
        MOV        AL,1            ; 1个扇区
        MOV        BX,0
        MOV        DL,0x00            ; A驱动器
        INT        0x13            ; 调用 BIOS,就是读取磁盘
        JNC       fin                ; 没出错的话跳到fin
        ADD       SI, 1            ; 往SI加1
        CMP        SI, 5            ; 比较SI与5
        JAE        error            ; SI >= 5时,跳到error
        MOV        AH, 0x00
        MOV       DL, 0x00        ;A驱动器
        INT       0x13            ;  重置驱动器(系统复位,复位软盘状态,然后再试)
        JMP        retry

JNC (jump if not carry),就是没进位(进位标志为0)时跳转

JAE(jump if above or equal),大于等于跳转。

AH=0, DL=0,NIT 0x13 就是 系统复位,复位软盘状态

3.3 读到18扇区

; 读磁盘
        MOV        AX,0x0820
        MOV        ES,AX
        MOV        CH,0            ; 柱面0
        MOV        DH,0            ; 磁头0
        MOV        CL,2            ; 扇区2
readloop:
        MOV       SI, 0             ; 记录失败次数的寄存器
retry:
        MOV        AH,0x02            ; AH=0x02 : 读入磁盘
        MOV        AL,1            ; 1个扇区
        MOV        BX,0
        MOV        DL,0x00            ; A驱动器
        INT        0x13            ; 调用 BIOS,就是读取磁盘
        JNC       next                ; 没出错的话跳到next
        ADD       SI, 1            ; 往SI加1 (失败计数+1)
        CMP        SI, 5            ; 比较SI与5
        JAE        error            ; SI >= 5时,如果失败5次跳到error
MOV AH, 0x00 MOV    DL, 0x00 ; A驱动器 INT   0x13 ; 重置驱动器(系统复位,复位软盘状态,然后再试) JMP retry
next:
  MOV  AX, ES   ;把内存地址后移0x20
  ADD  AX, 0x0020  
  MOV  ES, AX  ;因为没有ADD ES, 0x020指令,所以这里稍微绕个弯
  ADD  CL, 1  ;CL加1 (下一个扇区号)
  CMP  CL, 18  ;比较CL与18
  JBE  readloop  ;如果CL <= 18 ,跳转至readloop

JBE(jump if below or equal),小于等于则跳转。

要读下一个扇区,只需CL加1,CL是扇区号,给ES加上0x20,ES指定读入地址。0x20是十六进制下512 除以 16的结果, 也可以写成ADD AX,512/16。

因为一个扇区是512字节,所以读入软盘上下一个扇区时,内存上的(目标)读入位置也要向后移512字节,

推导:(新的ES值*16 + BX ) - (旧的ES值 * 16 + BX) = 512  , 即  (新ES-旧ES) *16 = 512 ,  即 新ES = 旧ES + 512 / 16 ,也就是 旧ES值 + 32 (0x20)。

还有,这里也可以直接给BX+512 ,即  ADD BX, 512

磁盘BIOS 读盘函数 中断处理的扇区数 1到255(0xff), 一次同时处理2个以上扇区时,不跨越多个磁道,也不能超过64KB。

这段程序,已经把软盘上C0-H0-S2到 C0-H0-S18的 512 *17 =8704字节的内容 装载到内存 0x8200 ~ 0xA3ff 了。

学习到52页。contiune

 3.4 读入10个柱面

 C0-H0-S18 的下一个扇区是磁盘反面的C0-H1-S1,读到C0-H1-S18,接着读下一个柱面C1-H0-S1(正面第二个柱面)。一直读到C9-H1-S18(反面第10个柱面最后一个扇区)。

next:
      MOV       AX, ES   ;把内存地址后移0x20
      ADD       AX, 0x0020  
      MOV       ES, AX  ;因为没有ADD ES, 0x020指令,所以这里稍微绕个弯
      ADD       CL, 1  ;CL加1
      CMP       CL, 18  ;比较CL与18
      JBE       readloop  ;如果CL <= 18 (还没读到第18个扇区),跳转至readloop

        MOV        CL,1        ;因为上面已经读完了18个扇区,接下来要从下一个柱面的第一个扇区开始读,所以这里要给CL=1
        ADD        DH,1       ; DH+1, DH=0 为正面磁头 DH=1代表就是反面磁头 
        CMP        DH,2    ; DH与2 比较
        JB        readloop        ; 如果 DH < 2 , 也就是0或1,则跳转到readloop 
        MOV        DH,0    ; 否则也就是DH=1, 说明一个柱面的正反两面都读完了,则恢复 正面磁头 (DH=0)
        ADD        CH,1    ; CH+1  下一个柱面
        CMP    CH,CYLS
     JB        readloop        ; 如果CH < CYLS ,则跳转到readloop

JB (jump if below),小于则跳转,

CLYS,就是一个常量(意思是cylinders柱面),

程序开头使用EQU指令:

CYLS EQU 10,意思是 CYLS=10 相当于C语言的#define命令。

现在这个程序已经可以用从软盘读取的数据填满内存0x08200 ~ 0x34FFF,  0x34FFF - 0x8200 =  0x2CDFF = 183807, 算上0x8200本身,一共是183808字节(179.5KB),算上系统加载时自动装载的启动区(512字节):183808 +512=184320(字节)=180K

3.5 着手开发操作系统

将文件保存到磁盘镜像文件里

写一个非常小的程序 haribote.nas:

fin:
        HLT
        JMP        fin

将镜像文件写入磁盘(如软盘),打开这个软盘,把一个考进去,最后再把磁盘备份为一个镜像文件。这一系列操作可以通过镜像工具完成,如书中用的edimg.exe

projects/03_day/harib00e下的Makefile修改如下:

TOOLPATH = ../z_tools/
MAKE     = $(TOOLPATH)make.exe -r
NASK     = $(TOOLPATH)nask.exe
EDIMG    = $(TOOLPATH)edimg.exe
IMGTOL   = $(TOOLPATH)imgtol.com
COPY     = copy
DEL      = del

# 默认动作

default :
    $(MAKE) img

# 文件生成规则

ipl.bin : ipl.nas Makefile
    $(NASK) ipl.nas ipl.bin ipl.lst

haribote.sys : haribote.nas Makefile
    $(NASK) haribote.nas haribote.sys haribote.lst

haribote.img : ipl.bin haribote.sys Makefile
    $(EDIMG)   imgin:../z_tools/fdimg0at.tek 
        wbinimg src:ipl.bin len:512 from:0 to:0 
        copy from:haribote.sys to:@: 
        imgout:haribote.img

# 命令

img :
    $(MAKE) haribote.img

run :
    $(MAKE) img
    $(COPY) haribote.img ..z_toolsqemufdimage0.bin
    $(MAKE) -C ../z_tools/qemu

install :
    $(MAKE) img
    $(IMGTOL) w a: haribote.img

clean :
    -$(DEL) ipl.bin
    -$(DEL) ipl.lst
    -$(DEL) haribote.sys
    -$(DEL) haribote.lst

src_only :
    $(MAKE) clean
    -$(DEL) haribote.img

然后执行make img, 得到haribote.img, 用二进制编辑器查看haribote.img,在0x2600的地方看到:

0x2600附近

0x4200附近,这里的F4 EB FD 其实就是上面 haribote.nas中的代码的机器码(我理解是这样)

向软盘保存文件地,文件名会写在0x2600以后的地方,

文件的内容会写在 0x4200以后的地方。

我们将操作系统本身的内容写到haribote.sys文件中,再把它保存到磁盘镜像文件里,然后从启动区执行这个haribote.sys就行了。也就是软盘上0x004200号地址的程序。

3.6 从启动区执行操作系统

程序启动区(C0-H0-S1,软盘上正面第0柱面第一扇区)的内容加载到了0x7C00 ~ 0x7DFF(512字节),软盘上第二个扇区开始一直到第10个柱面(反面)的最后一个扇区将内容加载到 0x08200 ~ 0x34FFF(179.5KB)(推导:0x34FFF - 0x8200 =  0x2CDFF = 183807, 算上0x8200本身,一共是183808字节)

所以软盘上0x4200处的内容应该位于内存0x8000 + 0x4200 = 0xc200号地址。(说明:因为软盘上第二个扇区对应内存上的是0x8200,那么软盘上的开始位置就对应的是内存上的0x8000,所以软盘上的0x4200,就相当于内存上的0x8000+0x4200

在haribote.nas中加上ORG 0xc200(注意是haribote.nas ,不是ipl.nas),然后在ipl.nas处理的最后加上JMP 0xc200这个指令,得到 03_day/harib00f/文件夹中的内容。

haribote.nas:

; haribote-os
; TAB=4

        ORG        0xc200            ;ORG 命令 表示这个程序将要被装载到内存中的什么地方
fin:
HLT JMP fin

ipl.nas:

next:
      MOV       AX, ES   ;把内存地址后移0x20
      ADD       AX, 0x0020  
  ...省略中间代码...
    
JB readloop ; 如果CH < CYLS ,则跳转到readloop ; 因为读完了执行haribote.sys! JMP 0xc200 error: MOV SI,msg
  ...省略代码...

3.7 确认操作系统的执行情况

ORG 命令 表示这个程序将要被装载到内存中的什么地方

疑问:之前用二进制编辑器查看,haribote.sys这个文件保存到haribote.img镜像中时位于0x4200,IPL会将磁盘的除启动区外的前10个柱面的数据写入内存的0x8200位置,上面说过,haribote.sys的内容在内存中对应0xC200,所以在ipl.nas中加入了JMP 0xC200,那为什么在haribote.sys中还要加ORG 0xc200这句呢? 不加会怎样?(实测,这句不加可以正常启动

03_day/harib00g/haribote.nas

; haribote-os
; TAB=4

        ORG        0xc200            ; 这个程序将要被装载到内存中的什么地方

        MOV        AL,0x13            ; VGA显卡,320x200x8位彩色
        MOV        AH,0x00
        INT        0x10
fin:
        HLT
        JMP        fin

设置显卡模式的BIOS中断信息:

  • AH=0x00
  • AL=模式:(省略一些不重要的画面模式)
  • 0x03:16色字符模式,80*25
  • 0x12:VGA图形模式,640*480*4位彩色模式,独特的4面存储模式(16色)
  • 0x13:VGA图形模式,320*200*8位彩色模式,调色板模式 (256色)
  • 0x6a:扩展VGA图形模式,800*600*4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
  • 返回值:无

如果画面模式切换正常,画面会变一片黑(见下面图片),图形模式光标会消失。

ipl.nas改名炒ipl10.nas表示读10个柱面,另外想把磁盘装载内容的结束地址告诉给haribote.sys,所以在JMP 0xc200之前加了 将 CYLS的值(也就是这里的CH写到内存0x0FF0,(为什么是0x0FF0?)

; 因为(磁盘上10个柱面的数据)读完了执行haribote.sys!

        MOV        [0x0ff0],CH        ; 将 读取柱面的数量(此时是10)写到内存的0x0FF0,这句是什么意思?记录 读了多少个柱面?
        JMP        0xc200

改之前的启动画面:

改完之后的启动画面:

3.8 32位模式前期准备

CPU有16位和32位两种模式,以16位模式启动,用AX、CX等16位寄存器会方便,但像EAX、ECX等32位寄存器使用起来会麻烦。16位和32位模式中机器语言命令代码不一样,同样的机器语言解释方法也不一样,所以16位和32位模式下机器语言不通用。

CPU的自我保护功能在16位下不能用。在32位下能用。

32位模式不能调用BIOS功能。BIOS是16位机器语言写的。如果有什么事情想用BIOS来做,就全部放在开头。比如画面模式的设定。

从BIOS换得键盘状态,指NumLock是开还是关。

03_day/harib00h/haribote.nas

; haribote-os
; TAB=4

; 有关BOOT_INFO
CYLS    EQU        0x0ff0            ; 设定启动区
LEDS    EQU        0x0ff1
VMODE    EQU        0x0ff2            ; 关于颜色数目的信息,颜色的位数
SCRNX    EQU        0x0ff4            ; 分辨率的X(screen X)
SCRNY    EQU        0x0ff6            ; 分辨率的Y(screen Y)
VRAM    EQU        0x0ff8            ; 图像缓冲区的开始地址

        ORG        0xc200            ; 这个程序将要被装载到内存中的什么地方
        MOV        AL,0x13            ; VGA显卡,320x200x8位彩色
        MOV        AH,0x00
        INT        0x10

        MOV        BYTE [VMODE],8    ; 记录画面模式
        MOV        WORD [SCRNX],320
        MOV        WORD [SCRNY],200
        MOV        DWORD [VRAM],0x000a0000

; 用BIOS 取得键盘上各种LED指示灯的状态

        MOV        AH,0x02
        INT        0x16             ; keyboard BIOS
        MOV        [LEDS],AL

fin:
        HLT
        JMP        fin

上面的BOOT_INFO是启动信息,

因为320的二进制是 1 0100 0000, 所以这里用WORD保存,200同理。

---------------------------------------------------------------------------------------

VRAM指显卡内存,它的各地址都对应着画面上的像素。VRAM分布在内存分布图上好几个不同地方,因为不同画面模式像素数不同,可以使用的内存不一样,所以把VRAM地址保存在BOOT_INFO里以备后用。

通过BIOS中断INT 0x10这个中断信息查询,可以得知这种画面模式下VRAM是0xA0000 ~ 0xAFFFF的64KB

从内存分布图上看,0x0FF0这一块并没有使用,所以把分辨率,颜色数,键盘状态都存在这个位置附近。

3.9 开始导入C语言

03_day/harib00i/

haribote.nax改名为asmhead.nas,它的前半部分用汇编写的,后半部分用C语言写的。为了调用C语言写的程序,加了100行左右汇编代码。(暂时不讲)

C语言部分 文件名 bootpack.c

void HariMain(void)
{

fin:
    /* 这里想写上HLT,但C语言中不能用HLT */
    goto fin;

}

第一行是定义函数,函数名是HariMain,参数为void,返回值为void。goto相当于汇编中的JMP,实际上也是编译成JMP指令。

-------------------------------------------------------------------------------------------------------------------

.c文件编译成机器语言的步骤:

  • 用cc1.exe 从 .c 生成.gas
  • 用gas2nask.exe 从 .gas 生成 .nas
  • 用nask.exe 从.nas 生成 .obj
  • 用obi2bim.exe 从 .obj 生成 .bim
  • 用 bim2hrb.exe 从 .bim 生成 .hrb
  • 用 copy指令 将 asmhead.bin 与 bootpack.hrb结合起来,得到haribote.sys

cc1是C编译器,将C语言编译成汇编语言源程序,这个是用gcc改造的,输出的是gas汇编语言源程序,

gas2nask,把gas变换成nask语法。

nask 将.nas文件转成目标文件。目标文件是一种特殊的机器语言文件,必须与其他文件链接后才能变成真正可以执行的机器语言

链接:C语言的局限性,不可能只用C语言编写所有的程序,所以其中有一部分必须用汇编来写,然后链接到C语言写的程序上。

单个的目标文件还不是独立的机器语言,为了做成完整的机器语言文件,必须将必要的目标文件全部链接上,使用obj2bim。(binary image,二进制镜像文件)。

bim 还不是完成品,只是将各部分全部链接在一起,做成一个完整的机器语言文件。为了实际使用还要针对不同操作系统的要求进行必要的加工。如加上识别用的文件头,或压缩等。为了本书要做的系统,作者开发了一个bim2hrb.exe。

-------------------------------------------------------------------------------

我们平时用的C编译器没有这么复杂的,是因为它内部也做了同样的步骤,这里的编译器是以能适应不同操作系统为前提而设计的。是特意像这样多生成一些中间文件的。好处是仅靠这个编译器就可以制作windows,linus,还有本书要开发的操作系统的可执行文件。

Makefile也做了很大修改。

----------------------------------------------------------------------------

程序是从HariMain()函数开始的,所以这个函数名不能改。

还是执行make run ,看到黑屏就说明正常启动了。

3.10 实现HLT

; naskfunc
; TAB=4

[FORMAT "WCOFF"]                ; 制作目标文件的模式 
[BITS 32]                        ; 制作32位模式用的机器语言


; 制作目标文件的信息

[FILE "naskfunc.nas"]            ; 源文件名信息

        GLOBAL    _io_hlt            ; 程序中包含的函数名


; 以下是实际的函数

[SECTION .text]        ; 目标文件中写了这些之后再写程序

_io_hlt:    ; void io_hlt(void);
        HLT
        RET

用汇编语言写了一个函数io_hlt 。

HLT 属于 I/O指令

MOV 属于传送指令

ADD 属于演算指令

用汇编写的函数之后还要与bootpack.obj链接,所以也要编译成目标文件,因此将输出格式设定为WCOFF模式,还要设定32位机器语言模式。

在nask目标文件模式下,必须设定文件名信息,然后再写明下面程序的函数名,要在函数名前加 "_",否则就不能很好地与C语言函数链接。需要链接的函数名都要用GLOBAL指令声明。

下面写一个实际的函数,先写一个与GLOBAL声明的函数名相同的标号(label),从此处开始写代码就可以,RET相当于C语言的return。

在C语言里使用这个函数

bootpack.c

/* 告诉C编译器,有一个函数在别的文件里 */

void io_hlt(void); 

/* 函数声明不用{}, 而用; 表示函数在别的文件中,你自己找一下吧 */

void HariMain(void)
{

fin:
    io_hlt(); /* 执行naskfunc.nas里的_io_hlt */
    goto fin;

}

Makefile也进行了修改,还是执行make run ,结果依然是黑屏,程序是正常的。

喜欢的话,请点赞,转发、收藏、评论,谢谢!
原文地址:https://www.cnblogs.com/johnjackson/p/12319036.html