linux c 读写mbr_【原创】从0学习Linux内核(一):用Nasm重写Linux引导启动程序

8f446d2f703661491d0f19964c4d0223.png

一、 背景

Linux引导启动程序就是一台裸机从开机到出现操作系统启动运行的过程,它由3个汇编程序组成:bootsect.S、setup.S和head.s。它们之间的关系是:

b9036bcd3814c77cab43f1714dab195b.png

学习Linux操作系统的第一大难点就是引导启动程序。很多人一进Linux的门可能就倒在了这里,因为要完全读懂这3个汇编程序还是有点困难,尤其是对于长期从事高级语言软件开发的人员来说。这个问题,除了读者水平高低以外,个人觉得这3个汇编程序写得确实不怎么样,程序模块化程度不够,因此可读性不好。另外,有些程序还是AT& T汇编,这个让我无法接受,也没有读下去的动力。

于是,我决定自己动手,用自己最喜欢和熟悉的Nasm重新写出Linux的三个引导启动程序,原则就是尽量保持Linux源码模样的基础上提升可读性:做到引导启动程序每一步在干什么,都有明确的屏幕输出提示。

二、引导程序启动过程

3个汇编程序在启动设备中的原始位置关系如图:

02d9fec6c26fca49e46d568189bb15ff.png

bootsect.S代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第1个扇区)。实际就是我们之前说过的MBR。

setup模块占用随后的4个扇区,逻辑扇区号:2-5。

system模块大约占随后的260个扇区,逻辑扇区号从6开始,大约需要读8个柱面。

3个汇编程序在启动过程中,它们在内存中的位置关系随时间而变化如图:

569f08153506d37ec443a9e87b328827.png

这是当年Linus设计的过程,我们接下来的任务就来严格按照这个过程进行程序编写。

三、 bootsect程序

在bootsect代码执行期间,它会将自己移动到内存绝对地址0x90000开始处并继续执行。该程序的主要作用是首先把从磁盘第2个扇区开始的4个扇区的setup模块(由setup.s编译而成)加载到内存紧接着bootsect后面位置处(0x90200),最后把磁盘上setup模块后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号。

具体到程序,需要我们依次来实现以下核心功能:

1. 将bootsect程序本身从0x07c0复制到0x9000(共1个扇区512B)

完成复制后,程序中有一个设计比较巧妙的地方:

jmp INITSEG:go
go: mov ax,cs  

这就是为什么要把bootsect程序要自身复制到0x9000的原因,因为完成复制且执行跳转指令jmp INITSEG:go后,程序才能刚好从go这个地方继续执行。

但是,我个人理解这一步并不是必须的。可以设计成先将setup读到0x9020,然后直接从MBR内跳转到setup(0x9020)即可,不清楚当初设计复制MBR这段代码到0x9000的意义何在。当然在Linux后面的内核程序中,也用到了在MBR中存储的ROOT_DEV和SWAP_DEV内存数据,所以这么做也有一定的道理。

2. 将setup程序装载到0x9020

共4个扇区4*512B,这是一个从启动设备读取数据到内存的过程。

3.将system程序装载到0x1000

共260个扇区260*512B,这也是一个从启动设备读取数据到内存的过程。

4. 跳转到setup程序所在的目标地址:0x9020

这是bootsect程序执行完成之后,最后的去向。

bootsect程序里面一个比较重要的东西就是如何通过BIOS中断来读取软盘数据到内存,这个在之前我的专栏文章中有详细描述,因此本程序可以直接使用:

kaka:计算机自制操作系统(三):读写磁盘操作​zhuanlan.zhihu.com
b8083b36cacb33252e06b0bd02c1978b.png

四、setup程序

setup的主要作用是利用ROM BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖掉了bootsect程序所在的地方)。然后setup程序将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。

接下来就是加载中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR),开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20 -0x2f。最后设置CPU的控制寄存器CR0(也称机器状态字),进入32位保护模式运行,并跳转到位于system模块最前面部分的head.s程序继续运行。

为了能让 head在32位保护模式下运行, 在本程序中临时设置了中断描述符表(IDT)和全局描述 符表(GDT),并在GDT中设置了当前内核代码段的描述符和数据。后面在head中会根据内核的需要重新设置这些描述符表。

具体到程序,setup模块需要依次实现以下核心功能:

1. 取扩展内存的大小值(KB)。

2. 检查显示方式(EGA/VGA)并取参数。

3. 取显示卡当前显示模式。

4. 取第一个硬盘的信息(复制硬盘参数表)。

5. 取第2个硬盘的信息(复制硬盘参数表)。

6. 检查系统是否有第2个硬盘,如果没有则把第2个表清零。

7. 将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。

8.准备进入32位保护模式:

(1) 初始化8259A芯片

(2) 打开A20地址线

(3) 设置GDT,IDT表

(4) 打开保护模式开关

9.进入32位保护模式

五、 head程序

head.s程序在被编译生成目标文件后会与内核其他程序的目标文件一起被链接成system模块,并位于system模块的最前面开始部分。这也就是为什么称其为头部(head)程序的原因。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始放置。

具体到程序,system模块中的head程序需要依次实现以下核心功能:

1. 重置GDT

实际上新设置的GDT表与原来在setup.s程序中设置的GDT表描述符除了在段限长上有些区别以外(原为8MB,现为16MB),其他内容完全一样。这里重新设置GDT的主要原因是为了把GDT表放在内存内核代码比较合理的地方。而前面设置的GDT表处于内存0x902XX处。这个地方将在内核初始化后用作内存高速缓冲区的一部分。

需要注意的是,一旦重置GDT表之后,需要一句修改CS值的指令(我在程序中采用的指令是jmp AA:BB)才能触发新的GDT表数据生效。而这在Linux源码中是没有的,个人觉得这多少是一个不太合理的地方,所幸并没有引起问题,但这并不是一个好的习惯。

2. 重置IDT

由于目前不需要精确制作各个中断源的中断服务,因此Linux让所有的256中断都指向这个统一的中断服务程序---称为哑中断,也即建立了256个一样的中断描述符在IDT中。

我编写的程序在Linux的基础上,增加了一个IDT验证测试的环节:

