分析Linux 0.11中的任务切换

函数switch_to()

在Linux 0.11中,Linus给出的切换程序看似非常简单,只是sched.h中的一段汇编代码:

#define switch_to(n) {\
struct {long a,b} __tmp;\
__asm__("cmpl %%ecx,_current\n\t"\      //任务n是当前任务吗?(current == task[n]?)
        "je 1f\n\t"\                    //是,则什么都不做,退出。
        "movw %%dx,%1\n\t"\             //将新任务TSS的16位选择符存入__tmp.b中。
        "xchgl %%ecx,_current\n\t"\     //current = task[n]; ecx = 被切换出的任务。
        "ljmp %0\n\t"\                  //执行长跳转到*&__tmp,造成任务切换。
                                        //在任务切换回来后才继续执行下面的语句。
        "cmpl %%ecx,_last_task_used_math\n\t"\ //原任务上次使用过协处理器吗?
        "jne 1f\n\t"\                   //没有,则跳转,退出
        "clts\n"\                       //原任务使用过协处理器,则清cr0中的任务切换标志TS
        "1:"\
        :\
        :"m"(*&__tmp.a),\               //嵌入式汇编变量%0,指向__tmp。
        "m"(*&__tmp.b),\                //嵌入式汇编变量%1,指向__tmp.b,用于存放新TSS的选择符。
        "d"(_TSS(n)),\                  //嵌入式汇编中使用寄存器edx,计算新任务n的TSS段选择符。
        "c"((long) task[n]));\          //嵌入式汇编中使用寄存器ecx,存放新任务n的任务结构指针task[n]。
}

对于整段程序,我最不理解的就是执行任务切换的那一行汇编代码"ljmp %0\n\t。赵博给出的解释是,这个跳转的地址并不是一个简单的段内偏移值,而是16位的段选择符:32位偏移地址,而造成跳转时32位的偏移地址并无用处。但是我还是不太理解跳转到底程序去了哪里。

通过debug,我发现系统在main函数中的fork()后很快就做了一次任务切换,目的是从任务0切换到任务1。

void main(void)
{
    ...
    move_to_user_mode();    //从内核模式切换到用户模式
    if (!fork()) {          //创建子进程(任务1)
        init();
    }

    for(;;) pause();        //调用系统函数pause(),系统可以进行任务切换
}

switch_to()的debug

用bochs 2.3.7版本看switch_to()对应的机器码的反汇编代码如下:

00006c5c: (                    ): mov edx, esi              ; 89f2
00006c5e: (                    ): shl edx, 0x04             ; c1e204
00006c61: (                    ): add edx, 0x00000020       ; 83c220
00006c64: (                    ): mov ecx, dword ptr ds:[esi*4+0x191a4] ; 8b0cb5a4910100
00006c6b: (                    ): cmp dword ptr ds:0x1919c, ecx ; 390d9c910100
00006c71: (                    ): jz .+0x00000017           ; 7417
00006c73: (                    ): mov word ptr ss:[ebp+0xfffffffc], dx ; 668955fc
00006c77: (                    ): xchg dword ptr ds:0x1919c, ecx ; 870d9c910100
00006c7d: (                    ): jmp far ss:[ebp+0xfffffff8] ; ff6df8
00006c80: (                    ): cmp dword ptr ds:0x191a0, ecx ; 390da0910100
00006c86: (                    ): jnz .+0x00000002          ; 7502
00006c88: (                    ): clts                      ; 0f06

可以看到跳转指令变成了jmp far ss:[ebp+0xfffffff8],对比代码,我们可以知道__tmp应该是存放在ss:[ebp+0xfffffff8]

从0x6c77这一行单步执行的情况如下:

<bochs:20> s
Next at t=80078778
(0) [0x00006c77] 0008:0000000000006c77 (unk. ctxt): xchg dword ptr ds:0x1919c, e
cx ; 870d9c910100
<bochs:21> print-stack
Stack address size 4
 | STACK 0x00019148 [0x00000003]
 | STACK 0x0001914c [0xffff0000]
 | STACK 0x00019150 [0x00000ffc]
 | STACK 0x00019154 [0x0001f248]
 | STACK 0x00019158 [0x00000030]
 | STACK 0x0001915c [0x0001f248]
 | STACK 0x00019160 [0x00006ca4]
 | STACK 0x00019164 [0x0000743b]
 | STACK 0x00019168 [0x00000003]
 | STACK 0x0001916c [0x0003e400]
 | STACK 0x00019170 [0x0001fae4]
 | STACK 0x00019174 [0x00000017]
 | STACK 0x00019178 [0x00000017]
 | STACK 0x0001917c [0x00000017]
 | STACK 0x00019180 [0x000067a1]
 | STACK 0x00019184 [0x0000000f]
