段描述符
硬件中包含很多段,比如代码段、数据段等,每个段都由一个8字节的段描述符表示,段描述符存放在全局描述符表(GDT)或者局部描述符表(LDT)中,段描述符中的字段如下所示:
注意其中的Type字段就描述了段类型,下面是Linux中采用的各种段描述符中每个字段的情况
GDT地址和大小存放在gdtr控制寄存器中,LDT地址和大小存放在ldtr控制寄存器中
Linux对gdt的定义如下
Linux中对gdt的定义如下
ENTRY(cpu_gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* 0x0b reserved */
.quad 0x0000000000000000 /* 0x13 reserved */
.quad 0x0000000000000000 /* 0x1b reserved */
.quad 0x0000000000000000 /* 0x20 unused */
.quad 0x0000000000000000 /* 0x28 unused */
.quad 0x0000000000000000 /* 0x33 TLS entry 1 */
.quad 0x0000000000000000 /* 0x3b TLS entry 2 */
.quad 0x0000000000000000 /* 0x43 TLS entry 3 */
.quad 0x0000000000000000 /* 0x4b reserved */
.quad 0x0000000000000000 /* 0x53 reserved */
.quad 0x0000000000000000 /* 0x5b reserved */
.quad 0x00cf9a000000ffff /*__KERNEL_CS 0x60 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* __KERNEL_DS 0x68 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* __USER_CS 0x73 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /*__USER_DS 0x7b user 4GB data at 0x00000000 */
.quad 0x0000000000000000 /* 0x80 TSS descriptor */
.quad 0x0000000000000000 /* 0x88 LDT descriptor */
/* Segments used for calling PnP BIOS */
.quad 0x00c09a0000000000 /* 0x90 32-bit code */
.quad 0x00809a0000000000 /* 0x98 16-bit code */
.quad 0x0080920000000000 /* 0xa0 16-bit data */
.quad 0x0080920000000000 /* 0xa8 16-bit data */
.quad 0x0080920000000000 /* 0xb0 16-bit data */
/*
* The APM segments have byte granularity and their bases
* and limits are set at run time.
*/
.quad 0x00409a0000000000 /* 0xb8 APM CS code */
.quad 0x00009a0000000000 /* 0xc0 APM CS 16 code (16 bit) */
.quad 0x0040920000000000 /* 0xc8 APM DS data */
.quad 0x0000000000000000 /* 0xd0 - unused */
.quad 0x0000000000000000 /* 0xd8 - unused */
.quad 0x0000000000000000 /* 0xe0 - unused */
.quad 0x0000000000000000 /* 0xe8 - unused */
.quad 0x0000000000000000 /* 0xf0 - unused */
.quad 0x0000000000000000 /* 0xf8 - GDT entry 31: double-fault TSS */
可以看到四个主要的Linux段描述符的值
.quad 0x00cf9a000000ffff /*__KERNEL_CS 0x60 kernel 4GB code at 0x00000000 /
.quad 0x00cf92000000ffff / __KERNEL_DS 0x68 kernel 4GB data at 0x00000000 /
.quad 0x00cffa000000ffff / __USER_CS 0x73 user 4GB code at 0x00000000 /
.quad 0x00cff2000000ffff /__USER_DS 0x7b user 4GB data at 0x00000000 */
其Base都为0
逻辑地址
逻辑地址由16位的段选择符(又称为段标识符)和32位的指向段内相对地址的偏移量组成,段标识符的组成如下所示:
分段单元
给定一个逻辑地址,我们就可以结合段描述符得到对应的线性地址,
(1)先检查段选择符的TI字段,TI字段指明段描述符是存放在GDT还是LDT中,如果是在GDT中,则需要从gdtr寄存器中读取GDT的线性基地址,否则就从ldtr寄存器中读取LDT的线性基地址。
(2)从段选择符中读取index字段的值,因为一个段描述符的大小是8字节,所以index字段的值乘以8在加上gdtr或者ldtr的值就可以得到对应段描述符的地址。
(3)读取段描述符中的Base字段的值,其是段的首字节的线性地址,将其与逻辑地址的偏移量相加就得到线性地址。
上述过程如下图所示:
快速访问段描述符
从上述的分段过程可以看到每次都需要访问gdtr或者ldtr寄存器,然后才能找到对应的段描述符,为了加速逻辑地址到线性地址的转换,80x86提供了一种附加的非编程的寄存器含有8个字节的段描述符,供6个可编程的段寄存器使用(cs,ss,ds,es,fs,gs),当一个段选择符被装入段寄存器时,相应的段描述符就由内存装入对应的非编程CPU寄存器,从那时起,真对哪个段的逻辑地址转换就可以不访问主存中的GDT或LDT,处理器只需要直接饮用存放段描述符的CPU寄存器即可,仅当段寄存器的内容改变时,才有必要访问GDT或LDT。
Linux中的分段
分段可以给每个进程分配不同的线性地址空间,而分页把同一线性地址空间映射到不同物理地址空间,2.6版本的Linux只有在80x86体系结构下才使用分段。运行在用户态所有Linux进程都使用同一对用户代码段和用户数据段,运行在内核态的所有Linux进程都使用同一对内核代码段和内核数据段,下面是这四个段描述符字段的值
相应的段选择符由__KERNEL_CS、__KERNEL_DS、__USER_DS、__USER_CS分别定义,例如,为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的值装进cs寄存器即可,相应的内核代码段描述符就会自动装入对应的非编程CPU寄存器。
从上表可以看到,Linux所有的段地址都是从0x00000000开始,也就是说Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。
CPU的当前特权级(CPL)反映了进程是处于内核态还是用户态,并由存放在cs寄存器中的段选择符的RPL字段指定。只要当前特权级改变,一些段寄存器就必须被更新,比如当CPL=3(用户态)时,ds寄存器必须含有用户数据段的选择符,而当CPL=0(内核态)时,ds寄存器必须含有内核数据段的选择符,类似的情况也出现在cs寄存器中。
当对执向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符号,因为cs寄存器就含有当前的段选择符号,例如,当内核调用一个函数时,它执行一条call汇编语言指令,该指令仅指定它逻辑地址的偏移量部分,而段选择符不用设置,其隐含在cs寄存器中了。因为“在内核态执行” 的段只有一种,叫做代码段,由宏_KERNEL_CS定义,所以只要当CPU切换入内核态时将__KERNEL_CS装载入cs。同样的道理也适用于指向内核数据结构的指针(隐含地使用ds寄存器)以及指向用户数据结构的指针(内核显式地使用es寄存器)。