(1) 先是打开键盘中断,然后运行程序在死循环过程中时按下键盘,如果屏幕能正常打印提升信息,则表面外部中断体系正常。

(2) 直接进行系统调用测试:int 0xXX.虽然进入保护模式后不在支持BIOS调用,但是CPU本身是支持int 0xXX指令的,它能出发CPU根据中断向量去中断表IDT中找寻相应的中断服务。因此 如果屏幕能正常打印提示信息,则表面外部中断体系正常。

3. 确认A20地址线开启

我不知道是否必须的,但是Linux源码中有,所以我也一并写了。

4. 分页存储:设置页目录和页表

这部分内容在之前我自己制作的操作系统中并没有涉及,因此这里特别重点学习一下。由于Linux支持虚拟存储技术,因此必须要用到分页存储。我之前自制的操作系统没有打开分页开关,因此实际访问内存的时候,线性地址=物理地址,现在用到了分页技术之后,线性地址到物理地址的映射还需要一次转换。而这个转换的过程需要操作系统进行设置和控制。

446164ace9138c2028acd1aa2fbf5775.png

有了分页机制之后,寻找物理地址的全过程就变成下面的方式:

cae14205ff8f21c6ed77bd57fc38ac15.png

看起来很复杂,其实只要多动手实践,是完全能理解和掌握的。但是如果只是靠反复看书来理解的话,确实有点抽象。所以,动手是关键。

LInux设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序已执行部分将被覆盖掉),紧随后面会放置共可寻址16MB内存的4个页表,并分别设置它们的表项。页目录表项和页表项格式见图所示。其中P是页面存在于内存标志;R/W是读写标志;U/S是用户/超级用户标志;A是页面已访问标志;D是页面内容已修改标志;最左边20比特是表项对应页面在物理内存中页面地址的高20比特位。

这里每个表项的属性标志都被设置成0x07(P=1、U/S=1、R/W=1),表示该页存在、用户可读写。

d398ebb6eecc05be0a3cb3c3a18b1b76.png

在这部分内容中,我和Linux源码的设计也有点不同。Linux是把head程序的前面部分放置在物理地址0开始的地方,然后在中间0x1000-0x5000放置页表,0x5400后再放head程序的后面部分。中间用了一连串的org来定位,搞得人眼花缭乱,要理解它这段设计的分布过程还是需要较高汇编水平的。

而我采用的方法是直接把head程序全部放在0x5400后面(除了物理地址0开始的一条jmp指令),所以我的程序一开始上来就直接将0x1000-0x5400全部填充0,简单明了,更易理解。

5. 转入C程序main()函数入口

方法是利用返回指令ret将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行C程序main()函数。我在程序中用下标_main人造了一个假的C语言main程序,因此暂时不需要写出C程序。

head程序执行结束之后,整体内存的分布如下图:

e150d3ae411423b0ee6604ad6d68eae2.png

我们单独来看一下,此时Linux操作系统中非常重要GDT表分布情况:

0ab860d4fe912eebf7d13d081a3b98c9.png

这个图在我们之前的自制操作系统专栏中有所涉及,相信到这里后会体会更深。

六、 程序调试

Linux中,3个引导启动程序最终生成操作系统映像的过程为:

8e82f06e106bc825f5a5e30524a207c2.png

现在,我们只是实验,不需要借助于Build工具,我这次生成操作系统映像文件的过程为:

1.bootsect:大小1个扇区,512B,汇编后的机器文件为bootsect.bin。汇编命令:

NASM bootsect.asm -o bootsect.bin

2.setup:大小4个扇区,4*512B=2KB,汇编后的机器文件为setup.bin。汇编命令:

NASM setup.asm -o setup.bin

由于我们最终的setup.bin不足2KB,因此需要用0来填充不足部分的机器代码,单独用二进制工具WinHex制作1个全0的填充机器代码文件:setup_blank.bin,文件大小:2KB-setup.bin.

3.system:大小260个扇区,260*512B=130KB。system模块的首部程序head.asm汇编后的机器文件为head.bin。汇编命令:

NASM head.asm -o head.bin

4.生成映像文件

由于以上3个程序在启动设备上是连续存放的,因此我们使用DOS的二进制连接命令copy便可将所有的机器代码连接在一起,即可生成Linux映像文件:

	copy /B setup.bin+setup_blank.bin setup_end.bin

	copy /B bootsect.bin+setup_end.bin boot_setup.bin

	copy /B boot_setup.bin+head.bin Linux.img

我们把所有的命令过程写成makefile,并在生成映像文件后自动启动虚拟机Vmware就是:

default :
	 make.exe -r Linux.img

bootsect.bin : bootsect.asm  Makefile
	NASM bootsect.asm -o bootsect.bin

setup.bin : setup.asm  Makefile
	NASM setup.asm -o setup.bin

head.bin : head.asm  Makefile
	NASM head.asm -o head.bin

setup_end.bin : setup.bin setup_blank.bin Makefile
	copy /B setup.bin+setup_blank.bin setup_end.bin

boot_setup.bin : bootsect.bin setup_end.bin Makefile
	copy /B bootsect.bin+setup_end.bin boot_setup.bin

Linux.img : boot_setup.bin head.bin Makefile
	copy /B boot_setup.bin+head.bin Linux.img
run :
	make.exe
	VMWARE  -x   ../../VWare/Linux/Linux.vmx

最终,计算机启动之后,我们可以清晰的看到Linux三个引导程序的启动过程:我改写的程序除了实时打印当前启动进度详细信息外,还在不同的阶段用了不同颜色的字符串进行区分。

f416fd30864da33e3ea6673fab7defee.png

c35c270512aa7a2fb7531be41611aa20.png

PS:软盘的最大柱面编号应是18。为了能在一个屏幕范围内装下打印信息,我截屏的时候,在程序中临时将最大柱面编号设置成了7,所以图中显示的柱面循环数据是6,7,1,2,3,4,5,6,7...。实际上,真实的柱面循环数据应该是:6,7,8,9,10,11,12,13,14,15,16,17,18,1,2,3,4,5,6,7,8,9......

七、 总结

通过重写Linux的引导启动程序,给我感受最深的就是:进入保护模式后,将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。这个设计非常的巧妙,因为这样做之后,好处会非常的多比如:

