保护模式

在16位cpu天下时,并没有实模式的概念,但是随着cpu发展到了32位,寄存器,总线等许多硬件设备得到了更新换代,随之而来的就是cpu新的运行模式,但新出的cpu必须兼容以前老版本的16位的运行模式,所以才有了保护模式和实模式之分。

GDT

到了保护模式下,内存段(如数据段、代码段等)不再是简单地用段寄存器加载一下段基址就能用,信息增加了很多,需要提前把段定义好才能使用。全局描述符表(Global Descriptor Table,GDT)是保护模式下内存段的登记表,寄存器GDTR负责指向它。

顾名思义,GDT是一个表,表中必然有表项,每一个表项大小为8个字,称作段描述符,用来描述各个内存段的起始地址、大小、权限等信息,可谓是相当详细。与之对应的,段寄存器也发生了变化,里面保存的不再是段地址,而是‘选择子’,selector,选择子用于索引GDT中的段描述符,看起来就像个数组下标一样。也正是因为段寄存器不再指向物理上的段地址,所以段寄存器中的地址没必要再左移四位硬凑20位的地址,一个32位的寄存器寻址范围是0x00000000 ~ 0xFFFFFFFF,即0~4GB,这样高效的寻址模式,称为平坦模式。

注意在GDT中第0(1)个描述符是不可用的,原因是在GDT中的段是要用选择子索引的,如果索引的选择子未初始化,选择子就是0,这将会导致直接指向第0个描述符。

段描述符

在保护模式模式下,段描述符是放在内存之中,占8个字大小,一共64位,每一位都有其特殊意义

image.png

  • 段基址:用于指向访问内存的起始地址

  • G:用于表示段界限的粒度大小,0表示粒度大小为1KB,1表示粒度大小为4GB

  • 段界限:限制程序访问的范围,但这里的段界限只是一个单位,真正的段界限=G(粒度)*(段界限+1)-1。段界限存在两种拓展方向,向高地址拓展或向低地址拓展。

  • S:用来指定描述符的类型,0表示系统段,1表示非系统段,非系统段同样分为两种:代码段和数据段

  • TYPE:TYPE一共有四位,用于指定这段非系统段的相关权限

    • X:EXecutable,表示该段是否可执行,数据和指令都是以同等地位存储在内存之中,但是指令可以执行但数据不能执行,所以可以用于区分代码段和数据段。代码段是可执行的,即 X 为 1。而数据段是不可执行的,即 X 为 0

    • R:Read,表示可读,R 为 1 表示可读,R 为 0 表示不可读

    • W:Write,表示可写,R 为 1 表示可写,R 为 0 表示不可写

    • C:Conforming,表示是否一致,一致性代码段是指如果自己是转移的目标段,并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级不与自己的 DPL 为主,而是与转移前的低特权级一致,也就是听从、依从转移前的低特权级。C 为 1 时则表示该段是一致性代码段,C 为 0 时则表示该段为非一致性代码段

    • A:Accessed,表示是否执行,由 CPU 设置,每当该段被 CPU 访问过后,CPU 就将此位置 1。

    • E:Extend,E 为 0 表示向上扩展,即地址越来越高,通q常用于代码段和数据段。E 为 1 表示向下扩展,地址越来越低,通常用于栈段。

      image.png

  • DPL:表示描述符特权级,这两位能表示 4 种特权级,分别是 0、1、2、3 级特权,数字越小,特权级越大。特权级是保护模式下才有的东西,CPU 由实模式进入保护模式后,特权级自动为 0,用户程序通常处于 3 特权级,权限最小。某些指令只能在 0 特权级下执行,从而保证了安全。

  • P:表示段是否存在,如果段存在于内存中,P 为 1,否则 P 为 0。P 字段是由 CPU 来检查的,如果为 0,CPU 将抛出异常,转到相应的异常处理程序。当初 CPU 的设计是当内存不足时,可以将段描述符中对应的内存段换出,也就是可以把不常用的段直接换出到硬盘,待使用时再加载进来。

  • AVL:操作系统可以随意用此位

  • L:用来设置是否是 64 位代码段。L 为 1 表示 64 位代码段,否则表示 32位代码段

  • D/B:有效表示地址(段内偏移地址)及操作数的大小。对于代码段来说,此位是 D 位,若 D 为 0,表示指令中的有效地址和操作数是 16 位,指令有效地址用 IP 寄存器。若 D 为 1,表示指令中的有效地址及操作数是 32 位,指令有效地址用 EIP 寄存器。对于栈段来说,此位是 B 位,用来指定操作数大小,此操作数涉及到栈指针寄存器的选择及栈的地址上限。若 B 为 0,使用的是 sp 寄存器,也就是栈的起始地址是 16 位寄存器的最大寻址范围,0xFFFF。若 B 为 1,使用的是 esp 寄存器,也就是栈的起始地址是 32 位寄存器的最大寻址范围,0xFFFFFFFF。

