想看懂linux内核必须要理解保护模式,也许它并不复杂只是书上讲的太抽象了,下面让我们来认识一下保护模式,在这里我们只考虑全局描述符表GDT。
先来看下面两个重要的寄存器
CS:代码段寄存器(Code Segment Register),其值为代码段的段值
IP:指令指针寄存器,它用来存放代码段中的偏移地址。在程序运行过程中,它始终指向下一条指令的首地址,它与代码段寄存器CS联用确定下一条指令的物理地址
CPU要运行就要执行代码,执行那的代码呢?
CPU永远将CS:IP指向的内存单元中的内容看做要执行的下一条指令。
下面这一点其实也很重要:
jmp的实质就是修改CS和IP这两个寄存器
实模式下和保护模式最大的差别就是地址转换方式发生了变化(当然还有其他)
实模式下 :地址值=段值:偏移值=段值*0x10+偏移值((段值<<4)+偏移值)
保护模式下:地址值=选择子:偏移值=描述符表中第(选择子>>3)项描述符给出的段基址+偏移值
实模式下,假设在CS中存放的是0x8,IP中存入0xFFFF,
那么CS:IP=0x8*0x10+0xFFFF=0x1007F,程序下一步要执行的指令的地址就是0x1007F
这就是众所周知的“段地址左移4位加偏移”(CS<<4+IP)
保护模式下,假设CS=0x8,IP=0xFFFF
那么CS:IP=全局描述符表中第1(0x8>>3)项描述符给出的段基址+0xFFFF
其实CS中的值右移3位可理解为数组的下标,这个数组的首地址就存放在lgdtr寄存器中,这个数组就是全局描述符表GDT,数组中的数组项是一个叫做描述符的结构体
要用保护模式我们应该怎么做呢?我们用C语言来描述着个过程吧
1.定义一个描述符结构体Descriptor
typedef struct{
unsigned int lim_low,//段界限低16位(0-15).word lim&0xffff;
unsigned int base_low,//段基址低16位(0-15).word base&0xffff;
char base_mid.byte, //段基址中间8位(16-23)(base>>16)&0xff;
unsigned int type,//段界限高4位(16-19)与属性的组合.word ((lim>>8)&0xf00)|(type&0x0f0ff);
char base_high//段基址高8位(24-31).byte ((base>>24)&0xff)
}Descriptor;
2.设置区全局描述符表gdt
Descriptor gdt[3];
gdt[0] = Descriptor_DUMMY;//第0项不用里面全是0
gdt[1] = Descriptor_CODE32;//里面存放32位代码段的段基址和段界限
gdt[2] = Descriptor_VIDEO;//里面存放显存的段基址和段界限
3.将全局描述符表gdt的首地址和gdt的界限赋给gdtr寄存器,即lgdt命令
gdtr=((gdt<<16)|(gdt+sizeof(gdt));//
4.关中断,打开地址线A20,设置cr0寄存器
5.跳转,让CS:IP指向你要执行的代码即可
CS=0x8//取gdt表中的第1项,里面有段基址等信息,gdt表在那里?问gdtr寄存器
IP=0xff //偏移地址
大致流程是这样的,下面看一下AT&T汇编代码吧:
.include "pm.inc"
.text
.globl start
.code16
start:
jmpl $0x0, $code
/**-----------------------------------------------------------------
* Global Descriptor Table: GDT
*-------------------------------*/
GDT_START:
Descriptor_DUMMY: Descriptor 0, 0, 0
Descriptor_CODE32: Descriptor 0, 0xFFFFF, (DA_C | DA_32)
Descriptor_VIDEO: Descriptor 0xB8000, 0xFFFF, DA_DRW
.set GdtLen,(. - GDT_START) /* GDT Lenght */
GdtPtr: .2byte GdtLen /* GDT Limit */
.4byte GDT_START /* GDT Base */
msg:
.string "Hello world!"
code:
mov %cs,%ax
mov %ax,%ds
mov %ax,%es
mov %ax,%ss
mov $0x8000,%sp
/*显示HelloWorld字符串*/
mov $msg ,%ax
mov %ax ,%bp
mov $12 ,%cx
mov $0x1301,%ax
mov $0x000c,%bx
mov $0 ,%dl
int $0x10
/*加载gdtr即将全局描述符表gdt的首地址和gdt的界限赋给gdtr寄存器*/
lgdt GdtPtr
/*关中断*/
cli
/*打开地址线A20*/
inb $0x92,%al
or $0x02,%al
outb %al,$0x92
/*设置cr0寄存器,切换到保护模式*/
movl %cr0,%eax
or $1,%eax
movl %eax,%cr0
/*真正进入保护模式,执行此命令后CS=0x8,IP=LABEL_SEG_CODE32的偏移地址*/
ljmp $0x8,$(LABEL_SEG_CODE32)
/*此时CS:IP=全局描述符表中第1(0x8>>3)项描述符给出的段基址+LABEL_SEG_CODE32的偏移地址*/
LABEL_SEG_CODE32:
.align 32
.code32
movw $0x10,%ax
movw %ax,%gs
movl $((80*11+79)*2),%edi/*第11行,79列*/
movb $0x0c,%ah/*高四位表示黑底,低四位表示红字*/
movb $'P',%al/*显示的字符*/
movw %ax,%gs:(%edi)
loop2:
jmp loop2
.org 0x1fe, 0x90
.word 0xaa55
pm.inc中的代码:
/**-----------------------------------------------------------------
* 描述符数据结构
*-------------------------------------------------------------*/
.macro Descriptor Base, Limit, Attr
.2byte \Limit & 0xFFFF
.2byte \Base & 0xFFFF
.byte (\Base >> 16) & 0xFF
.2byte ((\Limit >> 8) & 0xF00) | (\Attr & 0xF0FF)
.byte (\Base >> 24) & 0xFF
.endm
/**-----------------------------------------------------------------
* 描述符属性
*----------------------------*/
.set DA_32, 0x4000 /* 32-bit segment */
.set DA_DRW, 0x92 /* Read/write */
.set DA_C, 0x98 /* Execute-only */
如果上面pm.inc中的代码不好理解也可以写成这样:
#define Descriptor(base,lim,type)\
.word lim&0xffff;\
.word base&0xffff;\
.byte (base>>16)&0xff;\
.word ((lim>>8)&0xf00)|(type&0x0f0ff);\
.byte ((base>>24)&0xff)
DA_C = 0x98
DA_32 = 0x4000
DA_DRW = 0x92
.text
.globl start
.code16
start:
jmpl $0x0, $code
GDT_START:
Descriptor_DUMMY:Descriptor(0x0,0x0,0x0)
Descript_CODE32 :Descriptor(0x0,0xffffffff,DA_C+DA_32)
Descriptor_VIDEO:Descriptor(0xb8000,0x0ffff,DA_DRW)
GDT_END:
GdtPtr:
.word (GDT_END-GDT_START)-1 # so does gdt
.long GDT_START # This will be rewrite by code.
msg:
.string "Hello world!"
code:
mov %cs,%ax
mov %ax,%ds
mov %ax,%es
mov %ax,%ss
mov $0x8000,%sp
/*显示HelloWorld字符串*/
mov $msg ,%ax
mov %ax ,%bp
mov $12 ,%cx
mov $0x1301,%ax
mov $0x000c,%bx
mov $0 ,%dl
int $0x10
/*加载gdtr即将全局描述符表gdt的首地址和gdt的界限赋给gdtr寄存器*/
lgdt GdtPtr
/*关中断*/
cli
/*打开地址线A20*/
inb $0x92,%al
or $0x02,%al
outb %al,$0x92
/*设置cr0寄存器,切换到保护模式*/
movl %cr0,%eax
or $1,%eax
movl %eax,%cr0
/*真正进入保护模式,执行此命令后CS=0x8,IP=LABEL_SEG_CODE32的偏移地址*/
ljmp $0x8,$(LABEL_SEG_CODE32)
/*此时CS:IP=全局描述符表中第1(0x8>>3)项描述符给出的段基址+LABEL_SEG_CODE32的偏移地址*/
LABEL_SEG_CODE32:
.align 32
.code32
movw $0x10,%ax
movw %ax,%gs
movl $((80*11+79)*2),%edi/*第11行,79列*/
movb $0x0c,%ah/*高四位表示黑底,低四位表示红字*/
movb $'P',%al/*显示的字符*/
movw %ax,%gs:(%edi)
loop2:
jmp loop2
.org 0x1fe, 0x90
.word 0xaa55
其实你也可以在jmp前面添加如下代码:
/*初始描述符Descript_CODE32*/
xor %eax, %eax
mov %cs, %ax # %cs -> % ax
shl $4, %eax # 左移四位
addl $(LABEL_SEG_CODE32), %eax # 下面是用LABEL_SEG_CODE32填充描述符Descript_CODE32
movw %ax, (Descript_CODE32 + 2)
shr $16, %eax
movb %al, (Descript_CODE32 + 4)
movb %ah, (Descript_CODE32 + 7)
然后这样跳转:
ljmp $0x8,$0
我们可以讲初始化描述符定义成一个宏:
.macro InitDescrptor Descriptor, SegBase
xor %eax, %eax
mov %cs, %ax /* %cs -> % ax */
shl $4, %eax /*左移四位 */
addl $(\SegBase), %eax /* 下面是用SegBase填充描述符Descriptor */
movw %ax, (\Descriptor + 2)
shr $16, %eax
movb %al, (\Descriptor + 4)
movb %ah, (\Descriptor + 7)
.endm
当然也可以这样:
/*
*InitDescrptor(Descriptor,SegBase)初始化描述符函数
*Descriptor:要初始化的描述符
*SegBase:段基址
*/
#define InitDescrptor(Descriptor,SegBase)\
xor %eax,%eax; \
mov %cs,%ax ; \
shl $4,%eax ; \
addl $(SegBase), %eax ;\
movw %ax, (Descriptor + 2);\
shr $16, %eax;\
movb %al, (Descriptor + 4);\
movb %ah, (Descriptor + 7)
这样我们就可以像C语言一样使用了
/*初始描述符Descript_CODE32*/
InitDescrptor(Descript_CODE32,LABEL_SEG_CODE32);
还记得吗jmp的实质就是改变CS和IP寄存器的值,我们只要保证CS:IP=我们的目的地,然后你就会看到跳转成功。
上面代码含有宏,因as的预处理能力有限,所以要先用gcc做预处理可以先gcc -E myboot.S > myboot.s或gcc -E myboot.S -o myboot.s再进行编译
也可用下面的Makefile进行编译:
# Makefile for the simple example kernel.
AS =as
LD =ld
LDFLAGS_CODE32 =-m elf_i386 -e startup_32 -Ttext 0
LDFLAGS_CODE16 = --oformat binary -N -e start -Ttext 0x7c00
Image:myboot #head
dd bs=512 if=myboot of=Image count=1 conv=notrunc
sync
myboot:myboot.s
$(AS) -o myboot.o -a myboot.s
$(LD) $(LDFLAGS_CODE16) -o myboot myboot.o
clean:
rm -f Image myboot.s myboot head *.o
贴出nasm的汇编代码以供参考:
DA_32 EQU 4000h ; 32 位段
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_C EQU 98h ; 存在的只执行代码段属性值
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
org 07c00h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
GDT_START:
Descriptor_DUMMY: Descriptor 0, 0, 0 ; 空描述符
Descript_CODE32: Descriptor 0, 0xffffffff, DA_C + DA_32; 非一致代码段
Descriptor_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - GDT_START ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; END of [SECTION .gdt]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
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 号中断
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, GDT_START ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword 8:LABEL_SEG_CODE32 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
BootMessage: db "Hello, OS world!"
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, 0x10
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]