linux0.11 80386段
最近在看linux0.11源码,对其中的进程调度查了一些资料,《80386汇编语言精要》这本书写的非常好,对理解帮助很大,建议大家看一下,主要是两方面的知识:
- GDT\IDT与运行有什么关系?
- 80386有三种运行模式:实模式,保护模式,兼容8086模式
- linux0.11中具体是如何实现的
GDT\IDT与运行有什么关系?
先弄清楚以下这几个寄存器的作用(作用就是为了寻址)。
- GDT:该表地址存在GDTR寄存器里
全局描述符表 GDT ( Global Descriptor Table) , 除了任务门、中断门和陷井门描述符外 , 包含着系统中所有任务都可用的那些描述符。它的第一个 8 字节位置 没有使用。其中可以存放如下几种不同的描述符:
- 门:为什么task切换要使用软中断
门是用来控制访问在目标码段的入口点。有调用门、任务门、中断门和陷井门四种。所谓中断就是用于处理外部发生的突发事件, 陷井是一种特殊的中断, 用户定的软件中断其实就是陷井。调用门主要用于将程序控制转换到一个更高的特权级(数字低) , 任务门用于切换任务, 它只能涉及任务状态段。中断门和陷井门用于中断处理, 其中的地址是指向中断或陷井处理子程序起点的指针, 中断门禁止中断( IF = 0) , 而陷井门并不禁止。
- IDT:该表地址存在IDTR寄存器里
中断描述符表 IDT ( Interrupt Descriptor Table) , 可以包含 256 个描述符 , 每个描述符为 8 个字节。IDT 中只能包含任务门、中断门和陷井门描述符 , 虽然它最长也可以为 64K 字节 , 但只能存取 2K 字节以内的描述符 , 即 256 个。它最少为 256 个字节 ,因为 Intel 公司保留了 32 个中断描述符供自己使用。规定这些数字都是为了和早期的机器兼容。
- LDT:
局部描述符表 LDT ( Local Descriptor Table) , 包含了与一个给定任务有关的描述符 , 每一个任务都有一个各自的 LDT。有了 LDT, 就可以使给定任务的码、数据与别的任务相隔离。 LDT 中的描述符都在 GDT 中 , 所以 , 不同的任务可以有相同的描述符 , 这样就可以共享全局数据和代码。段描述符:就是存在GDT中的项
-
TSS:在 GDT
所谓任务状态段 TSS ( Task State Segment) , 就是一个特殊的固定格式的段 , 它包含了一个任务和与之相链接的允许嵌套的任务的所有状态信息!这个段在task切换中非常重要,我后面再分析。当任务状态段描述符所描述的任务正在执行时, 它就是类型B, 否则就置成类型9。TSS 就由任务状态寄存器TR 来标识,TR有16位可见+64位不可见,16位可见就是段选择子,相当于GDT中的索引,TR通过可见的16位找到TSS。执行IRET 时, 控制返回到原来的任务, 当前的任务状态则被保存到TSS 中, 并从原有任务的TSS 中恢复原有任务的状态。
- 段选择子:(放在段寄存器里,80386中有6个(即CS,SS,DS,ES,FS,GS)16位的段寄存器)
在实模式下 , 段寄存器存储的是真实的段地址 , 在保护模式下 , 16 位的段寄存器无法放下 32 位段地址 , 因此 , 它们被称为选择器 , 即起选择描符述表中的描述符的作用。这样 , 就把描述符中的 32 位段地址做为实际的段地址。
80386有三种运行模式:实模式,保护模式,兼容8086模式。
兼容8086模式很好理解!每种模式下寄存器的内容均有不同的意义,实模式与保护模式主要为了通过硬件的设计添加错误检查。你可以设计的程序只运行在实模式(实模式是16位,保护模式是32位)所有的地址都是硬件地址,随便一个指针错误都会造成死机,当然结果就是系统很容易崩溃!linux中setup.s最后几句就是进入保护模式。
# Well, now's the time to actually move into protected mode. To make # things as simple as possible, we do no register set-up or anything, # we let the gnu-compiled 32-bit programs do that. We just jump to # absolute address 0x00000, in 32-bit protected mode. #mov $0x0001, %ax # protected mode (PE) bit #lmsw %ax # This is it! mov %cr0, %eax # get machine status(cr0|MSW) bts $0, %eax # turn on the PE-bit mov %eax, %cr0 # protection enabled |
进入保护模式后,软件使用逻辑地址访问时,硬件可以通过段描述符检查是否溢出。
例如:若给定一个逻辑地址是 a:b ,根据逻辑地址的a(段选择符)的T1位确定是选择GDT还是LDT。
-
a若是T1位选择GDT,根据GDTR找到GDT的基址,根据a的 3~15位确定它的段描述符X在GDT中的位置(GDTR即基址+a的3-15bit即相对位置):确定段描述符X,再根据段描述符提取出其中包含的段基址信息,段基址+b(段内偏移),最终确定线性地址。
-
a若是T1位选择LDT,根据GDTR找到GDT的基址,根据LDTR的高13位确定它的LDTX描述符在GDT中的位置(GDTR基址+LDTR13bit即相对位置):确定LDTX描述符。LDTX描述符可以确定LDT的基址(LDTX描述符确定LDT表在内存中的起始位置),再根据段选择符a确定的相对位置,可以确定LDT中的私有段描述符Y。接下来同上面的:再根据段描述符提取出其中包含的段基址信息,段基址+b(段内偏移),最终确定线性地址。
linux0.11中具体是如何实现的?
在setup.s中有加载idt与gdt,指令如下:
lidt idt_48 # load idt with 0,0 |
lidt与lgdt操作数是48位,6 字节操作数装入全局描述符表寄存器IDTR与GDTR。
setup.s代码中的定义如下:需要注意的是此时系统还处于实模式16位操作。
gdt: .word 0x07FF # 8Mb - limit=2047 (2048*4096=8Mb) .word 0x07FF # 8Mb - limit=2047 (2048*4096=8Mb) idt_48: gdt_48: |
然后通过以下指令进入保护模式并跳转。跳转指令是ljmp。
#mov $0x0001, %ax # protected mode (PE) bit #lmsw %ax # This is it! mov %cr0, %eax # get machine status(cr0|MSW) bts $0, %eax # turn on the PE-bit mov %eax, %cr0 # protection enabled # segment-descriptor (INDEX:TI:RPL) .equ sel_cs0, 0x0008 # select for code segment 0 ( 001:0 :00) ljmp $sel_cs0, $0 # jmp offset 0 of code segment 0 in gdt |
进入保护模式之后第一件事是什么,当然是设置堆栈。这个时候再回头看head.s的开头就更清楚了,之前的boot分析其实还不是很懂这里为什么要lss两次。如果第一次lss不设置,那么这个堆栈寄存器esp根本就没有初始化。
startup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss stack_start,%esp call setup_idt call setup_gdt movl $0x10,%eax # reload all the segment registers mov %ax,%ds # after changing gdt. CS was already mov %ax,%es # reloaded in 'setup_gdt' mov %ax,%fs mov %ax,%gs lss stack_start,%esp xorl %eax,%eax 1: incl %eax # check that A20 really IS enabled movl %eax,0x000000 # loop forever if it isn't cmpl %eax,0x100000 je 1b |