GDTR

GDT位于内存之中,寄存器GDTR(GDT Reigster)负责指向它,GDTR是个48位的寄存器,对于该寄存器的初始化由专门的指令lgdt来做。

_ZM3M2T2GXI7KCM_4GA__F6.png

选择子(selector)

段寄存器 CS、DS、ES、FS、GS、SS在实模式下时只能用于存储段基地址,而在进入保护模式之后,这一限制就取消了,由于段基址已经存入了段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西—selector.段寄存器是 16 位,所以选择子也是 16 位。

image.png

  • RPL:请求特权级,即请求特权级,可以表示 0、1、2、3 四种特权级
  • TI:0表示在GDT中索引描述符,1表示在LDT中索引描述符
  • 描述符索引值:指向GDT或LDT之间的索引符地址

加载选择子的保护

当引用一个内存段时,实际上就是往段寄存器中加载个选择子,为了避免出现非法引用内存段的情况,在这时候,CPU会在以下几方面做出检查。

  • 根据选择子的值验证段描述符是否越界
    • 描述符表基地址+选择子中的索引值*8+7 <=描述符表基地址+描述符表界限值。
  • 利用type位检查段的类型
    • 只有具备可执行属性的段(代码段)才能加载到 CS 段寄存器中。
    • 只具备执行属性的段(代码段)不允许加载到除 CS 外的段寄存器中。
    • 只有具备可写属性的段(数据段)才能加载到 SS 栈段寄存器中。
    • 至少具备可读属性的段才能加载到 DS、ES、FS、GS 段寄存器中image.png
  • 利用P位检查段是否存在
    • CPU 通过段描述符中的 P 位来确认内存段是否存在,如果P 位为 1,则表示存在,这时候就可以将选择子载入段寄存器了,同时段描述符缓冲寄存器也会更新为选择子对应的段描述符的内容,随后处理器将段描述符中的 A 位置为 1,表示已经访问过了。如果 P 位为 0,则表示该内存段不存在,不存在的原因可能是由于内存不足,操作系统将该段移出内存转储到硬盘上了。这时候处理器会抛出异常,自动转去执行相应的异常处理程序,异常处理程序将段从硬盘加载到内存后并将 P 位置为 1,随后返回。CPU 继续执行刚才的操作,判断 P 位。

LDT

GDT是全局描述符表,与之相对的自然有局部描述符表。CPU 厂商建议每个任务的私有内存段都应该放到自己的段描述符表中,该表就是 LDT,即每个任务都有自己的 LDT,随着任务切换,也要切换相应任务的 LDT。LDT 也位于内存中,其地址需要先被加载到某个寄存器后,CPU 才能使用 LDT,该寄存器是 LDTR,即 LDT Register。同样也有专门的指令用于加载 LDT,即 lldt。以后每切换任务时,都要用 lldt 指令重新加载任务的私有内存段。

进入选择模式

进入选择模式一共分为三步:第一步打开A20 ,第二步加载GDT ,第三步将CR0的PE置1

打开A20

CPU为了兼容地址回绕的问题,用A20Gate(第21根地址线)来控制是否开启地址回绕,所以在进入保护模式之前,要把A20给关闭

1
2
3
in al,0x92 
or al,0000_0010B
out 0x92,al
地址回绕(wrap-around)

实模式下由于采用的是“段地址:偏移地址”的方法,实模式下的寄存器都是16位,所以理论存在的最大地址为0xFFFF * 16 + 0xFFFF = 0x1FFEF。而实模式下的可用的地址线有20跟,最大寻址空间是0xFFFFF(1MB),从比较中可得知,寄存器的最大地址比寻址空间稍微大那么一点,但是这些多余的地址并没有指向真正物理地址。为了让这些多余的地址也能够利用起来,CPU采用的做法是将超过 1MB 的部分自动回绕到 0 地址,继续从 0地址开始映射。相当于把地址对 1MB 求模。超过 1MB 多余出来的内存被称为高端内存区 HMA。