1.所有的段寄存器(CSDSESFSGSSS)都可以把基址设置成0。

2.所有的内存数据访问:段内偏移=绝对地址(因为段基址=0),寻址就太方便了。

3.基于以上情况,汇编程序和C程序能直接进行链接耦合,各段寄存器无需重新设置段值。详细原理在之前自制操作系统中已有阐述。

八、 Nasm程序源代码

用Nasm改写的Linux三个引导启动程序,均为本人亲自编写并真机验证通过。为了长时间后都能读懂程序,在本次改写过程中,我对3个源程序几乎做了不留死角的详细备注。

1. 头文件config.inc

;%define   debug             ;不注释调试,注释用于生产
%ifdef    debug
isdebug   equ   1
%else
isdebug   equ   0
%endif


DEF_INITSEG	equ	0x9000    ;MBR程序挪动后的目标地址
DEF_SYSSEG	equ	0x1000    ;SYSEM模块放置地址
DEF_SETUPSEG	equ	0x9020    ;SETUP模块放置地址

2.bootsect.asm

;*********************************************************
;****Linux操作系统Nasm引导程序:bootsect,制作者:Mr.Jiang***
;*************2020-10-17**********************************

%include "config.inc"


SETUPLEN equ 4                ;SETUP模块长度扇区数 
BOOTSEG  equ 0x07c0           ;MBR启动地址 
INITSEG  equ DEF_INITSEG      ;MBR程序挪动后的目标地址0x9000 
SETUPSEG equ DEF_SETUPSEG     ;SETUP模块放置地址0x9020 
SYSSEG   equ DEF_SYSSEG       ;SYSEM模块放置地址0x1000

SETUPSector   equ   2                    ;SETUP开始扇区号
SYSSector     equ   SETUPSector+SETUPLEN ;SYSTEM开始扇区号6
SYScylind     equ   7                    ;SYSTEM读到的柱面数(8*36>260扇区)


;root_dev定义在引导扇区508,509字节处
;当编译内核时,你可以在Makefile文件中指定自己的值。内核映像文件Image的
;创建程序tools/build会使用你指定的值来设置你的根文件系统所在设备号。

ROOT_DEV equ 0 ;根文件系统设备使用与系统引导时同样的设备(不指定);
SWAP_DEV equ 0 ;交换设备使用与系统引导时同样的设备(不指定);
 

;设备号=主设备号*256 + 次设备号(也即dev_no = (major<<8) + minor )
;主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
;0x300 - /dev/hd0 - 代表整个第1个硬盘;
;0x301 - /dev/hd1 - 第1个盘的第1个分区;
;…
;0x304 - /dev/hd4 - 第1个盘的第4个分区;
;0x305 - /dev/hd5 - 代表整个第2个硬盘;
;0x306 - /dev/hd6 - 第2个盘的第1个分区;
;…
;0x309 - /dev/hd9 - 第2个盘的第4个分区;

;次设备号 = type*4 + nr,其中
;nr为0-3分别对应软驱A、B、C或D;type是软驱的类型(2:1.2MB或7:1.44MB等)。
;因为7*4+0=28=0x1c,所以/dev/PS0 指的是1.44MB A驱动器,其设备号是0x021c

jmp  start

start: 
     mov   ax,0        ;BIOS把引导扇区加载到0x7c00时,ss=0x00,sp=0xfffe
     mov   ss,ax       
     mov   sp,BOOTSEG  ;重新定义堆栈0x7c00
     
     mov   ax,BOOTSEG
     mov   ds,ax       ;为显示各种提示信息做准备
     mov   si, welcome 
     call  showmsg     ;打印"Linux"
     
     ;0x021c :/dev/PS0 - 1.44Mb 软驱A盘
     mov   word  [root_dev],0x021c   
     ;不指定,将软驱A设置成根文件系统设备保存在root_dev 
     

        ;1.将bootsect程序从0x07c0复制到0x9000(共1个扇区512B) 
        mov	ax, INITSEG
	mov	es,ax
	mov	cx, 256	
	sub	si,si
	sub	di,di
	rep                  ;循环挪动次数=512B/16B
	movsw                ;一次挪动16B, 
        jmp	INITSEG:go

