调试分析 Linux 0.00 多任务切换
First
当执行完 system_interrupt 函数,执行 153 行 iret 时,记录栈的变化情况。
Q1:栈的变化情况如下
执行的地址位置是:0x0166,可以从header.s中找到对应的system_interrupt的地址位置。然后将断点设置在0x0166的位置。可以看到对应的函数iret位置,设置断点在0x017c位置。执行之前栈的数值为:
执行了iret之后返回到地址CS:EIP = 0xf:0x10eb的位置,此时的栈的状态是:
此时栈的状态已经从内核栈切换到了用户栈(栈顶指针为0xb08)
Second
当进入和退出 system_interrupt
时,都发生了模式切换,请总结模式切换时,特权级是如何改变的?栈切换吗?如何进行切换的?
Q1:模式切换的时候,特权级别是怎么改变的?
可以观察段选择符的变化来判断特权级别的改变:在进入的时候,段选择子的结构是:
描述符索引 | TI | RPL |
---|---|---|
0000000000001 | 0 | 00 |
在执行了iret指令之后,CS段选择子更改为了0x17,也就是:
描述符索引 | TI | RPL |
---|---|---|
0000000000010 | 1 | 11 |
表示在进行模式切换的时候,执行的是RPL,TI位的转换,是从GDT表项中转向LDT的表项中进行查找。这样就能够完成从内核态转变成用户态的过程。
Q2: 栈切换的方式?
可以观察在返回前和返回之后的SS的情况,根据上一个问题,再去观察SS的值,可以发现同样是从0x08更改到0x17,这个过程中出现了从内核栈到用户栈的转变。
Q3:是如何进行切换的?
首先,在调用系统中断程序的时候,需要先将当前的特权级别从用户态转化为内核态,将CS和SS特权级别从0x17更改为0x8,然后将中断返回地址(CS:EIP),eflags,用户栈的栈顶地址压入内核栈中,作为即将返回的程序所需上下文;之后执行完system_interrupt程序之后执行iret语句的时候,改变CS和SS从0x08变为0x17,然后将中断返回地址和用户栈的栈顶地址以及eflags弹出,恢复程序执行的地址,还原到用户态。
Third
当时钟中断发生,进入到timer_interrupt程序,请详细记录从任务0切换到任务1的过程。
从任务0切换到任务1的过程
检查最开始位置的timer_interrupt处理程序地址位于0x012c位置(因为push位置无法进行中断,所以定位到move语句进行分析)
使用多次断点进行调试可以看到已经可以不断地打印A和B了,现在重新进入任务0来进行分析。
现在将要执行指令:
jmpf 0x0030:00000000
此时观察GDT表可以看到:
现在这条处理器执行了jump语句,对应的是将一个TSS选择子装入CS,也就是把这个任务切换到这个TSS。可以从GDT表中看出,这条指令的选择子为0x30,对应的就是一个可选择的TSS段的位置。这时候可以看出任务切换之前的TSS如下所示:
再执行一句之后的TSS变换为:
此时可以看出跳转指令已经切换到task1了,由于此时的任务在之前已经执行过,所以切入的是对应的time_interrupt程序的位置;而如果是第一次执行,应该是执行的对应的程序的入口位置,也就是task1的标签位置。查看现在的TSS如上图。这个时候回到的是上一个task1指令跳转之后的下一个地址的位置。查看存储在tss1中的tss的数据进行比对:
这说明在任务切换的时候会将对应的TSS放入当前的寄存器中。
然后继续单步执行,执行到iret语句的时候返回到task1的主程序继续执行,这个时候是从中断程序跳转到对应的task1继续执行,查看在iret前后的寄存器信息和栈信息,可以看出是从内核栈转换到对应的任务的用户栈
返回用户栈的之后继续执行之前被中断的task1程序,返回到loop继续执行,然后一直执行对应的循环,直到下一次时钟中断的发生。
Fourth
又过了 10ms
,从任务1切换回到任务 0
整个流程是怎样的?TSS
是如何变化的?各个寄存器的值是如何变化的?
任务一继续执行,执行到下一次时钟中断的位置就发生从任务1转换到任务0的阶段,进入中断处理程序只需要进入下一个断点,此时可以看到进入的位置的状态如下:
当程序执行到jumpf的位置的时候,即将开始下一个任务的执行,当前的TSS状态为:
可以看出当前的TSS的值为当前的值,执行jumpf
语句之后,从GDT表中可以看出跳转到对应的任务,继续执行任务切换:
执行之后可以看到当前的寄存器和TSS都发生了对应的变化,第二次任务切换之后,由于第一次任务切换的时候现场保存到了TSS0里,因此将TSS0切换回来后,CS:EIP
会指向第一次任务切换的下一条地址,也就是0x08:0x0150
。
此时进入了任务1的中断程序的位置,继续执行到iret
指令,此时的寄存器和栈里面的信息为:
执行iret
之后观察寄存器的值中的CS:EIP
发生了切换的操作,可以看到之前的内核栈中存入了对应的eflags
,CS
,SS
,ESP
,EIP
都进行了返回的操作,进入到了用户模式下继续执行,此时的CS从0x8变化到0xf也就是从内核模式切换到了用户模式。任务1将继续执行。
请详细总结任务切换的过程。
任务切换的流程如下:
-
首先通过压入需要的
EFLAGS
/CS
/EIP
/SS
/ESP
入栈并通过返回操作进入用户模式使程序进入任务0开始执行。 -
当任务0执行到
loop
指令的时候产生时钟中断,任务0将当前的EFLAGS
/CS
/EIP
/SS
/ESP
压入任务0的内核栈中作为返回的位置,然后进入时钟中断处理程序,在程序内会检查current
获得当前执行的任务的序号,检查到当前任务序号为0的时候就将current
变量修改为1,并通过ljmp
(jmpf
)指令跳转到任务1开始执行,在执行的时候会将当前的寄存器状态存入TSS0中,并将TSS1中的寄存器状态读入当前的寄存器,在最开始的时候都会初始化为0。 -
初始状态下进入任务1之后,是从头开始执行,当开始执行到
loop
指令的时候发生时钟中断,此时又返回中断处理程序,此时和任务0的执行方式相似,将current
变量修改为0之后再跳转回任务0继续执行。 -
和之前的跳转不同的是这次跳转之后会在上一次执行的语句之后继续执行,任务0此时仍旧在执行中断处理程序,所以在
jumpf
语句之后继续执行并执行中断返回,内核栈的前5项进行弹栈操作,恢复到用户模式继续执行任务0的loop
语句,然后等到下一次loop
语句执行的时候再次进入中断处理程序,同样通过先修改current
变量的方式进行跳转操作,跳转到任务1继续执行。 -
任务1此时重新加载TSS1,在之前的
jumpf
语句之后继续顺序执行,通过iret
弹栈返回用户模式继续执行任务1的loop
语句,直到下一次的loop
语句产生时钟中断回到中断处理程序,再将current
变量修改为0,再次出现跳转操作,回到步骤4。然后语句将在步骤4和步骤5之间循环执行。