zoukankan      html  css  js  c++  java
  • 自制操作系统笔记-第三章

    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 ,结果依然是黑屏,程序是正常的。

    喜欢的话,请点赞,转发、收藏、评论,谢谢!
  • 相关阅读:
    WebGL_0008:支持移动端的控制台调试工具
    调整两数组元素使得两数组差值最小
    集五福
    打印机顺序打印
    子弹分发
    字符串分割
    乐观锁、悲观锁
    字符串去重
    数组最后剩下的数字
    shell常用工具
  • 原文地址:https://www.cnblogs.com/johnjackson/p/12319036.html
Copyright © 2011-2022 走看看