;完成复制后,CPU将会跳转到这里 
go:	mov	ax,cs        ;到新的段地址后重新设置DS 
        mov     ds,ax        ;为显示各种提示信息做准备
        mov     si, msg1    
        call    showmsg      ;打印必要信息 
        
        mov   ax,cs        ;重新定义堆栈,栈顶:0x9ff00-12(参数表长度=0x9fef4
        mov   ss,ax        ;因为栈顶后面安排了一个长度12的自建驱动器参数表
        mov   sp,0xfef4    ;刨除掉SS段值0x9000*10后,SP的偏移量是0xfef4   

        ;2.将setup程序装载到0x9020(共4个扇区4*512B)
        mov     si, msg2
        call    showmsg 
        mov	ax, SETUPSEG            ;设置setup装载到的目标段地址
        mov	es,ax                   ;设置setup装载到的目标段地址    
        mov     byte [sector+11],SETUPSector    ;设置开始读取的扇区号:2 
        call    loadsetup
     
     
        ;3.将system程序装载到0x1000(共240个扇区4*512B)
        mov     si, msg3
        call    showmsg          
        mov	ax, SYSSEG            ;设置system装载到的目标段地址
        mov	es,ax                 ;设置system装载到的目标段地址
        mov     byte [sector+11],SYSSector      ;设置开始读取的扇区号:6
        call    loadsystem
        ;jmp     $                    ;调试 
       
        jmp     SETUPSEG:0            ;bootsect运行完毕,跳到setup:0x9020 


showmsg:                              ;打印字符串子程序     
     call  newline
     call  printstr
     call  newline 
     ret
     


;读软盘逻辑扇区2-5共4个扇区  
loadsetup:       
     call    read1sector
     MOV     AX,ES
     ADD     AX,0x0020                  ;一个扇区占512B=200H,刚好能被整除成完整的段
     MOV     ES,AX                      ;因此只需改变ES值,无需改变BX即可。
     inc     byte [sector+11]             ;读完一个扇区
     cmp     byte [sector+11],SETUPLEN+1+1 ;读到的结束扇区
     jne     loadsetup 
     ret
     
 


;读软盘逻辑扇区6-8*36共282个扇区
loadsystem:          
     call    read1sector
     MOV     AX,ES
     ADD     AX,0x0020           ;一个扇区占512B=200H,刚好能被整除成完整的段
     MOV     ES,AX               ;因此只需改变ES值,无需改变BX即可。 
     inc   byte [sector+11]       ;读完一个扇区
     cmp   byte [sector+11],18+1  ;最大扇区编号18,
     jne   loadsystem             
     mov   byte [sector+11],1
     inc   byte [header+11]       ;读完一个磁头
     cmp   byte [header+11],1+1   ;最大磁头编号1
     jne   loadsystem             
     mov   byte [header+11],0
     inc   byte [cylind+11]        ;读完一个柱面
     cmp   byte [cylind+11],SYScylind+1
     jne   loadsystem            

     ret
     
     
numtoascii:     ;将2位数的10进制数分解成ASII码才能正常显示。
                ;如柱面56 分解成出口ascii: al:35,ah:36
     mov ax,0
     mov al,cl  ;输入cl
     mov bl,10
     div bl
     add ax,3030h
     ret

readinfo:       ;实时显示当前读到哪个扇区、哪个磁头、哪个柱面 
     mov si,cylind
     call  printstr
     mov si,header
     call  printstr
     mov si,sector
     call  printstr
     ret


 
read1sector:  ;读1扇区通用程序。扇区参数由 sector header  cylind控制

       mov   cl, [sector+11]   ;为了能实时显示读到的物理位置
       call  numtoascii
       mov   [sector+7],al
       mov   [sector+8],ah

       mov   cl,[header+11]
       call  numtoascii
       mov   [header+7],al
       mov   [header+8],ah

       mov   cl,[cylind+11]
       call  numtoascii
       mov   [cylind+7],al
       mov   [cylind+8],ah

       MOV        CH,[cylind+11]    ;柱面开始读
       MOV        DH,[header+11]    ;磁头开始读
       mov        cl,[sector+11]    ;扇区开始读        

        call       readinfo        ;显示软盘读到的物理位置
        mov        di,0
retry:
        MOV        AH,02H    ; AH=0x02 : AH设置为0x02表示读取磁盘
        MOV        AL,1      ; 要读取的扇区数
        mov        BX,    0  ; ES:BX表示读到内存的地址
        MOV        DL,00H    ; 驱动器号,0表示软盘A,硬盘C:80H C 硬盘D:81H
        INT        13H       ; 调用BIOS 13号中断,磁盘相关功能
        JNC        READOK    ; 未出错则跳转到READOK,出错的话EFLAGS的CF位置1
           inc     di
           MOV     AH,0x00
           MOV     DL,0x00   ; A驱动器
           INT     0x13      ; 重置驱动器
           cmp     di, 5     ; 软盘很脆弱,同一扇区如果重读5次都失败就放弃 
           jne     retry

           mov     si, Fyerror
           call    printstr
           call    newline
           jmp     exitread
READOK:    mov     si, FloppyOK
           call    printstr
           call    newline
exitread:
           ret




printstr:                  ;显示指定的字符串, 以'$'为结束标记 
      mov al,[si]
      cmp al,'$'
      je disover
      mov ah,0eh
      int 10h
      inc si
      jmp printstr
disover:
      ret

newline:                     ;显示回车换行
      mov ah,0eh
      mov al,0dh
      int 10h
      mov al,0ah
      int 10h
      ret

welcome db '(i)Linux-bootsect!','$'

msg1 db '1.bootsect to 0x9000','$'
msg2 db '2.setup to 0x9020','$'
msg3 db '3.system  to 0x1000','$'

cylind  db 'cylind:?? $',0    ; 设置开始读取的柱面编号
header  db 'header:?? $',0    ; 设置开始读取的磁头编号
sector  db 'sector:?? $',1,   ; 设置开始读取的扇区编号
FloppyOK db '-Floppy Read OK','$'
Fyerror db '-Floppy Read Error' ,'$'

times 512-2*3-($-$$) db 0     ;MBR程序中间部分用0填充 

swap_dev:
	dw SWAP_DEV     ;2Byte,存放交换系统所在设备号(init/main.c中会用)。
root_dev:                
	dw ROOT_DEV     ;2Byte,存放根文件系统所在设备号(init/main.c中会用)。

boot_flag: db 0x55,0xaa	  ;2Byte,MBR启动标记 

3.setup.asm

;******************************************************
;****Linux操作系统Nasm引导程序:setup,制作者:Mr.Jiang***
;*************2020-10-20*******************************

%include "config.inc"

INITSEG  EQU DEF_INITSEG   ;全部同bootsect 
SYSSEG   EQU DEF_SYSSEG	 
SETUPSEG EQU DEF_SETUPSEG 

jmp      start

start:

     mov   ax,SETUPSEG
     mov   ds,ax        ;为显示各种提示信息做准备
     mov   si, welcome 
     call  showmsg      ;打印"Welcome Linux"
     
;1.取扩展内存的大小值(KB)     
     mov   si, msg1
     call  showmsg   
     
     mov	ah,0x88    
     int	0x15      ;通过调用BIOS中断实现 
     mov	[2],ax    ;将扩展内存数值存在0x90002处(1个字)。
    
		

;2.检查显示方式(EGA/VGA)并取参数。
     mov   si, msg2
     call  showmsg
     
     	mov	ah,0x12
	mov	bl,0x10
	int	0x10
	mov	[8],ax
	mov	[10],bx     ;0x9000A =安装的显示内存;0x9000B=显示状态(彩/单色)
	mov	[12],cx     ;0x9000C =显示卡特性参数。
	
	mov ax,0x5019       ;在ax中预置屏幕默认行列值(ah = 80列;al=25行)。
	mov [14],ax         ;保存屏幕当前行列值(0x9000E,0x9000F)。
	                                 
	mov	ah,0x03	    ;取屏幕当前光标位置
	xor	bh,bh
	int	0x10		
	mov	[0],dx	    ;保存在内存0x90000处(2字节)	


;3.取显示卡当前显示模式
        mov   si, msg3
        call  showmsg
      
      	mov	ah,0x0f
	int	0x10
	mov	[4],bx	    ;0x90004(1字)存放当前页
	mov	[6],ax	    ;0x90006存放显示模式;0x90007存放字符列数。
	
;4.取第一个硬盘的信息(复制硬盘参数表)。
      ;第1个硬盘参数表的首地址竟然是中断0x41的中断向量值
      ;而第2个硬盘参数表紧接在第1个表的后面,中断0x46的向量向量值
      ;也指向第2个硬盘的参数表首址。表的长度是16个字节。
        mov   si, msg4
        call  showmsg

        push    ds           ;由于复制数据要修改DS的值,因此暂存起来 
      	mov	ax,0x0000
	mov	ds,ax
	lds	si,[4*0x41]
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0x0080     ;0x90080处存放第1个硬盘的表
	mov	cx,0x10
	rep
	movsb
	
;5.取第2个硬盘的信息(复制硬盘参数表)。  
        pop   ds             ;恢复DS为本段setup段地址,才能正常打印字符串 
        mov   si, msg5
        call  showmsg
        
        push    ds           ;由于复制数据要修改DS的值,因此暂存起来
      	mov	ax,0x0000
	mov	ds,ax
	lds	si,[4*0x46]
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0x0090     ;0x90090处存放第2个硬盘的表
	mov	cx,0x10
	rep
	movsb
	
;6.检查系统是否有第2个硬盘。如果没有则把第2个表清零。

        pop   ds              ;恢复DS的值,才能正常打印字符串
        mov   si, msg6
        call  showmsg
        
      	mov	ax,0x01500
	mov	dl,0x81
	int	0x13
	jc	no_disk1
	cmp	ah,3
	je	is_disk1
no_disk1:
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0x0090
	mov	cx,0x10
	mov	ax,0x00
	rep
	stosb
is_disk1:

;7.现在要进入保护模式了 
        mov   si, msg7
        call  showmsg
       
        mov   si, msg8
        call  showmsg
        
        mov   cx,14 
line:   call  newline       ;循环换行,清除一些屏幕显示    
        loop  line
        
        cli                 ;禁用16位中断
        
;8.将system模块移到正确的位置。
        ;bootsect引导程序会把 system 模块读入到内存 0x10000(64KB)开始的位置
        ;下面这段程序是再把整个system模块从 0x10000移动到 0x00000位置。即把从
        ;0x10000到0x8ffff 的内存数据块(512KB)整块地向内存低端移动了64KB字节。
        call  mov_system    ;会覆盖实模式下的中断区,BIOS中断再也无法使用 

	

;9.装载寄存器IDTR和GDTR 
	mov	ax,SETUPSEG	;ds指向本程序(setup)段
	mov	ds,ax
	lidt	[idt_48]	;加载IDTR 
	lgdt	[gdt_48]	;加载GDTR
	
;10.现开启A20地址线	
       
       call empty_8042          ;8042状态寄存器,等待输入缓冲器空。
                                ;只有当输入缓冲器为空时才可以对其执行写命令。
       mov al,0xD1              ;0xD1命令码-表示要写数据到
       out 0x64,al              ;8042的P2端口。P2端口位1用于A20线的选通。
       call empty_8042          ;等待输入缓冲器空,看命令是否被接受。
       mov al,0xDF              ;A20 on ! 选通A20地址线的参数。
       out 0x60,al              ;数据要写到0x60口。
       call empty_8042          ;若此时输入缓冲器为空,则表示A20线已经选通。
       
;11.设置8259A中断芯片,即int 0x20--0x2F   
       call  set_8259A                
       
;12.打开保护模式PE开关  
       mov	ax,0x0001	;保护模式比特位(PE)
       lmsw	ax		;就这样加载机器状态字!  
       

       
;13.跳转触发到32位保护模式代码
       ;jmp dword 1*8:inprotect+SETUPSEG*0x10 ;保护模式下的段基地址:0x90200  
                               ;这句是调试验证进入保护模式后系统是否正常                                
       jmp dword 1*8:0         ;setup程序到此结束 
                               ;跳转到0x00000,也即system程序(head.asm)                                 
       
    
;把整个system模块从 0x10000移动到 0x00000位置。 
mov_system:
        mov	ax,0x0000
	cld			;'direction'=0, movs moves forward
do_move:
	mov	es,ax		;es:di是目的地址(初始为0x0:0x0)
	add	ax,0x1000
	cmp	ax,0x9000       ;已把最后一段(从0x8000段开始的64KB)移动完?
	jz	end_move
	mov	ds,ax		;ds:si是源地址(初始为0x1000:0x0)
	sub	di,di
	sub	si,si
	mov 	cx,0x8000      ;移动0x8000字(64KB字节)。
	rep
	movsw
	jmp	do_move
end_move:  ret
 

;设置8259A中断芯片 
set_8259A: 	
        mov	al,0x11		 
	out	0x20,al	 
	dw	0x00eb,0x00eb	;jmp $+2, jmp $+2
	out	0xA0,al	 
	dw	0x00eb,0x00eb
	mov	al,0x20        ;Linux系统硬件中断号被设置成从0x20开始
	out	0x21,al
	dw	0x00eb,0x00eb
	mov	al,0x28		;start of hardware int's 2 (0x28)
	out	0xA1,al
	dw	0x00eb,0x00eb
	mov	al,0x04		;8259-1 is master
	out	0x21,al
	dw	0x00eb,0x00eb
	mov	al,0x02		;8259-2 is slave
	out	0xA1,al
	dw	0x00eb,0x00eb
	mov	al,0x01		;8086 mode for both
	out	0x21,al
	dw	0x00eb,0x00eb
	out	0xA1,al
	dw	0x00eb,0x00eb
	mov	al,0xFF		;屏蔽主芯片所有中断请求。
	out	0x21,al
	dw	0x00eb,0x00eb
	out	0xA1,al         ;屏蔽从芯片所有中断请求。 
        ret 
 
empty_8042:                     ;只有当输入缓冲器为空时(状态寄存器位1 = 0)
                                ;才可以对其执行写命令。
	dw	0x00eb,0x00eb
	in	al,0x64	        ;读AT键盘控制器状态寄存器。
	test	al,2		;测试位1,输入缓冲器满?
	jnz	empty_8042	;yes - loop
	ret 
     

idt_48:  dw 0x800              ;这里不能像书上设置成0,否则VMWARE调试会出错! 
         dw 0,0                ;IDT全部中断都设置成无效 
gdt_48:  dw 0x800              ;GDT长度设置为 2KB(0x7ff)表中共可有 256项。
         dw 512+gdt,0x9        ;GDT物理地址:0x90200 + gdt
        
                
         
gdt:
	dw	0,0,0,0		;0#描述符,它是空描述符

	dw	0x07FF		;8Mb - limit=2047 (2048*4096=8Mb)
	dw	0x0000		;base address=0
        dw	0x9A00		;code read/exec 代码段为只读、可执行
	dw	0x00C0		;granularity=4096, 386 颗粒度为4096,32位模式

	dw	0x07FF		;8Mb - limit=2047 (2048*4096=8Mb)
	dw	0x0000		;base address=0
	dw	0x9200		;data read/write  数据段为可读可写
	dw	0x00C0		;granularity=4096, 386颗粒度为4096,32位模式

showmsg: 
     call  newline
     call  printstr
     ret

printstr:                  ;显示指定的字符串, 以'$'为结束标记 
      mov al,[si]
      cmp al,'$'
      je disover
      mov ah,0eh
      int 10h
      inc si
      jmp printstr
disover:
      ret

newline:                     ;显示回车换行
      mov ah,0eh
      mov al,0dh
      int 10h
      mov al,0ah
      int 10h
      ret

welcome db '(ii) Welcome Linux---setup!',0x0d,0x0a,'$'

msg1 db '1.Get memory size','$'
msg2 db '2.Check for EGA/VGA and some config parameters','$'
msg3 db '3.Get video-card data','$'
msg4 db '4.Get hd0 data','$'
msg5 db '5.Get hd1 data','$'
msg6 db '6.Check that there IS a hd1','$'
msg7 db '7.Move system from 0x10000 to 0x00000','$'
msg8 db '8.Now Ready to Protect Mode!','$' 


[bits 32]
inprotect:                          ;测试进入保护模式后是否正常                     
mov eax,2*8 ;加载数据段选择子(0x10)
mov ds,eax

mov  esi,sysmsg+SETUPSEG*0x10   ;保护模式DS=0,数据需跨过段基址用绝对地址访问 
mov  edi, 0xb8000+18*160        ;显示在第18行,显卡内存地址也需用绝对地址访问 
call printnew

mov  esi,promsg+SETUPSEG*0x10
mov  edi, 0xb8000+20*160        ;显示在第20行
call printnew

mov  esi,headmsg+SETUPSEG*0x10
mov  edi, 0xb8000+22*160        ;显示在第22行
call printnew

jmp  $


printnew:                       ;保护模式下显示字符串, 以'$'为结束标记
        mov  bl ,[ds:esi]
        cmp  bl, '$'
        je   printover
        mov  byte [ds:edi],bl
        inc  edi
        mov  byte [ds:edi],0x0c  ;字符红色 
        inc  esi
        inc  edi
        jmp  printnew
printover:
        ret



sysmsg  db '(iii) Welcome Linux---system!','$'
promsg  db '1.Now Already in Protect Mode','$'
headmsg db '2.Run head.asm in system program','$'

4.head.asm

;*********************************************************
;****Linux操作系统Nasm引导程序:head,制作者:Mr.Jiang***
;*************2020-10-27**********************************

%include "config.inc"

SETUPSEG equ DEF_SETUPSEG  ;全部同bootsect和setup 
SYSSEG   equ DEF_SYSSEG

_pg_dir  equ  0x0000     ;页目录地址,大小4KB. 

pg0      equ  0x1000     ;第1个页表地址,大小4KB. 
pg1      equ  0x2000     ;第2个页表地址,大小4KB.
pg2      equ  0x3000     ;第3个页表地址,大小4KB.
pg3      equ  0x4000     ;第4个页表地址,大小4KB.

_tmp_floppy_area   equ  0x5000   ;软盘缓冲区地址. 
len_floppy_area   equ  0x400     ;软盘缓冲区大小1KB 

[bits 32]                        ;指定代码为32位保护模式   

jmp start

;这条伪指令不会执行任何操作,只在编译的时候起填充数字作用。 
times _tmp_floppy_area+len_floppy_area-($-$$) db 0  ;
;一个语句实现页目录和页表地址区域清0,省去程序后面Linux源代码中的清0部分 
;使head程序从0x5000+0x400位置开始放置(仅除第一条jmp指令外)。 


;这里已经处于32位运行模式,首先设置ds,es,fs,gs为setup.s中构造的内核数据段
;并将堆栈放置在stack_start指向的user_stack数组区,然后使用本程序后面定义的
;新中断描述符表和全局段描述表。新全局段描述表中初始内容与setup.s中的基本一样,
;仅段限长从8MB修改成了16MB。stack_start定义在kernel/sched.c。它指向user_stack
;数组末端的一个长指针。设置这里使用的栈,姑且称为系统栈。但在移动到任务0执行
;(init/main.c中137行)以后该栈就被用作任务0和任务1共同使用的用户栈了。

start:                          
mov eax,2*8                     ;加载数据段选择子(0x10)
mov ds,eax                      ;把所有数据类段寄存器全部指向GDT的数据段地址 
mov es,eax 
mov fs,eax
mov gs,eax
mov ss,eax


mov  esi,sysmsg                 ;保护模式DS=0,数据用绝对地址访问
mov  cl, 0x0c                   ;颜色红 
mov  edi, 0xb8000+13*160        ;显示在第18行,显卡内存地址也需用绝对地址访问
call printnew   

mov  esi,promsg 
mov  cl, 0x0c
mov  edi, 0xb8000+15*160        ;显示在第20行
call printnew

mov  esi,headmsg 
mov  cl, 0x0c
mov  edi, 0xb8000+16*160        ;显示在第22行   
call printnew

mov  esp,0x1e25c                ; 重新设置堆栈,暂时设置值参见书
                                ;《Linux内核设计的艺术_图解Linux操作系统架构
                                ; 设计与实现原理》P27
                                ; Linus源程序中是lss _stack_start,%esp 
                                ; _stack_start,。定义在kernel/sched.c,82-87行
                                ; 它是指向 user_stack数组末端的一个长指针                            
call setup_idt
call setup_gdt  

jmp  1*8:newgdt                   ;改变CS的值来触发新GDT表生效                      
     nop
     nop
newgdt:                         ;如能正常打印则表明程序正常运行,新GDT表无问题 
mov  esi,gdtmsg                 ;保护模式DS=0,数据用绝对地址访问
mov  cl, 0x09                   ;颜色蓝
mov  edi, 0xb8000+17*160        ;显示在第18行,显卡内存地址也需用绝对地址访问
call printnew    


;call test_keyboard            ;开键盘中断并按键测试,显示外部中断体系正常

sti ;开中断
int 00h                        ;手工系统中断调用,测试显示内部中断体系也正常 
cli ;关掉中断 
 

call  A20open
mov  esi,a20msg                 ;保护模式DS=0,数据用绝对地址访问
mov  cl, 0x09                   ;蓝色
mov  edi, 0xb8000+19*160        ;显示在第18行,显卡内存地址也需用绝对地址访问
call printnew
  

;前面3个入栈0值分别表示main函数的参数envp、argv指针和argc,但main()没有用到。
;push _main入栈操作是模拟调用main时将返回地址入栈的操作,所以如果main.c程序
;真的退出时,就会返回到这里的标号L6处继续执行下去,也即死循环。push _main将
;main.c的地址压入堆栈。这样,在设置分页处理(setup_paging)结束后执行'ret'
;返回指令时就会将main.c;程序的地址弹出堆栈,并去执行main.c程序了。

push 0 ;These are the parameters to main :-)
push 0 ;这些是调用main程序的参数(指init/main.c)。
push 0  
push L6 ;return address for main, if it decides to.
push _main ;'_main'是编译程序对main的内部表示方法。
jmp  setup_paging   ;这里用的JMP而不是call,就是为了在setup_paging结束后的
                    ;ret指令能去执行C程序的main() 