CR0 寄存器的 PE 位

控制寄存器CRX是CPU的窗口,既可以展示CPU的运行状态,又可用于控制CPU的内部状态,这里使用的是寄存器的PE(protect enable)的第0位,这里置0即可

1
2
3
mov eax, cr0 
or eax, 0x00000001
mov cr0, eax

image.png

这里暂时只用到PE位

image.png

代码部分

boot.inc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
;---------------loader and kernel---------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;---------------GDT 描述符属性------------------
DESC_G_4K equ 1_00000000000000000000000b ;颗粒度:4K
DESC_D_32 equ 1_0000000000000000000000b ;操作数和地址大小:32位
DESC_L equ 0_000000000000000000000b ;是否是64位代码段:否
DESC_AVL equ 0_00000000000000000000b ;不用此位,暂设置为:0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;段界限19-16位
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;段界限19-16位
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;显存
DESC_P equ 1_000000000000000b ;表示段存在
DESC_DPL_0 equ 00_0000000000000b ;特权级:0
DESC_DPL_1 equ 01_0000000000000b ;特权级:1
DESC_DPL_2 equ 10_0000000000000b ;特权级:2
DESC_DPL_3 equ 11_0000000000000b ;特权级:3
DESC_S_CODE equ 1_000000000000b ;表示非系统段
DESC_S_DATA equ DESC_S_CODE ;数据段,属于非系统段一部分
DESC_S_SYS equ 0_000000000000b ;表示系统段
DESC_TYPE_CODE equ 1000_00000000b ;Type字段-代码段:x=1,c=0,r=0,a=0
DESC_TYPE_DATA equ 0010_00000000b ;Type字段-数据段:x=0,e=0,w=1,a=0

DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO2_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B

;--------------选择子 属性-------------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

注意书中的boot.inc是有错误的

