文章目录
一、地址、section、vstart的浅尝辄止
1.地址访问
- 程序中各种数据结构的访问,本质上是“该数据结构的起始地址+该数据结构所占内存的大小”来实现的。
- 地址访问策略是程序中给出的地址,你去那里拿东西,那里必须提前准备好才行。
- 程序最终是要被加载到物理内存上的,因此该地址对应的物理内存一定是你想要的。这就是程序加载器要做的事情,根据你给出的地址,将程序加载到对应内存地址上。
2.vstart
- vstart没有改变数据本身在文件中的地址。
- vstart是虚拟起始地址,它给出了一个起始地址来规定后续的数据结构的地址(起始地址+偏移地址),然后编译器据此进行编译,加载器根据编译出的内容加载到物理内存上。
- vstart的时机:我预先知道我的程序要被加载到某地址处。程序只有被加载到非0地址处时vstart才有用,程序的默认起始地址是0。
- 编译器的好处是利于重定位。
二、CPU实模式
1.实模式与CPU构成
- 实模式是指8086 CPU的寻址方式、寄存器大小、指令用法等,用来反映CPU在该环境下如何工作的。
- 实模式的“实”体现在:程序用到的地址都是真实的物理地址。
- CPU的组成:运算单元、控制单元、存储单元
2. 控制单元
- 控制单元:CPU的核心,告诉CPU下一步该做什么。由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)、操作控制器OC(Operation Controller)组成。
- CPU工作流程:
- 程序被加载到内存(指令都在内存中了)
- 指令指针寄存器IP指向下一条待读取的指令的地址,控制单元根据IP的指向,将该指令加载到IR中。(但CPU不知道指令什么意思)
- 指令译码器ID将位于IR中的指令按照指令格式进行解码,分析出操作码、操作数等等。
3.存储单元
- 指令存储在指令寄存器IR中,那指令中用到的数据存储在哪里呢?——存储单元
- 存储单元构成:CPU内部的L1、L2缓存及寄存器
- 为什么要把内存中的操作数放到缓存中?
- 因为缓存基本采用SRAM(Static RAM)寄存器,静态存取寄存器,与DRAM不同,它无需刷新电路。
- SRAM性能强劲,但集成度低,比DRAM体积大很多。
- 寄存器分类:
- 程序员可见寄存器:通用寄存器、段寄存器
- 程序员不可见寄存器
4.运算单元
- 操作码、操作数都有了,就差执行指令了,随后操作控制器OC便向运算单元发送信号。
- 运算单元:负责算数运算和逻辑运算,它从控制单元那里接受信号并执行,无自主意识,只是个执行部件。
总结:控制单元要取下一条指令。读取IP寄存器的值,将指令放入IR中,然后ID对指令进行解码,先确定操作数,然后是操作码。若操作码在内存中,便将其取到存储单元中,若在寄存器中就直接用。最后控制单元向运算单元发送信号,由运算单元进行计算。然后IP寄存器再指向下一条指令,循环往复。
三、实模式下的寄存器
1.寄存器
- 寄存器:位于CPU内部的存储元件,速度快。
- CPU中的缓存L1、L2等SRAM是用寄存器来存储数据的。
- CPU中的寄存器分类:
- 内部使用寄存器,对程序员不可见:全局描述符表寄存器GDTR、中断描述符表寄存器IDTR、局部描述符表寄存器LDTR、任务寄存器TR、控制寄存器CR0~3、标志寄存器flags、调试寄存器DR0 ~7
- 对程序员可见寄存器:通用寄存器和段寄存器。
- 程序员可以对内部寄存器进行初始化:
- GDTR:用lgdt指令为其指定全局描述符表以及偏移量。
- IDTR:用lidt指令为其指定中断描述符表的地址。
- LDTR:用lldt为其指定局部描述符表ldt。
- TR:用ltr为其指定一个任务状体段tss。
- flags:pushf和popf指令,分别用于将flags寄存器的内容压入栈和将栈中内容弹到flags寄存器中。
- 在实模式下,用到的寄存器都是16位宽。
2.各种寄存器
- 段寄存器——代码段、数据段和栈段寄存器:
- 代码段是把所有指令连续排放在一起,里面存储的是指令的操作码及寻址方式。该区域可以在硬件上的文件中,也可以是被加载后的内存中,总之是一段指令区域。而代码段寄存器CS则指向这段区域(代码段)的起始地址。
- 数据段是代码段类似,里面存储的是数据。数据段寄存器DS指向该区域的起始地址。
- 因为栈段是操作系统分配的,所以是被加载到内存中才有的。因此栈段只在内存中,硬盘文件中没有。栈段寄存器SS用来指向栈段的起始地址。
- 附加寄存器:给大家多提供几个寄存器用。在16位CPU中,只有ES一个附加寄存器,FS和GS都是32位寄存器。
- 不可见寄存器
- CS是可见寄存器、IP是不可见寄存器,它们在一起组成了CPU的罗盘。
- flags中存储着一些标志位,有些指令的执行需要一定的条件,这些条件便存储在flags中。
- 通用寄存器
- 无论实模式还是保护模式,通用寄存器只有8个:AX、BX、CX、DX、SI、DI、BP、SP
- 拿AX举例:AX是16位,低8位为AL(A Low)、高8位为AH(A High)。AX可高位拓展为32位,即EAX。
- 因此虽然实模式下操作数是16位,但仍然可以用32位寄存器,因为EAX也是由AX,AH,AL构成的。
- 通用寄存器可以用来保存任何数据和地址。
- 惯用法:CX寄存器用作循环的次数控制,BX寄存器用于存储起始地址。
- 通用寄存器的特定功能:一些指令已经固定用一些特定的寄存器作为参数了:ESI作为很多有关数据复制指令的源地址,EDI为目的地址。
3. 实模式下的内存分段由来
- 实模式的“实”体现在:程序用到的地址都是真实的物理地址。“段基址:段内偏移地址”产生的逻辑地址就是真实的物理地址。
- x86代指所有…86的CPU体系,8086成为里程碑是因为它开创了段机制。
- 8086的地址总线是20位宽,寻址范围为2^20=1MB,寄存器为16位。
- 通过将16位的段基址左移4位+16位的段内偏移地址得到20位最终地址
- 通过“段基址+段内偏移地址”的寻址方式,我们可以寻址到的最大内存位0x10FFEF,但是这部分不存在,因为8086只有20条地址线。这时采用“回卷”方式进行处理,即把地址对1MB进行取模。
- 这引出了保护模式需要打开A20地址线的问题。
四、实模式下的CPU寻址方式
1.寄存器寻址
- 当数在寄存器中时,可以直接从寄存器拿数。
//用mul进行乘法:
mov ax,0x10 //立即数寻址
mov bx,0x9 //立即数寻址
mul bx //寄存器寻址
虽然乘数和被乘数都是8位,但是它们使用的寄存器是16位,因此看作是两个16位的数字相乘。
因此mul指令的被乘数默认放在ax中,乘数要保存在寄存器/内存中,结果默认保存在DS:AX(高16位:低16位)中。
2.立即数寻址
- 立即数就是常数
mov ax,0x18 //立即数寻址
mov ds,ax //寄存器寻址
mov ax,macro_selector //立即数寻址
mov ax,label_start //立即数寻址
macro_selector是个宏,label_start是个标号,这两个数在编译阶段会转换成数字,最终在可执行文件中是立即数。
3.内存寻址
- 寄存器寻址和立即数寻址都不在内存中,操作数在内存中的寻址被称为内存寻址。
- 内存寻址的方式:“段基址:段内偏移地址”这种寻址方式只能用在内存中。默认状态下的数据段寄存器是DS,即段基址已经有了,只要给出段内偏移地址即可访问内存。
- 因此,段内偏移地址也被称为有效地址。
- CPU的寻址方式看上去死板,因为一种寻址方式就对应一种电路实现。
-
直接寻址
- 直接给出内存地址,告诉CPU取这里的地址中的内容为操作数
mov ax,[0x1234] //直接寻址,取DS:0x1234中的数存入AX中 mov ax,[fs:0x5678] //段跨越前缀fs表示段基址变为gs,取gs:0x5678处的值存入AX中
-
基址寻址
- 实模式下:在操作数中用bx/bp寄存器作为基址寄存器(地址的起始)
- 保护模式:基址寄存器可以是任意通用寄存器
- bx的默认段寄存器是ds;bp的默认段寄存器是ss
-
SS——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值。
为什么ss段寄存器已经有sp寄存器了,还需要bp寄存器呢?
因为sp只是栈顶指针,是专门给push和pop指令开设的指针寄存器。//push ax: sub sp,2 mov sp.ax //pop ax mov ax,[sp] add sp,2
- 通过ss:bp的方式,可以将栈当作普通数据段来访问
- 举例:当用栈来保存局部变量和函数参数时,函数头入栈作为“基址”,以上为返回值和形参,以下为局部变量。
-
变址寻址
- 和基址寻址类似,只是寄存器由bx,bp变为si和di。
- si是源索引寄存器,di是目的索引寄存器,两个的默认段寄存器都是ds。
mov [di],ax //将寄存器ax中的值存入ds:di的内存中 mov [si+0x1234],ax //变址也可加多个偏移量
-
基址变址寻址
- 基址与变址的结合
mov [bx+di],ax //将ax的值存入 ds:bx+di 中 add [bx+si],ax //将ax的值加上 ds:bx+si 后存入 ds:bx+si 中
- 在基址寻址中,有效地址(即偏移地址)由两部分组成。一部分存放在基址寄存器中,另一部分为常量 。
- 在变址寻址中,有效地址(即偏移地址)由两部分组成。一部分存放在变址寄存器中(可能这部分还要乘以一个系数),另一部分为常量 。
变址寻址与基址寻址的不同,就在于偏移地址的计算更复杂了点。此时 CPU 不去基址寄存器,而是去变址寄存器。
4.栈
- 寄存器:栈段寄存器ss和栈指针寄存器sp
- 数据存取方法:push和pop
//push ax: sub sp,2 mov sp.ax //pop ax mov ax,[sp] add sp,2
- 字长:CPU一次可处理的数据的长度,在实模式下是16位。
- 虽然栈是向下发展的,但栈也是内存,访问内存仍然是从低地址到高地址。
五.实模式下的call和ret
1.前言
- 因为指令都在内存中,所以CPU也要访问内存才能拿到要执行的代码。
- IP寄存器是不可见寄存器,不可以直接赋值。
- 无条件转移:call、ret、jmp指令在原理上都是通过修改cs和ip寄存器的值,将CPU导向新的位置。
- jmp:一去不复返地执行新代码,使用环境——交接
- call:执行一段代码,通过ret返回,不跨段,近调用的call在栈中留下段内偏移地址。
- call far:远调用,通过retf返回,跨段,远调用的call far在栈中留下了段基址和段内偏移地址
2. call
- 16位相对近调用
- 相对:由于在同一个代码段中,所以只需要给出目标函数的相对地址即可
- 指令格式:call near 立即数地址
- 立即数可以是被调用的函数名、标号.
- 近调用必须是相对地址,因为CPU是根据地址的相对量来推算目标地址的。如果目标函数地址比call地址大,则地址相对量为正数,如果小则为负数。
call near near_proc
jmp $
addr dd 4
near_proc:
mov ax,0x1234
ret
- 16位实模式间接绝对近调用
- 间接:目标函数的地址没有直接给出,要么在寄存器中,要么在内存中,总之不以立即数的形式出现
- 绝对:目标函数的地址是绝对地址,不是相对地址
- 近调用:不跨段
- 指令格式:call 寄存器寻址/call 内存寻址
section call_test vstart=0x900 mov word [addr],near_proc //word是告诉CPU一次要读写多少字节(2字节)的,因为地址在编译阶段会被替换为数字,这个数字宽度是不定的,同样的0x18,可能是0x0018也可能是0x00000018 call [addr] //call 内存寻址 mov ax,near_proc call ax // call 寄存器寻址 jmp $ addr dd 4 near_proc: mov ax,0x1234 ret
- 16位实模式直接绝对远调用
- 直接:不需要寄存器/内存,在指令中直接给出,是立即数
- 远调用:跨段,CS和IP都要用新的。远调用的call far在栈中留下了段基址和段内偏移地址
- 指令格式:call far 段基址(立即数):段内偏移地址(立即数)
- 直接绝对远调用:far可以不写
section call_test vstart=0x900 call 0:far_proc jmp $ far_proc: mov ax,0x1234 retf
- 16位实模式间接绝对远调用
- 间接:操作数没有直接给出,在寄存器/内存中。但是段基址和段内偏移地址都是16位,一个寄存器放不下,因为寄存器宝贵,所以段基址和段内偏移地址都放在内存中。
- 指令格式:call far 内存寻址
- 需要用call far,不然和间接绝对近调用一样了
section call_test vstart=0x900 call far [addr] jmp $ addr dw far_proc,0 //低2字节是far_proc 段内偏移地址,高2字节是段基址 far_proc: mov ax,0x1234 retf
- 需要通过ret来返回
3.ret
- ret和retf
- ret:近返回,未跨段访问。在栈顶弹出2字节内容来替换IP寄存器
- retf(return far):远返回,从栈顶弹出4字节内容,栈顶的2字节置换IP寄存器,另外2字节置换CS寄存器。
- 指令不管里面的内容是数据还是地址,它只会执行
4.jmp
- 原理:jmp指令只需要更新cs和ip寄存器的值,由于不需要返回原处,所以不需要对原地址进行保存。
- 根据是否跨段分为近转移和远转移
- 16位实模式相对短转移
- 指令格式:jmp short 立即数(标号)
- 既然是相对,说明操作数是个相对增量,有正负之分
- 短转移:段内转移,跳转的范围是1字节有符号数的大小—— -128——127(8位有符号数)
- 和call的相对近转移一样
section jmp_test vstart=0x900 jmp short start //短转移 times 127 db 0 //+127是正偏移的最大值,如果是times 128 db 0就会报错:short jmp is out of range start: mov ax,0x1234 jmp $
- 扩大范围的方式:
- 将short换成near
- jmp后什么都不写,让nasm编译器自己判断是short还是near
- 相对近转移
- 指令格式:jmp near 立即数地址
- 近转移:操作数范围变大—— -32768——32767(16位有符号数)
section jmp_test vstart=0x900 jmp near start //近转移 times 128 db 0 start: mov ax,0x1234 jmp $ //短转移
- 16位实模式间接绝对近转移
- 绝对:目标地址是绝对地址
- 近转移:只改变IP值,16位地址,段内转移
- 间接:地址不是直接给出(立即数),而是在寄存器/内存中
- 指令格式:jmp near 寄存器寻址/内存寻址
- 若是内存寻址,则段基址寄存器默认为DS
section jmp_test vstart=0x900 mov ax,start jmp near ax //寄存器寻址 times 128 db 0 start: mov ax,0x1234 jmp $
section jmp_test vstart=0x900 mov word [addr],start //word用来告诉CPU一次读写多少字节 jmp near [addr] //内存寻址 times 128 db 0 addr dw 0 //定义addr为2字节大小变量 start: mov ax,0x1234 jmp $
- 16位实模式直接绝对远转移
- 直接:操作数是立即数
- 绝对:提供的操作数是绝对地址
- 远转移:跨段,cs和ip都要改变
- 指令格式:jmp 段基址(立即数):段内偏移地址(立即数)
section jmp_test vstart=0x900 jmp 0:start times 128 db 0 start: mov ax,0x1234 jmp $
- 16位实模式间接绝对远转移
- 间接:操作数不是立即数,在寄存器/内存中。由于操作数是两个数,所以只能放在内存中。
- 远转移:跨段,为了指示CPU在内存中取4个字节,需要用far关键字。前两个字节是段内偏移地址,后两个自己是段基址。
- 指令格式:jmp far 内存寻址
section jmp_test vstart=0x900 jmp far [addr] times 128 db 0 addr dw start,0 //低2字节是偏移地址,高2字节是段基址 start: mov ax,0x1234 jmp $
六、标志寄存器flags&有条件转移
- flags
- 作用:有条件转移的条件放在标志寄存器flags中。
- 实模式下的标志寄存器是16位的flags,在保护模式下拓展为32位的eflags。
- flags中的位
位 | 英文 | 名称 | 意义 |
---|---|---|---|
CF | carry flag | 进位 | 可用于检测无符号数加减法是否溢出 |
PF | parity flag | 奇偶位 | 用于标记结果低8位中1的个数(奇偶),常用于数据传输开始和结束时的对比 |
AF | auxiliary carry flag | 辅助进位标志 | 用于记录运算结果低4位的进、借位情况 |
ZF | zero flag | 零标志位 | 记录运算结果是否为0 |
SF | sign flag | 符号标志位 | 若运算结果为负,则SF=1 |
TF | trap flag | 陷阱标志位 | 若TF=1,则CPU进入单步运行方式,若TF=0,则进入连续工作方式。例如debug |
IF | interrupt flag | 方向标志位 | |
OF | overflow flag | 溢出位 |
- 有条件转移
- 指令格式:jxx 目标地址。(目标地址只能是段内偏移地址)
- 若条件满足则跳转到目标地址,反之则继续运行下一条指令。
七、实模式被淘汰的原因
- 主要是安全隐患
- 在实模式下,用户程序和操作系统是同一特权级的程序,因为实模式没有特权级,所以可以执行一些有破坏性的指令。
- 程序可以随意修改段基址,可以访问到1MB的任何位置,甚至可以访问到操作系统的内存数据。
八、显示器
1.CPU如何与外设通信——IO接口
- IO接口是连接CPU与外设的逻辑控制部件,有硬件软件两部分
- 硬件:协调CPU与外设之间的不匹配——速度不匹配、数据格式不匹配等
- 软件:控制接口电路工作的驱动程序&完成内部数据传输所需的程序
- IO接口的功能:
- 设置数据缓冲,解决CPU与外设的速度不匹配
数据先存入缓冲区,等需要的适合再发送出去。 - 设置信号电平转换电路
- 设置数据格式转换
外设可能输出数字信号和模拟信号,而CPU只能处理数字信号。 - 设置时序控制电路来同步CPU和外设
- 提供地址译码
- 设置数据缓冲,解决CPU与外设的速度不匹配
- 为了区别CPU内部的寄存器,IO接口中的寄存器称为端口(与网络的端口有别)
- in指令用于从端口中读取数据
- 指令格式:操作码 目的操作数,源操作数
- 只要用in指令,源操作数必须为dx,目的操作数根据寄存器宽度选择al(8位)或ax(16位)
in al,dx in ax,dx //al和ax用来存储从端口中获取的数据,dx是端口号
- out指令用于向端口中写数据
out dx,al out dx,ax out 立即数,al out 立即数,ax //在out指令中,可以选择dx寄存器或立即数充当端口号
2.显卡
- 某些IO接口也叫适配器,适配器是驱动某一外部设备的功能模块。
- 显卡也成为显示适配器,它的本质是IO接口,用来连接CPU与显示器。
- 无论哪种显卡,提供给我们的可编程接口都是一样的:IO端口和显存
- 显存是由显卡提供的,位于显卡内部的一块内存。
- 显卡的工作是不断读取显存,然后将其内容发送到显示器上。
- 显卡有自己的BIOS,位置是0xC0000到0xC7FFF。显卡支持三种模式:文本模式、黑白图形模式、彩色图像模式。
- 从0xB8000到0xBFFFF,这片32KB大小的内存区域是用于文本显示。
- 我们向0xB8000处输入字符就会落入显存中,显存中有了数据,显卡就会将它搬到大屏幕上。
- 彩色字符
- 每个字符的低字节是ASCII码,高字节是字符属性的元信息。在高字节中,低4位是前景色,高4位是字符的背景色。
- 颜色用RGB红绿蓝三种基色调和,第4位用来控制亮度。若为1则为高亮,为0则为正常亮度,第7位用来控制是否闪烁。
- 操作数所占空间
- byte、word、dword分别表示1字节,2字节和4字节
- 这些关键字指明了操作数的数据宽度(字节数),同C语言的变量类型一样。
- 如果源操作数和目的操作数已经明确了数据宽度,则指令中就不必显示地指明操作数所占空间了。
mov ax,0x10 //目的操作数ax是16位 mov byte [gs:0x00],'1' //‘1’的ASCII码是0x31,是个立即数,无法判断存储空间大小。 //byte是告诉[gs:0x00]这个'1'要用多少字节去存储它
九、硬盘
1.硬盘控制器
- 硬盘控制器:CPU与硬件的IO接口
- 硬盘控制器端口:我们通过读写硬盘控制器
- 端口分类
- Command Block registers:向硬盘驱动器写入字/从硬盘控制器获得硬盘状态
- Control Block registers:控制硬盘工作状态【基本用不到】
- Command Block register:
- 端口是按照通道给出的,一个通道上的主从两块磁盘都可以使用这个端口。要使用哪个端口的哪块磁盘需要单独指定。
- data端口:读取/写入数据,它是16位,其他寄存器都是8位。
- 当读硬盘中,硬盘控制器将数据存入缓冲区,然后不断读data端口将数据读出。
- 当写硬盘时,我们将数据源源不断地写入data端口,数据便被存入缓冲区中,硬盘控制器发现缓冲区中有数据了,便把数据写入对应磁盘中。
- data是端口,缓冲区是缓冲区,不是一个概念。
-
实模式的默认寄存器是16位,是指CPU,这些是硬盘中单独的寄存器。
- Error&Feature端口:
- Error:读磁盘失败时生效,里面会记录失败的信息。尚未读取的扇区数在sector count中。
- Feature:写磁盘时生效,有些命令会额外指定参数,这些参数保存在Feature中。
- Error和Feature是同一个寄存器,在对应环境下有不同的名字。
- Sector count端口:用来指定待存入/待写入的扇区数,每完成一个扇区,数值-1。8位寄存器,最大值是255。
- LBA:逻辑块地址,一种逻辑上为扇区址的方法,这里我们采用28位地址。
- LBA low:存储28位地址的0~7位
- LBA mid:存储28位地址的8~15位
- LBA high:存储28位地址的16~23位
- device端口:device寄存器是个杂项,宽度8位。
- 低4位用来存储LBA地址的24~27位。
- 第4位:指定通道上的主盘还是从盘,0代表主盘,1代表从盘。
- 第6位:设置是否用LBA方式,1代表用LBA,0代表用CHS。
- 第5位和7位固定为1,称为MBS位。
- Status端口:读磁盘时生效,用来给出磁盘的状态信息。
- 第0位:ERR位 = 1表示命令出错,具体原因见Error端口。
- 第3位:data request 位= 1表示磁盘已经把数据准备好,主机可以把它读出来。
- 第6位:DRDY位,表示磁盘检测正常,可以执行命令。
- 第7位:BSY位,表示磁盘是否繁忙,1表示忙碌。
- 其他位无效。
- command端口:和Status是同一端口,写磁盘时生效。只要把命令写入端口,硬盘就开始工作。
- identify:0xEC,磁盘识别
- read sector:0x20,读磁盘
- wirte sector:0x30,写磁盘
2.常见的磁盘操作方法
- 磁盘操作顺序:
- 先选择通道,向该通道的sector count端口中写入带读取扇区数
- 向该通道的三个LBA端口写入扇区起始地址的低24位
- 向device端口中写入LBA地址的24~27位,设置第6位=1,使其为LBA模式,设置第4位,选择操作的磁盘(master或slave磁盘)
- 向通道上的command端口写入操作命令
- 读取该通道的status端口,判断磁盘工作是否完成
- 如果以上步骤是读磁盘,进入7命令,否则结束
- 将磁盘数据读出
- 数据传送方式:(硬盘工作完成后,CPU如何获得数据呢?)
- 无条件传送方式
这些方式的数据源设备一定是随时准备好数据,CPU随时取随时拿都没问题,如寄存器、内存,CPU取数据不需要打招呼。 - 查询传送方式(程序IO,PIO)
在传送之前,由程序先去检测设备的状态。当数据源准备好时才能传送数据,因为数据源是低速设备。status端口里面保存了工作状态,可以对磁盘采用这种方式来获取数据。 - 中断传送方式(中断驱动IO)
当数据源准备好后自己通知CPU来取,通知CPU可以采用中断的方式。 - 直接存储器存取方式(DMA方式)
当数据源通过中断通知CPU时,CPU要压栈保护现场,浪费资源。设置一个DMA控制器(硬件),代替CPU由数据源和内存直接传输。当CPU需要数据时,直接来内存中拿即可。 - IO处理机传送方式
解放CPU,CPU不需要来内存取数据,IO处理机直接把数据处理也做了。
- 无条件传送方式
十、MBR
- 因为MBR只有512字节,无法保证能加载好内核。所以先在另一个程序中完成初始化环境以及加载内核的任务,这个程序称为loader加载器。
- 因为MBR在第0号扇区,loader邻着MBR不好,所以放在第2号扇区。
- 在实模式下,可用区域只有0x500~ 0x7BFF和0x7E00~ 0x9FBFF两块。因为loader中要加载一些数据结构(GDT全局描述符表等),所以loader加载到内核后不能被覆盖,又不像离loader太近,所以选择了0x900地址。