L6:
jmp L6 ;main程序绝对不应该返回到这里。不过为了以防万一,
     ;所以添加了该语句。这样我们就知道发生什么问题了。
     
     
     


_main:      ;这里暂时模拟出C程序main() 
     mov  esi,mainmsg                ;保护模式DS=0,数据用绝对地址访问
     mov  cl, 0x09                   ;蓝色
     mov  edi, 0xb8000+22*160        ;指定显示在某行,显卡内存地址需用绝对地址
     call printnew                   ;0xb8000为字符模式下显卡映射到的内存地址 
     ret  
     
     
test_keyboard:       ; 测试键盘中断
mov al, 11111101b  ; 开启键盘中断开关 
out 021h, al       ; 主8259, OCW1.
dw  0x00eb,0x00eb   ;时延
mov al, 11111111b   ; 屏蔽从芯片所有中断请求
out 0A1h, al       ; 从8259, OCW1.
dw	0x00eb,0x00eb  ;时延
ret


;Linux将内核的内存页表直接放在页目录之后,使用了4个表来寻址16 MB的物理内存。
;如果你有多于16 Mb的内存,就需要在这里进行扩充修改。
;每个页表长为4KB(1页内存页面),而每个页表项需要4个字节,因此一个页表共可存
;1024个表项。一个页表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。
setup_paging:

