switch_to源代码:
/*
* switch_to(n) should switch tasks to task nr n, first
* checking that n isn't the current task, in which case it does nothing.
* This also clears the TS-flag if the task we switched to has used
* tha math co-processor latest.
*/
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
"je 1f\n\t" \
"movw %%dx,%1\n\t" \
"xchgl %%ecx,_current\n\t" \
"ljmp %0\n\t" \
"cmpl %%ecx,_last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
汇编知识:
这段汇编代码分为四个部分,以":"号加以分割,一般形式为:
指令部:输出部:输入部:损坏部
在指令部中,数字前面加上前缀%, 如%0, %1表示使用寄存器样板操作数,可以使用的此类操作数的总数取决于具体CPU中通用寄存器的数量。那么这里的数字具体代表那些寄存器呢?这里就要看输出部和输入部了
操作数的编号从输出部的第一个约束(序号为0)开始,顺序下来,每个约束计数一次,表示约束条件的字母很多,主要有:
"m","v","o" ——表示内存单元;
"r" ——表示任何寄存器;
"q" ——表示eax,ebx,ecs,edx 之一;
"i"和"h" ——表示直接操作数;
"E"和"F" ——表示浮点数;
"g" ——表示“任意”;
"a","b","c","d" ——表示使用寄存器eax,ebx,ecx,edx
"S"和"D" ——表示要求使用寄存器esi和edi
"I" ——表示常数(0至31)
实例如下:
__asm__ __volatile__ (
LOCK "addl %1, %0"
: "=m" (v->counter)
: "ir" (i), "m" (v->counter));
这段代码的输出部为v->counter,存储在内存单元中,对于的寄存器编号为%0,
输入部:直接操作数i对应于任何寄存器,对应的寄存器编号为%1, v->counter存储在内存单元中,对应的寄存器编号为%2
没有损坏部
这段代码的含义是:将v->counter的值加上i,并把最终的结果写回到v->counter中
那么对应到switch_to的这段代码中,由于没有输出部,所以输入部对应参数如下:
"m" (&__tmp.a) ===> %0, &tmp.a存储在内存中
"m" (*&tmp.b) ===> %1, *&__tmp.b存储在内存中
"d" (_TSS(n)) ===> %2, _TSS(n)存储在edx中
"c" ((long) task[n]) ===> %3, (long) task[n]存储在ecx中
这段代码的主要意思是:首先判断需要切换到的进程是否是当前进程,cmpl %%ecx,_current\n\t" \ "je 1f\n\t"
,如果是,则什么都不做,直接跳转到标号1处,退出程序; 否则进行进程上下文环境的切换。
"movw %%dx,%1\n\t"
将_TSS(n)的值存储在tmp.b中,那么TSS(n)代表什么呢?
#define FIRST_TSS_ENTRY 4
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
为了理解这段宏,需要知道GDT中各描述符的组织结构和TSS段选择符的结构
从图中可以看出,0 ---> NULL, 1 ---> code segment, 2 ---> data segment, 3 ---> syscall, 4 --->tss0, 5 ---> ldt0, 6 ---> tss1, 7 ---> ldt1, ...
__TSS(n)是为了获得进程n的段选择符,TSS段选择符结构如下:
图中 TI=0表示TSS在GDT中,1表示TSS在LDT中, RPL是请求特权级,控制内核态和用户态对段的访问,描述符索引就是在GDT或是LDT中的索引。如tss0的描述符索引为4.
宏#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
中,FIRST_TSS_ENTRY<<3
即为4<<3,这里的4表示GDT中第一个tss是从下标4开始的,左移3位是因为TSS段选择符的第三位用于其他用途; (unsigned long) n)<<4
中将进程编号n左移4位是为了保证每个进程的TSS段选择子在GDT中的索引均为偶数。TSS(0) = 1000000(二进制),对应下标为4, TSS(1) = 1100000(二进制),对应下标为6, TSS(2) = 1000000(二进制), 对应下标为8,以此类推... 因此,这里将TSS(n)的段选择符装入到 tmp.b中
"xchgl %%ecx,_current\n\t"
当前任务变为task[n], ecx = 被切换出的任务
ljmp %0\n\t
或ljmp *%0
是进行进程环境切换的关键,先分析如下:
加入'',这是gas的语法,表示绝对跳转,如果程序中没有加,编译器会自己加上
*ljmp用法说明:
AS手册中,ljmp指令存在两种形式,即:
回到switch_to源码,ljmp %0
用的是ljmp的第二种用法,ljmp %0
这条语句展开后相当于ljmp *__tmp.a
, 也就是跳转到地址&tmp.a中包含的48bit逻辑地址处。按照struct tmp的定义,这也意味着_tmp.a即为逻辑地址的offset部分,tmp.b的低16bit为seg_selector(高16bit无用)部分,在'ljmp %0'之前已经执行了movw %%dx,%1
将TSS(n)的段选择子装入了 tmp.b中。
通过以上说明,我们知道ljmp将跳转到TSS选择子所指向的地方,大致过程是:ljmp判断为TSS类型,于是就告诉硬件要切换任务,硬件首先要将当前的PC,esp, eax等现场信息保存在自己的TSS端描述符中,然后再将目标TSS段描述符中的pc, esp, eax的值拷贝至对应寄存器中,这些工作全部做完以后,内核就实现了进程上下文环境的切换,切换过程参考下图:
cmpl %%ecx,_last_task_used_math
原任务使用过协处理器吗jne 1f
没有,直接退出clts
原任务使用过协处理器, 清cr0中的任务切换标志TS
下面用伪指令来描述Jump做了什么事情, 又是怎么区分这些事件的
1. JMP(SelectorType Selector, int Offset)
2. {
3. SegAttributes Attributes;
4. SelectorType GSelector;
5. int Base, Limit, GOffset;
6. if((Selector & 0FFFCh) == 0)
7. SegmentException($GP, 0);
8. ReadDescriptor(Selector,&Attributes,&Base,&Limit,&GSelector,&GOffset);
9. if(Attributes.DType){
10. CSDescriptorLoad(Selector, Attributes,Base,Limit, $GP);
11. if(Offset > CS.Limit)
12. SegmentException($GP,0);
13. CS.Selector= Selector;
14. CS.Selector.RPL = CPL;
15. EIP = Offset;
16. }
17. else{
18. if((Attributes.DPL < CPL) || (Attributes.DPL < Selector.RPL))
19. SegmentException($GP, Selector);
20. switch(Attributes.Type){
21. case 1:
22. if(Attributes.P == 0)
23. SegmentException($NP, Selector);
24. TaskSwitch286(Selector, Attributes, Base, Limit, 0);
25. break;
26. case 5:
27. if(Attributes.P == 0)
28. SegmentException($NP, Selector);
29. TaskGate(GSelector, 0);
30. break;
31. case 9:
32. if(Attributes.P == 0)
33. SegmentException($NP, Selector);
34. TaskSwitch(Selector, Attributes, Base, Limit, 0);
35. break;
36. case 4:
37. if(Attributes.P == 0)
38. SegmentException($NP, Selector);
39. JumpGate286(GSelector, GOffset, $GP);
40. break;
41. case 12:
42. if(Attributes.P == 0)
43. SegmentException($NP, Selector);
44. JumpGate386(GSelector, GOffset, $GP);
45. break;
46. Default:
47. SegmentException($GP, Selector);
48. }
49. }
50.}
1.装入的参数为Selector 和 Offset
3.定义变量Attribute, 对应段描述符中的属性字段
4.定义变量Gselector, 如果是类型为门, 则此描述符存放的是选择子Selector和偏移Offset, 再根据Gselector找到对应的段描述符.
5.定义基地址, 界限和Goffset(偏移, 用于系统段或门)
6-7.选择子为空, 产生段异常.
8.根据选择子, 返回段描述符中的属性, 基地址和段界限. 如果为门, 则返回门选择子和偏移.
9.如果描述符为存储段
10.CSDescriptorLoad函数会进行特权级检查,如果出错,则会出现段异常. 再把Attribute,Base, Limit装入到CS 投影寄存器.(经常说的清CPU的prefetch queue)
11-12.检查是否越界, 是则产生段异常
13.单独装入Selector.(MOV指令无法修改CS寄存器,只能靠JUMP, CALL和IRET指令)
14.RPL装入CPL, 可以仔细思考一下为什么这么做?
15.EIP装入Offset. 和上面的CS组合起来, 就是CS:EIP, 这就实现了跳转了.
17.如果描述符类型为系统段或门
18-19.特权级检查, 出错则调用段异常函数
20.根据属性中的类型, 进行不同的处理
21-25.调用TaskSwitch286进行任务切换, 参数0表示无链接
26-30.调用任务门TaskGate() 函数, 0 表示 无链接
31-35.调用任务切换TaskSwitch() 函数
36-40.调用286 调用门JumpGate286()
41-45.调用386调用门JumpGate386()
接下来通过伪代码函数TaskSwitch()做了哪些事情
1. TaskSwitch(SelectorType Selector, SegAttributes Attributes, int Base, int Limit, int Linkage)
2. {
3. SelectorType OldTSS;
4. if(Limit < 103)
5. SegmentException($TS, Selector);
6. AccessTSSState(1); //Write
7. TR.Base = Base;
8. TR.Limit = Limit;
9. TR.Attributes = Attributes;
10. OldTSS = TR.Selector;
11. TR.Selector = Selector;
12. AccessTSSState(0); //read
13. if(Linkage == 1)
14. {
15. AccessLinear(TR.Base, 2, 0, 1, &OldTSS);
16. EFLAGS.NT = 1;
17. SetTSSBusy(Selector, 1);
18. }
19. else if(Linkage == -1)
20. SetTSSBusy(OldTSS, 0);
21. else if(Linkage == 0)
22. {
23. SetTSSBusy(OldTSS, 0);
24. SetTSSBusy(Selector, 1);
25. }
26. CR0.TS = 1;
27. CPL = CS.Selector.RPL;
28. LDTR.Attributes.Present = 0;
29. CS.Attributes.Present = 0;
30. SS.Attributes.Present = 0;
31. CS.Attributes.Present = 0;
32. DS.Attributes.Present = 0;
33. ES.Attributes.Present = 0;
34. FS.Attributes.Present = 0;
35. GS.Attributes.Present = 0;
36. if(LDTR.Selector.TI == 1)
37. SegmentException($TS, LDTR.Selector);
38. if((LDTR.Selector & 0FFFCh) == 0)
39. LDTR.Attributes.P = 0;
40. else
41. {
42. ReadDescriptor(LDTR.Selector, &Attributes, &Base, &Limit, &GSelector, &GOffset);
43. if((Attributes.DType == 1)
44. || (Attributes.Type != 2)
45. || (Attributes.Present == 0))
46. SegmentException($TS, LDTR.Selector);
47. SetAccessed(LDTR.Selector);
48. LDTR.Attributes = Attributes;
49. LDTR.Base = Base;
50. LDTR.Limit = Limit;
51. }
52. JumpGate(CS.Selector, EIP, $TS);
53. SRegLoad(SS, SS.Selector, $TS);
54. SRegLoad(DS, SS.Selector, $TS);
55. SRegLoad(ES, SS.Selector, $TS);
56. SRegLoad(FS, SS.Selector, $TS);
57. SRegLoad(GS, SS.Selector, $TS);
58.}
4.任务状态段至少有104个字节
6.当前机器的状态(EIP, EFLAGS,EAX,ECX, EDX, ESP, EBP…….)保存到老任务的TSS中
7-11.TR及投影寄存器指向新任务的TSS, 原来任务的selector保存到OldTSS中
12.把新任务TSS中的内容装入到硬件寄存器(EIP,EFLAGS, EAX,ECX, EDX, ESP, EBP…….)中.
13-25 根据链家字段的值做不同的处理
26-27 设置任务切换位及当前特权级
28-35 各描述符投影寄存器存在位预置为0
36-37 LDTR的选择子必须指定全局描述符表,否则产生异常
38-39 局部描述符表可以为空, 如为空, 将描述符表存在位设置为0
40.局部描述符表非空
41-51 读出描述符的属性, 基地址和段界限. 检查出错,产生段异常. 之后装入局部描述符表投影寄存器.
52.装入CS投影寄存器
53-57 装入SS, DS, ES, FS, GS投影寄存器
下面是JumpGate的伪代码:
JumpGate(SelectorType Selector, int Offset, int GP)
{
SegAttributes Attributes;
SelectorType GSelector;
int Base, Limit, GOffset;
if((Selector&0FFFCh) == 0)
SegmentException(GP, 0);
ReadDescriptor(Selector,&Attributes,&Base,&Limit,&GSelector,&GOffset);
if(Attributes.DType == 0)
SegmentException(GP, Selector);
Selector.RPL = 0;
CSDescriptorLoad(Selector,Attributes,Base,Limit,GP);
if(Offset > CS.Limit)
SegmentException(GP, 0);
CS.Selector = Selector;
CS.Selector.RPL = CPL;
EIP = Offset;
}
Reference:
http://blog.csdn.net/smallmuou/article/details/6837087
http://blog.csdn.net/tynew/article/details/7585004