段机制在8086和80386的区别
- 最直接的区别是寻址方式的不同,8086下计算逻辑地址使用段寄存器左移4位加偏移地址的方法;80386的段寄存器的作用变了,存放的是真正的地址在描述符表中的索引,计算方法变成了描述符表中的段基址加偏移地址的方法
- 更深层次区别是80386不仅计算逻辑地址的方式不同,而且CPU跳转时引入了权限检查,起到保护的作用
- 之前的认识误区:386引入段机制,是出于兼容8086的段机制,现在理解386并没有在寻址上兼容8086(如果兼容两者寻址算法应该一样)。所谓“兼容”是寄存器上的沿用,使8086引入的段寄存器不被抛弃,除了把段寄存器作为仍然作寻址用,还利用多出来低3位进行权限检查。使保护模式成为可能。
8086段寄存器寻址方式
- 一句话解释,cpu利用段寄器左移4bit取值,加上偏移地址算出物理地址
物理地址=段寄存器*16 + 偏移 - 实验
org 07c00h ; 告诉编译器程序加载到7c00处
jmp 07c0h:DispStrOff
code:
times 10 db 0
; never reach here
DispStrOff equ $ - $$
DispStr:
mov ax, BootMessage
mov bp, ax ; ES:BP = 串地址
mov cx, 16 ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 000ch ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
mov dl, 0
int 10h ; 10h 号中断
jmp $
BootMessage: db "Hello, OS world!"
root@hy:/home/work/orange/source/mywork# cat real_mode.dis
00007C00 EA0F00C007 jmp 0x7c0:0xf //使用段寄存器的跳转 cs:0x7c0 off=0xf
00007C05 0000 add [bx+si],al
00007C07 0000 add [bx+si],al
00007C09 0000 add [bx+si],al
00007C0B 0000 add [bx+si],al
00007C0D 0000 add [bx+si],al
00007C0F B8237C mov ax,0x7c23
00007C12 89C5 mov bp,ax
00007C14 B91000 mov cx,0x10
00007C17 B80113 mov ax,0x1301
00007C1A BB0C00 mov bx,0xc
00007C1D B200 mov dl,0x0
00007C1F CD10 int 0x10
00007C21 EBFE jmp short 0x7c21 // 没有使用段寄存器的短跳转,直接使用偏移。不使用CS参与计算。
......
上面一段程序直接写死了cs的值,依然可以成功跳转,因为我们知道cpu计算地址的方法,反汇编代码后得到跳转的目标地址0x7C0F,反推出CS的值。
JMP前,CPU准备执行下面这条指令,此时cs = 0,ip = 0x7C00。当前地址 = 0 * 16 + 0x7C00 = 0x7C00
CS没有内容。
00007C00 EA0F00C007 jmp 0x7c0:0xf
运行结果:
JMP执行后:当前地址 = 0x7C0 * 16 + 0xF = 0x7C0F,可以看到执行时CS寄存器被加载,IP寄存器被加载。
80386段寄存器寻址方式(分页未开启情况)
-
一句话解释,cpu利用段寄存器查找描述符获取物理地址基址,加上偏移算出物理地址
描述符表定义所有段的物理地址基址,段寄存器提供在表中查找描述符的索引物理地址 = GDT[index]取段基址部分 + OFF或者物理地址 = LDT[index]取段基址部分 + OFF
-
保护模式寻址流程
- 编写代码段,数据段
- 计算出段基址和段长度
- 定义GDT表,填入段基址
- 定义选择子,方便寻址时载入段寄存器
- 计算GDT地址,使用lgdt指令加载到GDTR
- 关中断,打开A20地址线,cr0最低位PE置位
- jmp指令加载选择子到CS,进入保护模式
-
实验
- 准备阶段
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 07c00h
jmp LABEL_BEGIN
;1)准备代码段,计算出段基址和段长度
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32: // 标号就是段基址
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32 // 计算段长度
; END of [SECTION .s32]
;2)定义GDT(Global Descriptor Table),先定义一个格式,段基址后面写入
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
;3)定义选择子
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
;4)填入之前没有写入的段基址
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
;5)计算GDT地址,加载 GDTR
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
;6)关中断,打开地址线,cr0 PE 位置位
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
;7)进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; END of [SECTION .s16]
运行结果:
我们在jmp dword SelectorCode32:0
前断住,查看当前指令地址0x000000007c8d
,段选择子0x0008
,cs内容为0。
选择子低三位做其它用,其余部分作为索引,index=8%8 = 1,指示cpu去gdt表的第1条去描述符,查看gdt表第一条基址为base=0x00007c04
,得到目标跳转地址0x00007c04+0=0x00007c04
。
附:段选择子介绍
上图是intel手册介绍段选择子的图片。
TI:Table Indicator,指示cpu去GDT(TI=0)取描述符,还是去LDT(TI=1)中取描述符。LDT表属于系统段,在GDT表之后加载。
RPL:Request Privilege Level,指示CPU申请权限。
实验完整代码见:my github 实模式与保护模式