;首先对5页内存(1页目录 + 4页页表)清零。由于在程序第一行已经实现,此处可省。 
;mov ecx,10
;xor eax,eax
;xor edi,edi  ;页目录从0x000地址开始。  
;cld          ;edi按递增方向 
;rep
;stosd         ;eax内容存到es:edi所指内存位置处,且edi增4。

;下面4句设置页目录表中的项。因为内核共有4个页表,所以只需设置4项(索引)。
;页目录项的结构与页表中项的结构一样,4个字节为1项。
;例如"pg0+7"表示:0x00001007,是页目录表中的第1项。
;则第1个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;
;第1个页表的属性标志 = 0x00001007&0x00000fff = 0x07,表示该页存在、用户可读写。
;一句指令就把页表的地址和属性完全完整定义了,这个写法设计得有点巧妙。    
mov dword [_pg_dir],pg0+7       ;页表0索引 将直接覆盖0地址处的3字节长度jmp指令 
mov dword [_pg_dir+4],pg1+7     ;页表1索引
mov dword [_pg_dir+8],pg2+7     ;页表2索引
mov dword [_pg_dir+12],pg3+7    ;页表3索引 


;下面填写4个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096项(0-0xfff),
;也即能映射物理内存 4096*4Kb = 16Mb。
;每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)。
;填写使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。
;每一个页表中最后一项在表中的位置是1023*4 = 4092.
;此最后一页的最后一项的位置就是pg3+4092。
mov edi,pg3+4092;edi->最后一页的最后一项。
mov eax,0xfff007;16Mb - 4096 + 7 (r/w user,p) */
;最后1项对应物理内存页面的地址是0xfff000,
;加上属性标志7,即为0xfff007。
std ;方向位置位,edi值递减(4字节)。
goon:
stosd  
sub eax,0x1000;每填写好一项,物理地址值减0x1000。
jge goon ;如果小于0则说明全添写好了。  jge是大于或等于转移指令


