现在是一头扎进操作系统里面,浩瀚的各种新老知识,一望无垠的学习内容,只能一个一个堡垒攻下来了。目前看到linux中加载tr和ldt寄存器时候的计算方法,一开始有点懵,貌似理解,又貌似不理解,琢磨了一段时间现在理解了,所以记录一下.
先直接上代码
```c
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
```
上面四行代码,先分别介绍下他们的功能。
前2行是用宏定义了2个变量值,分别是第一个tss(任务状态段)和第一个ldt(局部描述符)的在gdt(全局描述符)中的偏移量,tss是4,ldt是5(可以理解为数组下标)。
第3和第4个也是2个宏,分别计算TSS和LDT的起始内存地址。_TSS(n)表示第n个任务,他的tss所在的内存开始地址,同理_LDT(n)是第n个任务的ldt描述符所在的内存开始地址。
最后2行是内嵌的汇编语句,ltr和lldt指令分别是加载指定内存地址开始的tss和ldt。
好了,下面就看下算法的理解。拿tss这个来说明,ldt也是一样的意思。((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))这句语句先看后半句(FIRST_TSS_ENTRY<<3),这个等价于4<<3,其实可以理解为4*8,因为左移1位等于乘以2,所以左移3个等于乘以8。
这里要先说明下,在linux中gdt表是从内存地址0开始的,一个gdt描述符是8个字节(即64位),所以上面FIRST_TSS_ENTRY<<3的结果等于是第一个tss的内存地址,4*8嘛,即是跳过前4个gdt后的内存地址是多少。
然后我们在看下前半句,前半句n << 4,表示n * 16,这个又是什么意思呢。这里还要作下说明,tss和ldt都是表示一个任务,一个是任务当前所有上下午环境的数据,一个是这个任务所在的内存段以及信息,所以一个任务在gdt表中是占了2个描述符,一个描述符是8个字节,那么2个描述符就是16个字节。那么这句代码就很好理解了,就是前n个描述符占了多少字节。
然后把上面2句代码相加就是第n个任务tss所在的内存开始位置。同理_LDT(n)也是一样的。
好了,下面还有汇编代码,来看看是什么意思。ltr指令表示把指定内存开始的tss段选择子加载进tr寄存器。段选择子是个什么东西呢?下面简单介绍一下
段选择子是一个16位的数据,高13位表示描述符在gdt表中的下标,即是第几个gdt描述符。低2位是权限,0-3,表示RPL,即请求特权级,即只运行什么级别的请求来执行,第3位是Ti,表示该描述符是gdt还是ldt中的。由于现在都是在gdt中的,所以Ti是0,而目前GDT任务都是只允许内核来请求,所以低3位就都是0了,那么剩下就是计算出高13位gdt偏移量了。前面我们已经说过了上面2行代码计算出的是第n个描述符的内存地址,但是同时他也是他们的段选择子,所以说这里就比较巧妙了。
例如上面的计算n << 4 + FIRST_TSS_ENTRY << 3,前面是从内存地址的角度去理解这个算式的值,但是我们也可以从另一个角度来理解,我们说了段选择子低3位是这个描述符的权限等信息,高13位是gdt表的下标,这样我们理解FIRST_TSS_ENTRY << 3这句的时候可以看做把FIRST_TSS_ENTRY左移3位,即空出低3位FIRST_TSS_ENTRY就是第一个tss在gdt中的下标。然后n << 4可以理解为 n << 3 + n << 1, n << 3表示空出低3位,那么就与前面的FIRST_TSS_ENTRY对齐了,然后前面说过了每个任务有1个tss和一个ldt,那么两个tss之间就要跳过一个描述符,那么n<<1等于n*2,每个都是2的倍数,不就是表示第n个tss在gdt表中的下标吗?这样这个也就是一个段选择子了,是不是很巧妙。ldt也是同理的。
linux源码确实是有许多可以学习的地方,一个思想确实闪烁着前人的智慧,学习无止境。
linux加载tr和ldt寄存器时巧妙的计算方法