1. /*程序描述:   

  2.     ;   boot.s程序编译出的代码共512字节,将被存放在软盘映像文件的第一个扇区中。PC在加电启动时, 

  3.     ;   BIOS程序会把启动盘上第一个扇区加载到物理内存0x7c00位置开始处, 

  4.     ;   然后跳转到0x7c00处开始执行boot.s程序代码。 

  5.  

  6.     ;   本程序(boot.s程序)将内核代码(head.s代码)加载到0x10000处,然后再移动到0x0处0,   

  7.     ;   注意;加载到0x0处是为了设置GDT表时可以简单一些,因而也可以让head.s程序尽量短一些,   

  8.     ;   但不能一开始就加载到0x0处是因为加载操作需要使用BIOS提供的中断过程,而BIOS使用的   

  9.     ;   中断向量表正处于内存0开始的地方,并且在内存1KB开始处是BIOS程序使用的数据区,所以   

  10.     ;   若直接将head代码加载于此将导致BIOS中断过程不能正常运行。   

  11.     ;   最后进入保护模式,并转到0x0处继续运行。   

  12.     */          

  13.         

  14.   

  15.     BOOTSEG = 0x07c0   /*前面讲到PC加电启动时会加载本程序到0x7c00处,那为什么这里却是0x7c0,而不 

  16.                          是0x7c00呢,因为8086CPU刚启动时是处在实地址模式,实模式下最多寻址1M, 

  17.                          并将1MB存储空间分成许多逻辑段,每个段的长度被固定为64K。这样每个存储 

  18.                          单元就可以用“段基地址+段内偏移地址”表示。段基地址由16位段寄存器 

  19.                          值左移4位表达,段内偏移表示相对于某个段起始位置的偏移量。所以这里的 

  20.                          0x07c0实际上是段基地址。 

  21.                         */  

  22.     SYSSEG = 0x1000    //将head.s加载于此(这里为什么是0x1000而不是0x10000原因同上)   

  23.   

  24.     SYSLEN = 17        /*内核占用的最大磁盘扇区数,为了简化程序,这里只能加载长度不超过16个扇区 

  25.                          的内核,这个作为BIOS的读扇区功能的参数。问:既然仅限制16个扇区,为何以 

  26.                          17作为读入扇区数? 其实这里设置成16系统也能照常运行,多拷贝一个扇区可 

  27.                          能出于安全考虑。 

  28.                         */  

  29.     entry start        //这个的作用是什么?汇编器汇编时必须有一个start指明入口地址,否则出现汇编错误   

  30.     start:    

  31.         jmpi    go, #BOOTSEG    /*jmpi是段间跳转指令,执行的结果是CS寄存器值变为0x7c0,接下来执 

  32.                                   行“BOOTSEG:go”处的指令。 为什么需要这句话呢?不写不也是从下 

  33.                                   面顺序执行吗? 答:刚开机时所有段寄存器(包括CS)的值为0,数据传 

  34.                                   送指令是不能把数据传送给CS的,因为CS是代码段寄存器,CS如果被修 

  35.                                   改程序就无法执行。所以必须用jmpi把cs改为0x7c0。 

  36.                                  */  

  37.     go:  mov     ax, cs          //ax是属于通用寄存器之一的累加寄存器     

  38.          mov     ds, ax      

  39.          mov     ss, ax      /*让两个段寄存器ds和ss指向0x7c0段,问:1.为何需要让这两个段寄存 

  40.                                器指向这里? 2.为何要通过ax间接传递数据而不能直接赋值呢?答: 

  41.                                1.这里是为了让DS和SS指向和代码段一致的段,ss里面存放堆栈段的 

  42.                                段地址,sp存放偏移地址,物理地址=ss* 10H+ sp。这样结合下面一 

  43.                                句,堆栈从物理地址0x7c00+0x400开始,留1K的代码空间 2. 80x86 

  44.                                中规定不能对段寄存器(CS,DS等)直接给立即数  

  45.                               */  

  46.          mov     sp, #0x400   /*设置临时栈指针。其值大于程序末端并有一定空间即可。问:1.为何需要 

  47.                                 一个临时栈指针?  2.这个值怎么定,程序末端在哪里如何计算?1.中断 

  48.                                 需要使用到堆栈 2.8086堆栈的生长方向为向下增长。boot.s占用512字节 

  49.                                 ,这里设置成远大于512的任意值就可以。 

  50.                                */  

  51.                               //现在加载内核代码(head.s程序)至0x10000开始处     

  52.     load_system:              /*问:标号有没有实际作用?标号指明其所在位置的地址 

  53.                                 首先介绍一下BIOS的0x13的0x02号功能   

  54.                                 BIOS INT 0x13的0x02号功能 - 读扇区   

  55.        

  56.                                 INT 0x13/AH=0x02 - 将磁盘上的扇区读入内存   

  57.                                 AH = 0x02   

  58.                                 AL = 要读入的扇区数   

  59.                                 CH = 柱面(磁道)号的低8位   

  60.                                 CL = 位7、6是柱面(磁道号)高2位,位5-0是读入的起始扇区号(从1计, 

  61.                                      第一扇区存放的是boot.s,第二扇区开始放的是head.s,这里要 

  62.                                      读的是head.s所以从第二个扇区开始读) 

  63.                                 DH = 磁头号   

  64.                                 DL = 驱动器号   

  65.                                 ES:BX = 缓冲区(用于保存读入扇区)的位置   

  66.                                 返回值:   

  67.                                 AH = 状态码   

  68.                                 AL = 读到的扇区数   

  69.                                 CF = 失败为1,成功为0        

  70.                                */  

  71.         mov ch, #0x00    

  72.         mov cl, #0x02         /*问:为什么是加载2号扇区?答:因为磁盘的第一扇区放置的即是本程序 

  73.                                 (引导启动程序boot.s),而紧邻的第二扇区开始则放置内核代码head.s 

  74.                                 。扇区号从1开始计算。 

  75.                                */  

  76.         mov dh, #0x00    

  77.         mov dl, #0x00          //问:驱动器是指什么?这里的驱动器号是0,表示floppya,即第一个软盘驱动器。   

  78.         mov ax, #SYSSEG        /*不能直接执行mov es, #SYSSEG,编译时会出现illegal immediate mode 

  79.                                  错误,因为80x86中规定不能对段寄存器(CS,DS等)直接给值/立即数 

  80.                                 */  

  81.         mov es, ax    

  82.         xor bx, bx              //将内核放置于1000:0000位置处     

  83.         mov ah, #0x02    

  84.         mov al, #SYSLEN    

  85.         int 0x13                //设置好各项参数后即可调用BIOS的0x13功能     

  86.         jnc ok_load             //jnc(jump not c)是一跳转指令,当进位标记C为0时跳转,为1时执行后面的指令    

  87.    die:    jmp die              //死循环   

  88.             

  89.                /*到目前为止我们已将内核代码从磁盘读入到内存中指定位置了,下面就开始将内核 

  90.                  代码转移到0x0这个内存开始位置。共移动8K字节((16个扇区*512B/每扇区)/1024=8KB)。   

  91.                 */  

  92.     ok_load:    

  93.         cli            /*关中断    问:为何在开始移动时要关中断,是为了防止什么事件的发生吗? 

  94.                          若不关会怎样?   

  95.                          在搬移之前先介绍一下REP指令及MOVW指令        

  96.                          REP:重复前缀,字符串操作本身每次只处理一个内存值,但如果使用重复前 

  97.                          缀的话,该指令就会使用ECX作为计数器进行重复。换句话说,就是可以用一 

  98.                          条指令处理整个数组。   

  99.                          MOVW:将DS:SI(源变址寄存器)的内容送至ES(附加段数据寄存器):DI 

  100.                          (目的变址寄存器),是复制过去,原来的代码还在。   

  101.                          附:ES和DS的功能相同,程序中设有多个数据段时,可以选用ES寄存器。一般 

  102.                          在串处理时用得比较多。比如将一段内存空间存储的数据复制到另一段空间,可 

  103.                          以分别设置DS:SI指向源存储数据的地址,ES:DI指向目的存储数据的地址   

  104.                         */  

  105.         mov ax, #SYSSEG    //移动开始位置:DS:SI=0x1000:0;目的位置:ES:DI=0:0     

  106.         mov ds, ax    

  107.         xor ax, ax    

  108.         mov es, ax    

  109.         mov cx, #0x1000    //设置共移动4K次,每次1个字(即移动16个扇区的代码)。     

  110.         sub si, si    

  111.         sub di, di    

  112.         rep movw           //执行重复移动指令     

  113.          

  114.         

  115.                            //加载IDT和GDT基地址寄存器IDTR和GDTR     

  116.         mov ax, #BOOTSEG    

  117.         mov ds, ax    /*让DS重新指向0x7c0段(问:1.不一定要让数据段指向这个位置吧? 答:这里必 

  118.                        须让ds重新指向0x07c0段,因为lidt和lgdt隐含的完整格式上是ds:idt_operand和 

  119.                        ds:gdt_operand,会在ds:operand这个位置去寻找它们的六字节操作数。2.这时 

  120.                        的情况是不是:0x0-0x2000:简单内核的代码区;0x7c00:数据段起始地址; 

  121.                       */  

  122.         lidt idt_48         //加载IDTR。6字节操作数:2字节表长度,4字节线性基地址     

  123.         lgdt gdt_48         //加载GDTR。6字节操作数:2字节表长度,4字节线性基地址     

  124.         

  125.            /*设置好了中断描述符表IDT和全局描述符表GDT,并且加载好IDTR和GDTR后,准备进入保护模式   

  126.              设置控制寄存器CR0(即机器状态字),进入保护模式。段选择符值8对应GDT表中第2个段描述符   

  127.              控制寄存器(CR0、CR1、CR2和CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性 

  128.                 CR0中含有控制处理器操作模式和状态的系统控制标志   

  129.                 CR1保留不用   

  130.                 CR2含有导致页错误的线性地址   

  131.                 CR3含有页目录表物理内存基地址(因此该寄存器也被称为页目录基地址寄存器PDBR)   

  132.             */  

  133.         mov ax, #0x0001 //(操作数的第四位是0x1=0001,将传给CR0)     

  134.           /*先介绍一下LMSW指令:LMSW: Load Machine Status Word(置处理器状态字)  只有操作数的低4 

  135.             位被存入CR0,只有PE(位0),MP(位1)和EM(位2)和TS(位3)被改写,CR0其他位不受影响。   

  136.            */  

  137.         lmsw    ax      //设置CR0,进入保护模式。    

  138.          /*先介绍下JMPI指令(段间跳转指令),在实模式下JMPI B, A 是跳到以A为段基地址,以B为偏 

  139.            移地址处执行,在保护式下JMPI B,A是跳转到以A为段选择符,偏移为B处执行。JMP是段内的跳转 

  140.           */  

  141.     jmpi    0, 8    //然后跳转至段选择符指定的段中,偏移0处     

  142.              /*注意此时段值已是段选择符。该段的线性基地址是0。   

  143.                这个8是怎么来的?这个8(实际是 0x0008 ,二进制:0000000000001 0 00)是段选择符,由 

  144.                段选择符的定义可知,该选择符选择的是RPL为0,索引为1的GDT描述符,前面的lgdt指令已 

  145.                经在GDTR寄存器中存放了GDT表的位置跟长度,每个段描述符固定占用8字节,所以根据索引 

  146.                值就可以找到GDT表中的段描述1,可以看到该段基地址是0x0000000,加上偏移值0,由于这 

  147.                里没有开启分页保护,所以是跳到物理地址0处执行。 

  148.               */  

  149.       //下面是全局描述符表GDT的内容。其中包含3个段描述符。第1个不用,另2个是代码和数据段描述符    

  150.     gdt:    .word   0,0,0,0       //段描述符0,不用。每个描述符项占8字节。    

  151.         

  152.             .word   0x07FF        //段描述符1,段限长8M,(2048*4096 = 8M)   

  153.             .word   0x0000        //段基地址是0x00000000   

  154.             .word   0x9A00        //可读可执行的代码段   

  155.             .word   0x00C0        //颗粒度:4K   

  156.         

  157.             .word   0x07FF        //段描述符2   

  158.             .word   0x0000    

  159.             .word   0x9200    

  160.             .word   0x00C0    

  161.         

  162.            //下面分别是LIDT和LGDT指令的6字节操作数     

  163.     idt_48: .word   0    

  164.             .word   0,0    

  165.     gdt_48: .word   0x7ff    

  166.             .word   0x7c00+gdt,0    

  167.         

  168.     .org 510    /*问:这是什么?答:.org指令表示以后的内容从510字节开始存放,下面的AA55是引导 

  169.                   扇区的结束标志,占二字节,这就是为什么boot.s刚好占512字节的原因, 

  170.                   如果该标志错误系统就不能启动。 

  171.                  */  

  172.     .word   0xAA55