;现在设置页目录表基址寄存器cr3,指向页目录表。cr3中保存的是页目录表的物理地址
;再设置启动使用分页处理(cr0的PG标志,位31)
xor eax,eax ;pg_dir is at 0x0000 */ # 页目录表在0x0000处。
mov cr3,eax ;cr3 - page directory start */
mov eax,cr0
or eax,0x80000000  ;添上PG标志。
mov cr0,eax ; set paging (PG) bit */

# 软盘缓冲区: 共保留1024项,填充数值0。在程序第一行已经实现,此处可省。
;mov ecx,1024/4; 
;xor eax,eax
;mov edi,_tmp_floppy_area  ;软盘缓冲区从0x5000地址开始。
;cld                      ;edi按递增方向
;rep
;stosd                    ;eax内容存到es:edi所指内存位置处,且edi增4。


mov  esi,pagemsg                ;保护模式DS=0,数据用绝对地址访问
mov  cl, 0x09                   ;蓝色字体 
mov  edi, 0xb8000+20*160        ;指定显示在某行,显卡内存地址也需用绝对地址访问
call printnew 
 
mov  esi,asmmsg                 ;保护模式DS=0,数据用绝对地址访问
mov  cl, 0x09                   ;蓝色字体
mov  edi, 0xb8000+21*160        ;指定显示在某行,显卡内存地址也需用绝对地址访问     
call printnew
     
