x86
从计算机的历史谈起
- 远古时期的程序开发 : 直接操作物理内存
- CPU 指令的操作数直接使用实地址( 实际内存地址 )
- 程序员拥有绝对的权利( 利用 CPU 指哪打哪 )
绝对的权利带来的问题
- 难以重定位
程序每次都需要同样地址的内存执行,某一程序能在A电脑上运行,到了B电脑上就不一定能运行了(B电脑内存不一定够)
- 给多道程序设计带来障碍
不管内存多大,但凡一个字节被其它程序占用都无法执行,对于A程序和外设备交互,不再需要处理机了,这个时候处理机去调度B程序,如果A程序和B程序有重合那么问题就很大了
CPU 历史的里程碑- 8086
- 地址线宽度为 20 位 , 可访问 1M(2^20)内存空间
- 引入 [ 段地址 : 偏移地址 ] 的内存访问方式
8086的段寄存器和通用寄存器为16位
单个寄存器寻址最多访问64K 的内存空间
需要两个寄存器配合 , 完成所有内存空间的访问
这个时候重定位就非常简单了,只需要写死偏移地址,如果有冲突就修改段基址即可
深入解析 [ 段地址 : 偏移地址 ]
- 硬件所做的工作
段地址左移4 位 , 构成20 位的基地址( 起始地址 )
基地址 + 偏移地址 = 实地址
- 对于开发者的意义
更有效的划分内存的功能( 数据段 , 代码段 , 等 )
当出现程序地址冲突时 , 通过修改段地址解决冲突
1 2
| mov ax, [0x1234] ; 实地址 : (ds << 4) + 0x1234 如果不指定段地址,硬件会选择默认段地址,默认段地址存储在寄存器ds mov ax, [es:0xl234] ; 实地址 : (es << 4) + 0x1234
|
一个问题:[ 段地址 : 偏移地址 ] 能访问的最大地址为 0xFFFF : 0xFFFF即 : 10FFEF; 超过了(0xFFFFF)1MB 的空间 , CPU 如何处理 ?
8086 中的高端地址区( High Memory Area )
1 2 3 4 5
| 0XFFFF : 0XFFFF = 0XFFFF0 + 0XFFFF = 0XFFFF0 + (0XF + 0XFFF0) = (0XFFFF0 + 0xF) + 0XFFF0 = 0xFFFFF + 0xFFF0
|
8086的处理方式
由于 8086 只有 20 位地址线 , 因此最高位被丟弃( 溢出 ) !
1 2 3 4 5
| 0XFFFF : 0XFFFF = 10FFEF #16进制 = 100001111111111101111 #2进制 总共21位,超出一位 -> 00001111111111101111 #2进制 舍弃最高位变为20位 内存回卷 =0XFFEF
|
再谈 8086 历史
- 8086 在当时是非常成功的一款产品
- 因此 , 拥有一大批的开发者和应用程序
- 各种基于 8086 程序设计的技术得到了发展
- 不幸的是 , 各种奇技淫巧也应运而生
8086 时期应用程序中的问题
- 1MB 内存完全不够用( 内存在任何时期都不够用 )
- 开发者在程序中大量使用内存回卷技术( HMA 地址被使用 )
- 应用程序之间没有界限,相互之间随意干扰
• A 程序可以随意访问 B 程序中的数据
• C 程序可以修改系统调度程序的指令
80286的登场
- 8086已经有那么多应用程序了 , 所以必须兼容再兼容
- 加大内存容量 , 増加地址线数量( 24位 )
- [段地址:偏移地址] 的方式可以强化一下
为每个段提供更多属性( 如:范围,特权级,等 )
为每个段的定义提供固定方式
80286的兼容性
- 默认情况下完全兼容 8086 的运行方式( 实模式 )
- 默认可直接访问1MB 的内存空间
- 通过保护模式访问1MB+ 的内存空间 (地址线24位)
保护模式
初识保护模式
- 每一段内存的拥有一个属性定义 描述符,类似于变量( 描述符 Descriptor )
- 所有段的属性定义构成一张表,类似于数组( 描述符表 Descriptor Table )
- 段寄存器保存的是属性定义在表中的索引,访问数组需要“下标”( 选择子 Selector )
描述符的内存结构
段基址为什么放三个段然后拼接起来?这是个历史原因,硬件会帮我们拼接起来
描述符表
选择子
进入保护模式的方法
- 定义描述符表
- 打开 A20 地址线
80286默认情况下兼容8086运行方式,只使用0-19这20根地址线,访问超过1MB内存时,会自动回卷,
但是打开了A20地址线就可以使用更多地址线,访问的地址范围更大了
- 加载描述表
将描述表的地址放入一个特殊寄存器
- 通知 CPU 进入保护模式
将一个特殊寄存器的值0变为1
80286 的光荣退场
80386的登场( 计算机新时期的标志)
- 32 位地址总线( 可支持 4G 的内存空间 )
- 段寄存器和通用寄存器都为 32 位
任何一个寄存器都能访问到内存的任意角落
开启了平坦内存模式的新时代
段基址为 0 , 使用通用寄存器访问4G 内存空间
新时期的内存使用方式
- 实模式:兼容 8086的内存使用方式( 指哪打哪 )
- 分段模式:通过[段地址:偏移地址]的方式将内存从功能上分段(数据段,代码段 )
- 平坦模式:所有内存就是一个段[0:32位偏移地址]
一个问题:x86指的究竟是什么处理器?
8086,80286,80386…
GDT表初始化
GDT表项
段属性定义:
选择子属性定义:
保护模式的段定义
借助宏来实现
GDT的定义
这样借助宏只需要传入三个参数即可
汇编小贴士
- section 关键字用于”逻辑的“定义一段代码集合
- section 定义的代码段不同于 [ 段地址 : 偏移地址 ] 的代码段(内存中可执行的代码)
section定义的代码段仅限于源码中的代码段( 代码节 ),未经过编译以文本形式存在的代码段,只存在于源码,不会加载到物理内存中
[ 段地址 : 偏移地址]的代码段指内存中的代码段
汇编程序中以.
开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示,称为汇编指示(Assembler Directive)或伪操作(Pseudo-operation),由于它不是真正的指令所以加个“伪”字。.section
指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限。.data
段保存程序的数据,是可读可写的,相当于C程序的全局变量。
.s1代码段逻辑上是连续的,代码节之间存在内存对齐,必须四字节对齐,因此.s1补了两个字节的0
- [bits16] 用于指示编译器将代码按照16 位方式进行编译
- [bits32]用于指示编译器将代码按照32 位方式进行编译
初始化GDT
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
| ; inc.asm ; Segment Attribute DA_32 equ 0x4000 DA_DR equ 0x90 DA_DRW equ 0x92 DA_DRWA equ 0x93 DA_C equ 0x98 DA_CR equ 0x9A DA_CCO equ 0x9C DA_CCOR equ 0x9E
; Selector Attribute SA_RPL0 equ 0 SA_RPL1 equ 1 SA_RPL2 equ 2 SA_RPL3 equ 3
SA_TIG equ 0 SA_TIL equ 4
; 描述符 ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; Attr: dw (lower 4 bits of higher byte are always 0) %macro Descriptor 3 ; 段基址, 段界限, 段属性 dw %2 & 0xFFFF ; 段界限1 dw %1 & 0xFFFF ; 段基址1 db (%1 >> 16) & 0xFF ; 段基址2 dw ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0xFF ; 段基址3 %endmacro ; 共 8 字节
|
这里实模式实现的功能非常的简单,将32位只执行代码段的GDT表项初始好了,然后jmp强制跳转到了32位保护模式下
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
| %include "inc.asm"
org 0x9000
jmp CODE16_SEGMENT
[section .gdt] ; GDT definition ; 段基址, 段界限, 段属性 GDT_ENTRY : Descriptor 0, 0, 0 ;段描述表第一个不使用,仅作为占位符 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 ;保护模式下的32位只执行代码段 ; GDT end
GdtLen equ $ - GDT_ENTRY ;全局描述符表的长度
GdtPtr: dw GdtLen - 1 ;结构体界限 dd 0 ;GDT的起始地址,4字节 ; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 ;选择子 索引1 + 属性GDT + 特权级0
; end of [section .gdt]
;实模式16位代码段 [section .s16] [bits 16] CODE16_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; initialize GDT for 32 bits code segment 获取CODE32的真实段基地址 mov eax, 0 ;eax寄存器值清0 mov ax, cs ;eax = cs shl eax, 4 ;eax = cs<<4 add eax, CODE32_SEGMENT ;得到32位代码段的真实段基地址[cs << 4 + CODE32_SEGMENT] ;对段描述符的段基址部分进行初始化设置 mov word [CODE32_DESC + 2], ax ;32位代码段的段基地址低16位放到偏移2处 shr eax, 16 ;低16位被移出去了,ax变为了高16位 mov byte [CODE32_DESC + 4], al ;将段基地址17-23位放到偏移4处 mov byte [CODE32_DESC + 7], ah ;将段基地址24-31位放到偏移7处 ; initialize GDT pointer struct mov eax, 0 ;eax寄存器值清0 mov ax, ds ;eax = cs shl eax, 4 ;eax = cs<<4 add eax, GDT_ENTRY ;得到全局GDT段表的起始地址 mov dword [GdtPtr + 2], eax ;放入到结构体GdtPtr起始地址处
; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 4. enter protect mode mov eax, cr0 or eax, 0x01 mov cr0, eax ; 5. jump to 32 bits code jmp dword Code32Selector : 0 ;这里就已经使用的是选择子
[section .s32] [bits 32] CODE32_SEGMENT: mov eax, 0 jmp CODE32_SEGMENT
Code32SegLen equ $ - CODE32_SEGMENT
|
注意事项:
- 段描述表中的第 0 个描述符不使用( 仅用于占位 )
- 代码中必须显示的指明16 位代码段和 32 位代码段
- 必须使用jmp指令从 16 位代码段跳转到 32 位代码段
为什么不直接使用标签定义描述符中的段基地址,换种说法为什么CODE32_DESC在定义的时候设置段基地址为0,然后实模式下才去初始化好端基地址 ?
- NASM 将汇编文件当成一个独立的代码段编译
- 汇编代码中的标签( Label ) 代表的是段内偏移地址(逻辑地址)
- 实模式下需要配合段寄存器中的值才能计算得到标签的真实物理地址
为什么16 位代码段到 32 位代码段必须无条件换种说法32位代码段和16位代码段相连,为什么需要跳转 ?
- 处理器为了提高效率将当前指令和后缴旨令预取到流水线
- 因此 , 可能同时预取的指令中既有16 位代码又有 32 位代码
- 为了避免将 32 位代码用16 位的方式运行 , 需要刷新流水线
- 无条件jmp 能强制刷新流水
小结
- 80386 处理器是计算机发展史上的里程碑
- 32 位的寄存器和地址总线能够直接访问 4G 内存的任意角落
- 需要在16 位实模式中对 GDT 中的数据进行初始化
- 代码中需要为 **GDT 定义一个标识数据结构( GdtPtr )**,用于加载段描述符表
- 需要使用jmp 指令从 16 位代码跳转到 32 位代码
显存段
为了显示数据 , 必须存在两大硬件 : 显卡 + 显示器
- 显卡
为显示器提供需要显示的数据
控制显示器的模式和状态
- 显示器
将目标数据以可见的方式呈现在屏幕上
显存的概念和意义
- 显卡拥有自己内部的数据存储器 , 简称显存
- 显存在本质上和普通内存无差别 , 用于存储目标数据
- 操作显存中的数据将导致显示器上内容的改变
显卡的工作模式 : 文本模式 & 图形模式
- 在不同的模式下 , 显卡对显存内容的解释是不同的
- 可以使用专属指令或 int 0x10 中断改变显卡工作模式
- 文本模式下 :
显存的地址范围映射为 : [ 0XB8000,0xBFFFF ]
屏幕可以显示 25 行,每行 80 个字符(1个字符占用2字节,第一个字节存字符,第二个字节存属性,恰好可以用一个ax寄存器存下)
PrintString
1 2 3 4 5 6 7 8
| CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax ;将显存段的选择子放入gs寄存器 mov edi, (80 * 12 + 37) * 2 ;每个字符占用两个单元字符和属性 mov ah, 0x0C ;黑底红字方式显示 mov al, 'P' ;显示P字符 mov [gs:edi], ax ;放到显存段中 jmp CODE32_SEGMENT
|
在保护模式下 , 打印指定内存中的字符串
- 定义全局堆栈段( .gs ) ,用于保护模式下的函数调用
- 定义全局数据段( .dat ) , 用于定义只读数据 ( D.T.OS! )
- 利用对显存段的操作定义字符串打印函数( Printstring )
32 位保护模式下的乘法操作(mul)
- 被乘数放到 AX 寄存器
- 乘数放到通用寄存器或内存单元( 16位 )
- 相乘的结果放到 EAX 寄存器中
再 论 $ 和 $$
- $表示当前行相对于代码起始位置处的偏移量
- $$表示当前代码节( section )的起始位置
1 2 3 4 5
| [section .dat] [bits 32] FYOS db "FYOS!",0 ; 计算FYOS在当前代码节的偏移位置 FYOS_OFFSET equ FYOS -$$
|
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
| ; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds : ebp] ;cl 取得当前字符 cmp cl, 0 je end mov eax, 80 mul dh ;80*row -->eax add al, dl shl eax, 1 mov edi, eax ;mov edi, (80 * 12 + 37) * 2 mov ah, bl ;字符属性 mov al, cl ;打印字符放入al mov [gs:edi], ax ;放入显存 inc ebp ;下一个的位置 inc dl ;显存中下一个字符的打印位置
jmp print
|
全局栈段
32位代码段下调用一个函数,我们也需给32位代码段也应设置一个栈空间
1 2 3 4 5 6 7 8 9
| STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32 ;GDT描述符32位 可读可写
Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ;选择子 索引4 + 属性GDT + 特权级0 ;.... mov ax, Stack32Selector mov ss, ax ;.... call PrintString
|
这里并没有对esp指针进行初始化,似乎有bug?那么调用函数前esp的寄存器的值是多少?
这边可以反编译ndisasm -o 0x9000 loader > loader.txt
看一下对应文件然后设置断点调试
可以看到调用printstring函数前esp寄存器的值就是栈顶,是我们所预期的,那么这是为什么呢?
因为在16位实模式代码段就已经赋值0x7c000了,只要16位代码段的操作正确不乱修改栈,那么到32位下初始值也为0x7c00
保护模式下的栈段( Stack Segment )
- 指定一段空间 , 并为其定义段描述符
- 根据段描述符表中的位置定义筆子
- 初始化栈段寄存器( ss <- StackSelector )
- 初始化栈顶指针( esp <- TopOfStack )
因此对栈段进行修改,添加一个section
1 2 3 4 5 6 7
| [section .gs] [bits 32] STACK32_SEGMENT: times 1024 * 4 db 0
Stack32SegLen equ $ - STACK32_SEGMENT TopOfStack32 equ Stack32SegLen - 1
|
最终代码
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
| %include "inc.asm"
org 0x9000
jmp CODE16_SEGMENT
[section .gdt] ; GDT definition ; 段基址, 段界限, 段属性 GDT_ENTRY : Descriptor 0, 0, 0 ;段描述表第一个不使用,仅作为占位符 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 ;保护模式下的32位只执行代码段 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 ;显存段描述符 可读可写且显存已经被访问过了 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 ;32位 只读段 STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32 ;32位 可读可写,注意这个栈空间段基址无需初始化,从0~0x7c00作为我们的全局栈空间
; GDT end
GdtLen equ $ - GDT_ENTRY ;全局描述符表的长度
GdtPtr: dw GdtLen - 1 ;结构体界限 dd 0 ;GDT的起始地址,4字节 ; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 ;选择子 索引1 + 属性GDT + 特权级0 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 ;选择子 索引2 + 属性GDT + 特权级0 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 ;选择子 索引3 + 属性GDT + 特权级0 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ;选择子 索引4 + 属性GDT + 特权级0
; end of [section .gdt]
TopOfStackInit equ 0x7c00 [section .dat] [bits 32] DATA32_SEGMENT: FYOS db "F.Y.OS!", 0 FYOS_OFFSET equ FYOS - $$ ;数据段内偏移地址 Data32SegLen equ $ - DATA32_SEGMENT
;实模式16位代码段 [section .s16] [bits 16] CODE16_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStackInit ; initialize GDT for 32 bits code segment mov esi, CODE32_SEGMENT mov edi, CODE32_DESC
call InitDescItem
mov esi, DATA32_SEGMENT mov edi, DATA32_DESC call InitDescItem
; initialize GDT pointer struct mov eax, 0 ;eax寄存器值清0 mov ax, ds ;eax = cs shl eax, 4 ;eax = cs<<4 add eax, GDT_ENTRY ;得到全局GDT段表的起始地址 mov dword [GdtPtr + 2], eax ;放入到结构体GdtPtr起始地址处
; 1. load GDT lgdt [GdtPtr] ; 2. close interrupt cli ; 3. open A20 in al, 0x92 or al, 00000010b out 0x92, al ; 4. enter protect mode mov eax, cr0 or eax, 0x01 mov cr0, eax ; 5. jump to 32 bits code jmp dword Code32Selector : 0 ;这里就已经使用的是选择子
; esi --> code segment label ; edi --> descriptor label InitDescItem: push eax mov eax, 0 ;eax寄存器值清0 mov ax, cs ;eax = cs shl eax, 4 ;eax = cs<<4 add eax, esi ;得到32位代码段的真实段基地址[cs << 4 + CODE32_SEGMENT] ;对段描述符的段基址部分进行初始化设置 mov word [edi + 2], ax ;32位代码段的段基地址低16位放到偏移2处 shr eax, 16 ;低16位被移出去了,ax变为了高16位 mov byte [edi + 4], al ;将段基地址17-23位放到偏移4处 mov byte [edi + 7], ah ;将段基地址24-31位放到偏移7处 pop eax ret
[section .s32] [bits 32] CODE32_SEGMENT: mov ax, VideoSelector mov gs, ax ;将显存段的选择子放入gs寄存器
mov ax, Stack32Selector mov ss, ax
mov ax, Data32Selector mov ds, ax
mov ebp, FYOS_OFFSET ;都是32位段下面的,因此使用FYOS_OFFSET偏移 mov bx, 0x0C mov dh, 12 mov dl, 33
call PrintString
jmp $
; ds:ebp --> string address ; bx --> attribute ; dx --> dh : row, dl : col PrintString: push ebp push eax push edi push cx push dx print: mov cl, [ds : ebp] ;cl保存需要打印的字符 cmp cl, 0 ;判断是否为0 je end mov eax, 80 ; mul dh ;80*row -->eax add al, dl shl eax, 1 ;* mov edi, eax ;mov edi, (80 * 12 + 37) * 2 mov al, cl mov [gs:edi], ax inc ebp inc dl
jmp print end: pop dx pop cx pop edi pop eax pop ebp ret
Code32SegLen equ $ - CODE32_SEGMENT
|
小结
- 实模式下可以使用32位寄存器和32位地址
- 显存是显卡内部的存储单元,本质和普通内存无差别
- 显卡有两种工作模式:文本模式&图形模式
- 文本模式下操作显存单元中的数据能够立即反应到显示器上
保护模式返回实模式
本质上是处理器的两种工作模式,因此应该是允许相互跳转的。
处理器中的设计简介
- 80286 之后的处理器都提供兼容 8086 的实模式
- 然而,绝大多时候处理器都运行于保护模式
- 因此,保护模式的运行效率至关重要,性能瓶颈在于段描述符位于内存,必须读内存
- 处理器的速度远远快于内存读取速度,那么,处理器如何高效的访问内存中的段描述符 ???
解决方案:高速缓冲存储器
当使用选择子设置段寄存器时(mov操作)
• 根据选择子访问内存中的段描述符
• 将段描述符加载到段寄存器的高速缓冲存储器
• 需要段描述符信息时 , 直接从高速缓冲存储器中获得
一个问题:当处理器运行于实模式时 , 段寄存器的高速缓冲存储器是否会用到 ?
- 在实模式下,高速缓冲存储器仍然发挥作用
- 高速缓冲区的段基址是 32 位 , 实模式下其值是相应段寄存器的值乘以16
- 实模式下段基址有效位为 20 位 (32位的空间完全容纳的下),段界限固定为0xFFFF( 64K )
- 段属性的值不可设置(实模式下都是指哪打哪内存属性都是可读可写) , 只能继续沿用保护方式下所设置的值
高速缓冲存储器依然发挥作用,但是进入实模式时必须保证高速缓冲存储器段界限和段属性是正确的
然而高速缓冲存储器不允许直接访问但是又需要设置它的值
因此需要借助段描述符(mov)修改高速缓冲存储器的值,具体就是通过加载一个合适的描述符选择子到有关段寄存器 , 以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性 ! !
80x86 中的一个神秘限制
- 无法直接从 32 位代码段回到实模式
- 只能从 16 位代码段间接返回实模式
- 在返回前必须用合适的选择子对段寄存器赋值
保护模式下也是可以定义16位代码段,并且16位代码段也是存在运行于保护模式中,中转代码段不干任何其他操作,只用合适选择子给段寄存器赋值
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
| CODE16_DESC : Descriptor 0, 0xFFFF, DA_C UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW ;16位 可读可写
Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0 UPDATESelector equ (0x0006 << 3) + SA_TIG + SA_RPL0
;16位保护模式代码段,保护模式下jmp Code16Selector : 0即可跳转到这 [section .s16] [bits 16] CODE16_SEGMENT: mov ax, UPDATESelector ;定义一个特殊的段描述符用于刷新高速缓冲存储器 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ;注意不能刷新cs寄存器,因为这段代码还在保护模式下面
;退出保护模式 mov eax, cr0 and al, 11111110b mov cr0, eax
BACK_TO_REAL_MODE: jmp 0 : BACK_ENTRY_SEGMENT ;段间跳转,cs是未知的,因此应该在前面动态填入
|
深入讲解jmp指令:段内跳转和段间跳转是不同的,因此在实模式下可以考虑动态修改操作数,可以跳转到另一个段中期望的地址
因此在刚开始最先进入实模式时就可以mov [BACK_TO_REAL_MODE + 3], ax
动态的修改了这个段基址jmp 0 : BACK_ENTRY_SEGMENT--->jmp cs : BACK_ENTRY_SEGMENT
另外注意jmp 0 : BACK_ENTRY_SEGMENT
这里已经是在保护模式下运行了,但是我们并没有刷新cs寄存器对应的高速缓冲存储器,段界限段属性无法刷新,而如果设置的段界限太小了会直接报错,因为实模式下寻址范围是64k
因此在开始初始化的时候就应该将cs寄存器对应的高速缓冲存储器的偏移地址设置为0xFFFF
跳转到16位实模式后,对段寄存器的赋值只会改变相应高速缓冲区的段基地址,段界限和段属性沿用UPDATE_DESC中的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| BACK_ENTRY_SEGMENT: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, TopOfStack16
in al, 0x92 ;关闭A20地址线 and al, 11111110b out 0x92, al
sti ;启用硬件中断 mov bp, HELLO mov cx, 12 mov dx, 0 mov ax, 0x1301 mov bx, 0x0007 int 0x10
jmp $
|