1
DESC_VIDEO2_HIGH4 equ   (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

显存的起始地址是0xb8000,在段描述符低4字节中段基址0-15位存储的是0x8000,所以段描述符高4字节最初8位是段基址的23-16位的值应该是0xB,而不是0x00,所以这一行应该改成:

1
DESC_VIDEO2_HIGH4 equ   (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B

loader.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

jmp loader_start

;构建 GDT 及其内部的描述符
GDT_BASE:
dd 0x00000000
dd 0x00000000

CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4

DATA_STACK_DESC: ;直接用普通的数据段作为栈段
dd 0x0000FFFF
dd DESC_DATA_HIGH4

VIDEO_DESC:
dd 0x80000007 ;limit=(0xbffff - 0xb8000)/4k = 7
dd DESC_VIDEO2_HIGH4;此时dpl为0

GDT_SIZE equ $ - GDT_BASE ;获取 GDT 大小
GDT_LIMIT equ GDT_SIZE - 1 ;获取 段界限

times 60 dq 0 ;预留60个空位,为以后填入中断描述符表和任务状态段TSS描述符留空间
;times 60 表示后面的内容循环60次,是nasm提供的伪指令

SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

;以下是 gdt 指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

loadermsg db '2 loader in real.'

;---------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述符:打印字符串
;---------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX = 字符串长度
;(DH,DL)=坐标(行,列)
;ES:BP=字符串地址
;AL=显示输出方式
;0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;2——字符串中只含显示字符和显示属性。显示后,光标位置不变。
;3——字符串中只含显示字符和显示属性。显示后,光标位置改变。
;无返回值


loader_start:
;显示字符串,表示当前在实模式
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ;ES:BP 字符串地址
mov cx, 17 ;字符串长度
mov ax, 0x1301 ;AH=13h,AL=01h
mov bx, 0x001f ;页号为0(BH=0h),蓝底粉红字(BL=1fh)
mov dx, 0x1800 ;
int 0x10 ;int 10 BIOS中断

;准备进入保护模式
;1.打开A20地址线
in al, 0x92
or al, 00000010B
out 0x92, al

;2.加载GDT
lgdt [gdt_ptr]

;3.将CR0的PE位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
;流水线是CPU 的工作方式,会把当前指令和后面的几个指令同时放在流水线中重叠执行,由于之前的代码是16位,接下来的代码变成32位了,指令按照16位进行译码会出错,通过刷新流水线可以解决这个问题

[bits 32] ;编译成32位程序
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160],'P'

jmp $

mbr.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h

; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,4 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数

mov eax,esi ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al

;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al

;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al

;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55,0xaa

编译与运行

1
2
3
4
5
nasm -I include/ -o mbr.bin mbr.S 
nasm -I include/ -o loader.bin loader.S

dd if=mbr.bin of=../a.img bs=512 count=1 conv=notrunc
dd if=loader.bin of=../a.img bs=512 count=4 seek=2 conv=notrunc

运行效果

image.png

处理器微架构简介

流水线

CPU流水线技术是一种将指令分解为多步,并让不同指令的各步操作重叠,从而实现几条指令并行处理,以加速程序运行过程的技术。指令的每步有各自独立的电路来处理,每完成一步,就进到下一步,而前一步则处理后续指令。

CPU每执行一条指令都需要经过取指、译指、执行的过程。但在cpu中,每一步都有独立的电路结构去完成对应的能,就像流水线上的工人一样。这样的流水线结构大大增加了cpu的运行效率

image.png

值得一提的是,CPU 是按照程序中指令顺序来填充流水线的,也就是说按照程序计数器 PC(x86中是 cs:ip)中的值来装载流水线的,所以当执行到jmp指令时,cpu会自动清空原本的流水线

乱序执行

乱序执行,指的是cpu并不按照原本代码中的顺序执行,而是按照一定的策略打乱代码之后执行,所打乱的代码之间不具备相关性

1
2
3
4
;一段栈代码
mov eax , [0x1234]
push eax
call function

第一步中要访问内存[0x1234],而访存需要先寻址,在寻址过程之中可以先执行下面的代码push eax

第二步将push eax可以拆分为sub esp,4mov [esp],eax两部分

第三种将当前地址压入栈中,cs:ip跳转到对应的地址执行分支程序function

由于第 2 步中的微操作 sub esp,4,可以让 CPU 知道 esp 的最新值,不用等到 mov [esp], eax 完成,第 3 步 call 指令向栈中压入返回地址的操作就可以执行了。故第 2 步未执行完就开始第 3 步的执行了,也许第 3 步先于第 2 步完成。

分支预测

cpu的指令在流水上运行时,时常会遇到分支指令(如if,else),这时cpu就需要判断将哪个分支放在流水线上,这个判断的过程叫做分支预测

分支预测最简单的算法是 2 位预测法。用 2 位 bit 的计数器来记录跳转状态,每跳转一次就加 1,直到加到最大值 3 就不再加啦,如果未跳转就减 1,直到减到最小值 0 就不再减了。当遇到跳转指令时,如果计数器的值大于 1 则跳转,如果小于等于 1 则不跳。这只是最简单的分支预测算法,CPU 中的预测法远比这个复杂,不过它们都是从 2 位预测法发展起来的。

Intel 的分支预测部件中用了分支目标缓冲器BTB(Branch Target Buffer),CPU 遇到分支指令时,先用分支指令的地址在 BTB 中查找,若找到相同地址的指令,根据跳转统计信息判断是否把相应的预测分支地址上的指令送上流水线。在真正执行时,根据实际分支流向,更新 BTB 中跳转统计信息。如果BTB中没有相同的记录,这时候可以使用 静态预测器(Static Predictor),存储在里面的预测策略是固定写死的,它是由人们经过大量统计之后,根据某些特征总结出来的。程序在实际执行转移分支指令后,再将转移记录录入到 BTB。

jmp和流水线

在上面loader.s中有一段代码使用了远跳转,这里使用jmp有两个意图,一是加载正确的选择子,二是刷新流水线

1
jmp dword SELECTOR_CODE:p_mode_start

在实模式和保护模式中,段描述符缓冲寄存器都会保存段基地址,且在不重新引用一个段时,其中的存储的值是不会变的,所以从实模式进入保护模式时,原本20位的实模式段基址变成选择子就会出现错误,所以必须使用jmp来加载正确的选择子。

其次,在默认情况下,如果使用了未知的[bits]伪指令来配置运行环境,编译默认是按照16位实模式来编译。在loader.s中,完成的任务是由实模式进入保护模式,其中[bits]由16位变为32位,但由于cpu为了提高效率采用流水线,所以指令之间是重叠的,故原本应该是32位的程序仍然以16位来进行编译,这必然是错误的,为此必须清空已经装填好的流水线,即使用jmp指令来完成这一操作