ret  ;setup_paging这里用的是返回指令ret。
;该返回指令的另一个作用是将压入堆栈中的main程序的地址弹出,
;并跳转到/init/main.c程序去运行。本程序到此就真正结束了。      
 
;用于测试A20地址线是否已经开启。采用的方法是向内存地址0x000000处写入任意
;一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,
;就一直比较下去,也即死循环表示地址A20线没有选通,就不能使用1MB以上内存。
A20open:
       xor   eax, eax
       inc   eax 
       mov   [0x000000],eax   
       cmp   eax,[0x100000]
       je    A20open
       ret

printnew:                       ;保护模式下显示字符串, 以'$'为结束标记
        mov  bl ,[ds:esi]
        cmp  bl, '$'
        je   printover
        mov  byte [ds:edi],bl
        inc  edi
        mov  byte [ds:edi],cl  ;字符颜色
        inc  esi
        inc  edi
        jmp  printnew
printover:
        ret

setup_idt:
          ;暂时将所有的中断全部指向一个中断服务程序:ignore_int 
          lea  edx,[ignore_int]   ;将ignore_int的有效地址(偏移值)值送edx 
          mov  eax,0x00080000  ;将选择符0x0008置入eax的高16位中。
          mov  ax,dx           ;selector = 0x0008 = cs */
                               ;偏移值的低16位置入eax的低16位中。此时eax含有门
                               ;描述符低4字节的值。
          mov dx,0x8E00        ;interrupt gate - dpl=0, present 
                               ;此时edx含有门描述符高4字节值,偏移地址高16位是0 
          lea edi,[_idt]       ;_idt是中断描述符表的地址。
          ;以上为单独一个中断描述符的设置方法 
          
          mov ecx,256          ;IDT表中创建256个中断描述符 
;将上面的中断描述符重复放置256次,让所有的中断全部指向一个中断服务程序:哑中断
 rp_sidt:
          mov [edi],eax       ;将哑中断门描述符存入表中。
          mov [edi+4],edx     ;edx内容放到 edi+4 所指内存位置处。
          add  edi,8           ; edi指向表中下一项。
          loop rp_sidt    
              
          lidt [idt_descr]       ;加载中断描述符表寄存器值。
          ret          
                               
 
;让所有的256中断都指向这个统一的中断服务程序                        
ignore_int:
           cli               ;首先应禁止中断,以免中断嵌套 
           pushad            ;进入中断服务程序首先保存32位寄存器

           push ds           ;再保存所有的段寄存器
	   push es
	   push fs
	   push gs
	   push ss
	   mov  eax,2*8      ;进入断服务程序后所有数据类段寄存器都转到内核段
           mov ds,eax
           mov es,eax
           mov fs,eax
           mov gs,eax
           mov ss,eax

	  mov  esi,intmsg                ;保护模式DS=0,数据用绝对地址访问
          mov  cl, 0x09                  ;蓝色
          mov  edi, 0xb8000+18*160       ;指定显示在某行,显卡内存需用绝对地址
          call printnew

           pop ss             ;恢复所有的段寄存器
	   pop gs
	   pop fs
	   pop es
	   pop ds
	   
           popad              ; 所有32位寄存器出栈恢复
           iret                ;中断服务返回指令 


align 2 ;按4字节方式对齐内存地址边界。
dw    0 ;这里先空出2字节,这样_idt长字是4字节对齐的。 
                               
;下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数。
;前2字节是idt表的限长,后4字节是idt表在线性地址空间中的32位基地址。
idt_descr:
         dw 256*8-1 ;idt contains 256 entries # 共256项,限长=长度 - 1。
         dd _idt                               
         ret
         

    
setup_gdt:

        lgdt [gdt_descr] ;加载全局描述符表寄存器。
        ret


align 2 ;按4字节方式对齐内存地址边界。
dw    0 ;这里先空出2字节,这样_gdt长字是4字节对齐的。

;加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限长,
;后4字节是gdt表的线性基地址。因为每8字节组成一个描述符项,所以表中共可有256项。
;符号_gdt是全局表在本程序中的偏移位置。
gdt_descr:
        dw 256*8-1  
        dd _gdt 
 
 


sysmsg  db '(iii) Welcome Linux---system!','$'
promsg  db '1.Now Already in Protect Mode','$'
headmsg db '2.Run head.asm in system program','$'
gdtmsg  db '3.Reset GDT success:New CSEIP normal','$'
intmsg  db '4.Reset IDT success:Unknown interrupt','$'  
a20msg  db '5.Check A20 Address Line Stdate:Open','$'
pagemsg db '6.Memory Page Store:Page Tables is set up','$'
asmmsg  db '7.Pure Asm Program:bootsect->setup->head(system) is Finished','$'
mainmsg db '8.Now Come to C program entry:Main()','$'
      
 
;IDT表和GDT表放在程序head的最末尾

;中断描述符表:256个,全部初始化为0。        
_idt:    times 256  dq 0  ;idt is uninitialized # 256项,每项8字节,填0。

 
;全局描述符表。其前4项分别是:空项、代码段、数据段、系统调用段描述符,
;后面还预留了252项的空间,用于放置新创建任务的局部描述符(LDT)和对应的
;任务状态段TSS的描述符。
;(0-nul,1-cs,2-ds,3-syscall,4-TSS0,5-LDT0,6-TSS1,7-LDT1,8-TSS2 etc...)        
_gdt: dq 0x0000000000000000 ;NULL descriptor */
      dq 0x00c09a0000000fff ;16Mb */ # 0x08,内核代码段最大长度16MB。
      dq 0x00c0920000000fff ;16Mb */ # 0x10,内核数据段最大长度16MB。
      dq 0x0000000000000000 ;TEMPORARY - don't use */
      times 252 dq 0        ;space for LDT's and TSS's etc */ # 预留空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值