Linux 0.12 switch_to切换过程

Linux 0.12 switch_to切换过程

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中


下面对switch_to汇编代码进行详细分析

这段代码的主要意思是:首先判断需要切换到的进程是否是当前进程,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段选择符的结构

Linux 0.12 switch_to切换过程_第1张图片

从图中可以看出,0 ---> NULL, 1 ---> code segment, 2 ---> data segment, 3 ---> syscall, 4 --->tss0, 5 ---> ldt0, 6 ---> tss1, 7 ---> ldt1, ...

__TSS(n)是为了获得进程n的段选择符,TSS段选择符结构如下:

Linux 0.12 switch_to切换过程_第2张图片

图中 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\tljmp *%0是进行进程环境切换的关键,先分析如下:
加入'',这是gas的语法,表示绝对跳转,如果程序中没有加,编译器会自己加上
*ljmp用法说明:

AS手册中,ljmp指令存在两种形式,即:

  • 直接操作数跳转,此时操作数即为目标逻辑地址(选择子,偏移量), 即形如:ljmp $seg_selector, $offset的方式
  • 使用内存操作数,按照AS手册规定,内存操作数必须使用''做前缀,即形如:ljmp mem48,其中内存位置mem48存放目标逻辑地址:高16bit存放的是set_selector, 低32bit存放的是offset.注意,这条指令的''只是表示间接跳转的意思,与C语言的''完全不同

回到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的值拷贝至对应寄存器中,这些工作全部做完以后,内核就实现了进程上下文环境的切换,切换过程参考下图:
Linux 0.12 switch_to切换过程_第3张图片

cmpl %%ecx,_last_task_used_math 原任务使用过协处理器吗
jne 1f 没有,直接退出
clts 原任务使用过协处理器, 清cr0中的任务切换标志TS


ljmp 详解

下面用伪指令来描述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

你可能感兴趣的:(Linux,2.6.xx内核分析)