<bochs:22> r
rax: 0x00000000:00fff000 rcx: 0x00000000:00fff000
rdx: 0x00000000:00000030 rbx: 0x00000000:0000000f
rsp: 0x00000000:00019148 rbp: 0x00000000:0001915c
rsi: 0x00000000:00000001 rdi: 0x00000000:000191a8
r8 : 0x00000000:00000000 r9 : 0x00000000:00000000
r10: 0x00000000:00000000 r11: 0x00000000:00000000
r12: 0x00000000:00000000 r13: 0x00000000:00000000
r14: 0x00000000:00000000 r15: 0x00000000:00000000
rip: 0x00000000:00006c77
eflags 0x00001683
id vip vif ac vm rf nt IOPL=1 of DF IF tf SF zf af pf CF
<bochs:23> s
Next at t=80078779
(0) [0x00006c7d] 0008:0000000000006c7d (unk. ctxt): jmp far ss:[ebp+0xfffffff8]
; ff6df8
<bochs:24> r
rax: 0x00000000:00fff000 rcx: 0x00000000:00018194
rdx: 0x00000000:00000030 rbx: 0x00000000:0000000f
rsp: 0x00000000:00019148 rbp: 0x00000000:0001915c
rsi: 0x00000000:00000001 rdi: 0x00000000:000191a8
r8 : 0x00000000:00000000 r9 : 0x00000000:00000000
r10: 0x00000000:00000000 r11: 0x00000000:00000000
r12: 0x00000000:00000000 r13: 0x00000000:00000000
r14: 0x00000000:00000000 r15: 0x00000000:00000000
rip: 0x00000000:00006c7d
eflags 0x00001683
id vip vif ac vm rf nt IOPL=1 of DF IF tf SF zf af pf CF
<bochs:25> x /4x ds:0x1919c
[bochs]:
0x000000000001919c <bogus+       0>:    0x00fff000      0x00000000      0x000181
94      0x00fff000
<bochs:26> s
Next at t=80078780
(0) [0x0000677c] 000f:000000000000677c (unk. ctxt): test eax, eax             ;
85c0
<bochs:27> r
rax: 0x00000000:00000000 rcx: 0x00000000:0003e400
rdx: 0x00000000:00000021 rbx: 0x00000000:00000003
rsp: 0x00000000:0001f248 rbp: 0x00000000:0001f248
rsi: 0x00000000:ffff0000 rdi: 0x00000000:00000ffc
r8 : 0x00000000:00000000 r9 : 0x00000000:00000000
r10: 0x00000000:00000000 r11: 0x00000000:00000000
r12: 0x00000000:00000000 r13: 0x00000000:00000000
r14: 0x00000000:00000000 r15: 0x00000000:00000000
rip: 0x00000000:0000677c
eflags 0x00001616
id vip vif ac vm rf nt IOPL=1 of DF IF tf sf zf AF PF cf
<bochs:28> print-stack
Stack address size 4
 | STACK 0x0001f248 [0x0000d3cc]
 | STACK 0x0001f24c [0x00005412]
 | STACK 0x0001f250 [0x00000000]
 | STACK 0x0001f254 [0x00000000]
 | STACK 0x0001f258 [0x00000000]
 | STACK 0x0001f25c [0x00000000]
 | STACK 0x0001f260 [0x00000000]
 | STACK 0x0001f264 [0x00000000]
 | STACK 0x0001f268 [0x00000000]
 | STACK 0x0001f26c [0x00000000]
 | STACK 0x0001f270 [0x00000000]
 | STACK 0x0001f274 [0x00000000]
 | STACK 0x0001f278 [0x00000000]
 | STACK 0x0001f27c [0x00000000]
 | STACK 0x0001f280 [0x00000000]
 | STACK 0x0001f284 [0x00000000]

从上面的log可以看出,在0x00006c7d这一行单步执行后,系统跳转到了0x0000677c这一行,仔细看前面的段寄存器描述符,从0x0008变为0x000f,表明系统从内核态转为用户态。另外,stack和寄存器也都发生了变化。
只是单纯的一行跳转就发生了这么多变化,基本上来说不大可能是软件实现的,看来是CPU实现的功能,但是这些状态也肯定是预先存放某个地方。

任务切换后跳转到的程序

我们再来看这个地址附近机器码的反汇编程序:

