001. 注释过的boot.s

从网上搜罗一个很详细注释的boot.s版本,加了小小一点点自己的理解,不太多.

用 as86, ld86 可以编译,   ubuntu下可以通过 apt install bin86 来安装好像.

    ; 程序描述:  
    ;   boot.s程序编译出的代码共512字节,将被存放在软盘映像文件的第一个扇区中。PC在加电启动时,
    ;   BIOS程序会把启动盘上第一个扇区加载到物理内存0x7c00位置开始处,
    ;   然后跳转到0x7c00处开始执行boot.s程序代码。
    ;   本程序(boot.s程序)将内核代码(head.s代码)加载到0x10000处,然后再移动到0x0处0,  
    ;   注意;加载到0x0处是为了设置GDT表时可以简单一些,因而也可以让head.s程序尽量短一些,  
    ;   但不能一开始就加载到0x0处是因为加载操作需要使用BIOS提供的中断过程,而BIOS使用的  
    ;   中断向量表正处于内存0开始的地方,并且在内存1KB开始处是BIOS程序使用的数据区,所以  
    ;   若直接将head代码加载于此将导致BIOS中断过程不能正常运行。  
    ;   最后进入保护模式,并转到0x0处继续运行。  
    ; ###############################################################################;        
      
 
    BOOTSEG = 0x07c0   ; 前面讲到PC加电启动时会加载本程序到0x7c00处,那为什么这里却是0x7c0,而不
                       ;  是0x7c00呢,因为8086CPU刚启动时是处在实地址模式,实模式下最多寻址1M,
                       ;  并将1MB存储空间分成许多逻辑段,每个段的长度被固定为64K。这样每个存储
                       ;  单元就可以用“段基地址+段内偏移地址”表示。段基地址由16位段寄存器
                       ;  值左移4位表达,段内偏移表示相对于某个段起始位置的偏移量。所以这里的
                       ;  0x07c0实际上是段基地址。
                       ;  所以,是BOOT SEGMENT 启动 段 
                        
    SYSSEG = 0x1000    ; 将head.s加载于此(这里为什么是0x1000而不是0x10000原因同上) ,  系统 段 地址 
 
    SYSLEN = 17        ; 内核占用的最大磁盘扇区数,为了简化程序,这里只能加载长度不超过16个扇区
                       ;  的内核,这个作为BIOS的读扇区功能的参数。问:既然仅限制16个扇区,为何以
                       ;  17作为读入扇区数? 其实这里设置成16系统也能照常运行,多拷贝一个扇区可
                       ;  能出于安全考虑。 只要你的head.s 编译链接后, 不要超过定义的  [扇区数] * 512 个字节就没什么问题
                        
    entry start        ; 这个的作用是什么?汇编器汇编时必须有一个start指明入口地址,否则出现汇编错误 as86 用的,  Gnu as 又是别的伪指令了
    start:  
        jmpi    go, #BOOTSEG    ; jmpi是段间跳转指令,执行的结果是CS寄存器值变为0x7c0,接下来执
                                ;  行“BOOTSEG:go”处的指令。 为什么需要这句话呢?不写不也是从下
                                ;  面顺序执行吗? 答:刚开机时所有段寄存器(包括CS)的值为0,数据传
                                ;  送指令是不能把数据传送给CS的,因为CS是代码段寄存器,CS如果被修
                                ;  改程序就无法执行。所以必须用jmpi把cs改为0x7c0。
                                ;  这里要特别注意的是, 加电后, Bios最后一步,只是把 磁盘第一扇区为 
                                ;  AA55 那512字节的内容,加载到 7c00处而已,CS段,依然是 0,所以才需要这里的jmpi 来这只CS

    go:  mov     ax, cs         ; ax是属于通用寄存器之一的累加寄存器  
         mov     ds, ax    
         mov     ss, ax      ; 让两个段寄存器ds和ss指向0x7c0段,问:1.为何需要让这两个段寄存
                             ;  器指向这里? 2.为何要通过ax间接传递数据而不能直接赋值呢?答:
                             ;  1.这里是为了让DS和SS指向和代码段一致的段,ss里面存放堆栈段的
                             ;   段地址,sp存放偏移地址,物理地址=ss* 10H+ sp。这样结合下面一
                             ;   句,堆栈从物理地址0x7c00+0x400开始,留1K的代码空间
                             ;  2. 80x86 中规定不能对段寄存器(CS,DS等)直接给立即数 
                             ; 说明: ebp 栈底指针, esp-栈顶指针,ebp不知道啥时候用, 但是esp是从高-低发展的, push ax;入栈操作 ,会导致  esp-2.
                              
         mov     sp, #0x400   ; 设置临时栈指针。其值大于程序末端并有一定空间即可。问:1.为何需要
                              ;  一个临时栈指针?  2.这个值怎么定,程序末端在哪里如何计算?1.中断
                              ;  需要使用到堆栈 2.8086堆栈的生长方向为向下增长。boot.s占用512字节
                              ;  ,这里设置成远大于512的任意值就可以。
                              ; 至于中断如何使用堆栈, 可以看看head.s.  0x400 = 1024 , 这里 boot.o 占用了512个字节,所以,这里 从 1024 -> 513 ,就留给了栈空间.
                              ; head.s被加载到了 0x10000 ,所以,更加不会影响到这个区域,再加上,是临时用的话,只要 head.s没有被拷贝到0位置,就不碍事

                              ; 现在加载内核代码(head.s程序)至0x10000开始处  
    load_system:              ; 问:标号有没有实际作用?标号指明其所在位置的地址
                              ;  首先介绍一下BIOS的0x13的0x02号功能  
                              ;  BIOS INT 0x13的0x02号功能 - 读扇区  
      
                              ;  INT 0x13/AH=0x02 - 将磁盘上的扇区读入内存  
                              ;  AH = 0x02  
                              ;  AL = 要读入的扇区数  
                              ;  CH = 柱面(磁道)号的低8位  
                              ;  CL = 位7、6是柱面(磁道号)高2位,位5-0是读入的起始扇区号(从1计,
                              ;       第一扇区存放的是boot.s,第二扇区开始放的是head.s,这里要
                              ;       读的是head.s所以从第二个扇区开始读)
                              ;  DH = 磁头号  
                              ;  DL = 驱动器号  
                              ;  ES:BX = 缓冲区(用于保存读入扇区)的位置  
                              ;  返回值:  
                              ;  AH = 状态码  
                              ;  AL = 读到的扇区数  
                              ;  CF = 失败为1,成功为0       
                              ; 这里只是操作硬盘IO的功能函数,了解一下即可.不会对系统的理解造成影响 
        mov ch, #0x00         ;  磁道号的 低八位  
        mov cl, #0x02         ; 问:为什么是加载2号扇区?答:因为磁盘的第一扇区放置的即是本程序
                              ;  (引导启动程序boot.s),而紧邻的第二扇区开始则放置内核代码head.s
                              ;  。扇区号从1开始计算。
                              ;  这里,是通过Makefile 的 dd 命令把 boot 和  head放到一起的,并不是一起编译的, as86 ld86 编译链接 boot , Gnu as ,Gnu ld 编译链接的 head --> system, 
                              ;  从Makefile 很容易就能看出来
        mov dh, #0x00         ; 磁头号  
        mov dl, #0x00         ; 问:驱动器是指什么?这里的驱动器号是0,表示floppya,即第一个软盘驱动器。
        mov ax, #SYSSEG       ; 不能直接执行mov es, #SYSSEG,编译时会出现illegal immediate mode
        mov es, ax            ;   错误,因为80x86中规定不能对段寄存器(CS,DS等)直接给值/立即数
                              ; 上面介绍了 es:bx 指定了数据到从磁盘中读取到内存的哪儿, 所以 es = 0x1000, bx = 0,就是我们想要的  0x10000 处了. 
         
        xor bx, bx            ;  将内核放置于1000:0000位置处  , xor 异或, 只是为了复位为0
        mov ah, #0x02  
        mov al, #SYSLEN  
        int 0x13              ;  设置好各项参数后即可调用BIOS的0x13功能  , 如果出错 CF会被置位 1
        jnc ok_load           ;   jnc(jump not c)是一跳转指令,当进位标记C为0时跳转,为1时执行后面的指令 
   die:    jmp die            ;  这里其实可以打印出错结果的 Todo: ??????
          
        ; 到目前为止我们已将内核代码从磁盘读入到内存中指定位置了,下面就开始将内核
        ;  代码转移到0x0这个内存开始位置。共移动8K字节((16个扇区*512B/每扇区)/1024=8KB)。  
        ; 
    ok_load:  
        cli            ; 关中断    问:为何在开始移动时要关中断,是为了防止什么事件的发生吗?
                       ;  若不关会怎样?  
                       ;  在搬移之前先介绍一下REP指令及MOVW指令       
                       ;  REP:重复前缀,字符串操作本身每次只处理一个内存值,但如果使用重复前
                       ;  缀的话,该指令就会使用ECX作为计数器进行重复。换句话说,就是可以用一
                       ;  条指令处理整个数组。  
                       ;  MOVW:将DS:SI(源变址寄存器)的内容送至ES(附加段数据寄存器):DI
                       ;  (目的变址寄存器),是复制过去,原来的代码还在。  
                       ;  附:ES和DS的功能相同,程序中设有多个数据段时,可以选用ES寄存器。一般
                       ;  在串处理时用得比较多。比如将一段内存空间存储的数据复制到另一段空间,可
                       ;  以分别设置DS:SI指向源存储数据的地址,ES:DI指向目的存储数据的地址  
                       #
        mov ax, #SYSSEG     ; 移动开始位置:DS:SI=0x1000:0;目的位置:ES:DI=0:0  
        mov ds, ax          ; DS = 0x1000
        xor ax, ax          ; 清零
        mov es, ax          ; 目标  0
        mov cx, #0x1000     ; 设置共移动4K次,每次1个字(即移动16个扇区的代码)。  cx 计数寄存器, 0x1000 = 4096 
        sub si, si          ; si 清零
        sub di, di          ; di 清零
        rep 
        movw                ; 执行重复移动指令  , 貌似as86 需要换行,才会编译通过
        ; ##################################################################################;       
      
                ;     加载IDT和GDT基地址寄存器IDTR和GDTR  
        mov ax, #BOOTSEG  
        mov ds, ax      ; 让DS重新指向0x7c0段(问:1.不一定要让数据段指向这个位置吧? 答:这里必
                        ; 须让ds重新指向0x07c0段,因为lidt和lgdt隐含的完整格式上是ds:idt_operand和
                        ; ds:gdt_operand,会在ds:operand这个位置去寻找它们的六字节操作数。2.这时
                        ; 的情况是不是:0x0-0x2000:简单内核的代码区;0x7c00:数据段起始地址;
                      
        lidt idt_48         ; 加载IDTR。6字节操作数:2字节表长度,4字节线性基地址  
        lgdt gdt_48         ; 加载GDTR。6字节操作数:2字节表长度,4字节线性基地址  
      
            ;    设置好了中断描述符表IDT和全局描述符表GDT,并且加载好IDTR和GDTR后,准备进入保护模式  
            ;   设置控制寄存器CR0(即机器状态字),进入保护模式。段选择符值8对应GDT表中第2个段描述符  
            ;   控制寄存器(CR0、CR1、CR2和CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性
            ;    CR0中含有控制处理器操作模式和状态的系统控制标志  
            ;    CR1保留不用  
            ;    CR2含有导致页错误的线性地址  
            ;    CR3含有页目录表物理内存基地址(因此该寄存器也被称为页目录基地址寄存器PDBR)  
            
        mov ax, #0x0001 ; (操作数的第四位是0x1=0001,将传给CR0)  
          ; 先介绍一下LMSW指令:LMSW: Load Machine Status Word(置处理器状态字)  只有操作数的低4
          ;  位被存入CR0,只有PE(位0),MP(位1)和EM(位2)和TS(位3)被改写,CR0其他位不受影响。  
        lmsw    ax      ; 设置CR0,进入保护模式。 
            ; 先介绍下JMPI指令(段间跳转指令),在实模式下JMPI B, A 是跳到以A为段基地址,以B为偏
            ; 移地址处执行,在保护式下JMPI B,A是跳转到以A为段选择符,偏移为B处执行。JMP是段内的跳转
    jmpi    0, 8    ; 然后跳转至段选择符指定的段中,偏移0处  jmpi: 偏移, 段,其实就是设置了 cs= 0x8, ip = 0x0
            ; 注意此时段值已是段选择符。该段的线性基地址是0。  
            ;   这个8是怎么来的?这个8(实际是 0x0008 ,二进制:0000000000001 0 00)是段选择符,由
            ;   段选择符的定义可知,该选择符选择的是RPL为0,TI=0,表示索引GDT,描述符索引=0x1, 索引为1的GDT描述符,前面的lgdt指令已
            ;   经在GDTR寄存器中存放了GDT表的位置跟长度,每个段描述符固定占用8字节,所以根据索引
            ;   值就可以找到GDT表中的段描述1,可以看到该段基地址是0x0000000,加上偏移值0,上面只是置位了PE为1, PG没动, 所以只开启了保护,没有开启分页.
            ;   这里没有开启分页保护,所以是跳到物理地址0处执行。
             
      ;下面是全局描述符表GDT的内容。其中包含3个段描述符。第1个不用,另2个是代码和数据段描述符 
    gdt:    .word   0,0,0,0       ; 段描述符0,不用。每个描述符项占8字节。 
      
            .word   0x07FF        ; 段描述符1,7ff是段限长的0-15位, =8M,(2048*4096 = 8M)
            .word   0x0000        ; 段基地址是0x00000000
            .word   0x9A00        ;9A= 1001 1010  基地址16-23=0, type=1010, P/DPL/S = 1 00 1, s=1 代码/数据段, TYPE=1010 = 可读可执行的代码段
            .word   0x00C0        ; C0 = 1100 0000 颗粒度:4K  0000段限长的一部分, =0,不用在意  1100 G/DB/0/AVL, G 粒度,1=4K,DB=1 ,32位段
      
            .word   0x07FF        ; 段描述符2
            .word   0x0000        ; 同上 
            .word   0x9200        ; 92 1001 0010, 还是 P存在, S=1,代码/数据段 TYPE=0010 , 可读写数据段
            .word   0x00C0        ;同上
      
           ; 下面分别是LIDT和LGDT指令的6字节操作数  
    idt_48: .word   0               ; idt 段限长 0
            .word   0,0             ; 基地址 0x0
    gdt_48: .word   0x7ff           ; gdt 段限长  2047, 一个描述符8个字节, 大约存 256个描述符, 实际上面就存了 3个, 第一个CPU规定,必须默认是 null
            .word   0x7c00+gdt,0    ; 基地址 这里gdt的偏移不会太大,所以,可以理解成, 高四位全是0, 低四位 0x7c00 +gdt,说白了, 就是相对于启动地址7c00再偏移到 gdt标签定义的那里
                                    ; 这里的段限长,其实主要是用来检查的, 如果非要读取gdt的 第3项之后的东西, 肯定是有问题的,毕竟就定义了3个项,但是有限制长度是256,不出错才有鬼
      
    .org 510    ;   问:这是什么?
                ;   答:.org指令表示以后的内容从510字节开始存放,下面的AA55是引导
                ;  扇区的结束标志,占二字节,这就是为什么boot.s刚好占512字节的原因,
                ;  如果该标志错误系统就不能启动。
                
    .word   0xAA55 ; 启动扇区标识, 第一扇区512的最后两个字节是AA55即可
 

猜你喜欢

转载自www.cnblogs.com/davytitan/p/12097191.html