00006752: (                    ): sti                       ; fb
00006753: (                    ): mov eax, esp              ; 89e0
00006755: (                    ): push 0x00000017           ; 6a17
00006757: (                    ): push eax                  ; 50
00006758: (                    ): pushfd                    ; 9c
00006759: (                    ): push 0x0000000f           ; 6a0f
0000675b: (                    ): push 0x00006761           ; 6861670000
00006760: (                    ): iretd                     ; cf
00006761: (                    ): mov eax, 0x00000017       ; b817000000
00006766: (                    ): mov ds, ax                ; 668ed8
00006769: (                    ): mov es, ax                ; 668ec0
0000676c: (                    ): mov fs, ax                ; 668ee0
0000676f: (                    ): mov gs, ax                ; 668ee8
00006772: (                    ): add esp, 0x0000000c       ; 83c40c
00006775: (                    ): mov eax, 0x00000002       ; b802000000
0000677a: (                    ): int 0x80                  ; cd80
0000677c: (                    ): test eax, eax             ; 85c0
0000677e: (                    ): jnl .+0x0000000c          ; 7d0c
00006780: (                    ): neg eax                   ; f7d8
00006782: (                    ): mov dword ptr ds:0x1fae4, eax ; a3e4fa0100
00006787: (                    ): mov eax, 0xffffffff       ; b8ffffffff
0000678c: (                    ): test eax, eax             ; 85c0
0000678e: (                    ): jnz .+0x00000005          ; 7505
00006790: (                    ): call .+0x000000ef         ; e8ef000000
00006795: (                    ): mov edx, 0x0001fae4       ; bae4fa0100
0000679a: (                    ): mov eax, 0x0000001d       ; b81d000000
0000679f: (                    ): int 0x80                  ; cd80
000067a1: (                    ): test eax, eax             ; 85c0
000067a3: (                    ): jnl .+0xfffffff5          ; 7df5
000067a5: (                    ): neg eax                   ; f7d8
000067a7: (                    ): mov dword ptr ds:[edx], eax ; 8902
000067a9: (                    ): jmp .+0xffffffef          ; ebef
000067ab: (                    ): add cl, cl                ; 00c9
000067ad: (                    ): ret                       ; c3

从截取的代码可以看到,程序地址0x0000677c实际上就在main函数里,正是系统发送中断0x80调用fork()函数之后的地方。
这时,我想到,在调用fork()时,系统首先调用find_empty_process()申请了进程的任务号,然后调用copy_process()复制了大部分的父进程信息,那么这个程序地址很有可能就是在此时存储到子进程中的。

TSS中存储的内容

我们可以重新运行一次系统,在任务切换之前看看内存中存储的被切换进程的任务状态信息。
任务状态信息保存在任务对应的TSS描述符指向的TSS中,通过分析,我看到任务1的task_struct保存在0xfff000这个地方,而它的tss_struct就在task_struct的末尾处,通过分析task_struct,我得到tss_struct的偏移为0x02ec。

<bochs:23> x /32x 0x0008:0xfff2d0
[bochs]:
0x0000000000fff2d0 <bogus+       0>:    0x00000000      0x00000000      0x000000
9f      0x04c0fa00
0x0000000000fff2e0 <bogus+      16>:    0x0000009f      0x04c0f300      0x000000
00      0x01000000
0x0000000000fff2f0 <bogus+      32>:    0x00000010      0x00000000      0x000000
00      0x00000000
0x0000000000fff300 <bogus+      48>:    0x00000000      0x00000000      0x000067
7c      0x00001616
0x0000000000fff310 <bogus+      64>:    0x00000000      0x0003e400      0x000000
21      0x00000003
0x0000000000fff320 <bogus+      80>:    0x0001f248      0x0001f248      0xffff00
00      0x00000ffc
0x0000000000fff330 <bogus+      96>:    0x00000017      0x0000000f      0x000000
17      0x00000017
0x0000000000fff340 <bogus+     112>:    0x00000017      0x00000017      0x000000
38      0x80000000

整理结果如下:

寄存器 地址 数据
back_link 0xfff2e8 0x00000000
esp0 0xfff2ec 0x01000000
ss0 0xfff2f0 0x00000010
esp1 0xfff2f4 0x00000000
ss1 0xfff2f8 0x00000000
esp2 0xfff2fc 0x00000000
ss2 0xfff300 0x00000000
cr3 0xfff304 0x00000000
eip 0xfff308 0x0000677c
eflags 0xfff30c 0x00001616
eax 0xfff310 0x00000000
ecx 0xfff314 0x0003e400
edx 0xfff318 0x00000021
ebx 0xfff31c 0x00000003
esp 0xfff320 0x0001f248
ebp 0xfff324 0x0001f248
esi 0xfff328 0xffff0000
edi 0xfff32c 0x00000ffc
es 0xfff330 0x00000017
cs 0xfff334 0x0000000f
ss 0xfff338 0x00000017
ds 0xfff33c 0x00000017
fs 0xfff340 0x00000017
gs 0xfff344 0x00000017
ldt 0xfff348 0x00000038
trace_bitmap 0xfff34c 0x80000000

对比后可知,寄存器的内容都存放在TSS中,同时eip就是跳转后执行的代码段的偏移地址。

结论

所以,Linux 0.11中的任务切换是通过跳转到下一个任务的TSS段的指令,使CPU自动完成切换。切换时CPU会把下一个任务的TSS自动加载到CPU的寄存器中。
同理,CPU也会把切换前的CPU状态保存到切换前任务的TSS中,这样当切换前任务以后再恢复运行时,就会执行ljmp的下一行指令。这样看似程序是顺序执行,实际上CPU的寄存器和栈内容都已经发生了变化。

阅读更多
换一批

没有更多推荐了,返回首页