首先先大致感受下转换流程,稍后解释
原来的汇编代码:
#### This function works by assuming that the thread we're switching #### into is also running switch_threads(). Thus, all it has to do is #### preserve(保存) a few registers on the stack, then switch stacks and #### restore the registers. As part of switching stacks we record the #### current stack pointer in CUR's thread structure. .globl switch_threads .func switch_threads switch_threads: # Save caller's register state. # # Note that the SVR4 ABI allows us to destroy %eax, %ecx, %edx, # but requires us to preserve %ebx, %ebp, %esi, %edi. See # [SysV-ABI-386] pages 3-11 and 3-12 for details. # # This stack frame must match the one set up by thread_create() # in size. pushl %ebx pushl %ebp pushl %esi pushl %edi # Get offsetof (struct thread, stack). .globl thread_stack_ofs mov thread_stack_ofs, %edx # Save current stack pointer to old thread's stack, if any. movl SWITCH_CUR(%esp), %eax movl %esp, (%eax,%edx,1) # Restore stack pointer from new thread's stack. movl SWITCH_NEXT(%esp), %ecx movl (%ecx,%edx,1), %esp # Restore caller's register state. popl %edi popl %esi popl %ebp popl %ebx ret .endfunc .globl switch_entry .func switch_entry switch_entry: # Discard switch_threads() arguments. addl $8, %esp # Call thread_schedule_tail(prev). pushl %eax .globl thread_schedule_tail call thread_schedule_tail addl $4, %esp # Start thread proper. ret .endfunc2 进入切换前 在static void
3.进入切换点
4.
保存原线程的寄存器,push入栈
5.
切换线程,本质是保存原有的esp,并将其赋值为下一个线程的esp
6
切换线程后,将它的寄存器值pop出来赋值给四个寄存器
大概就是这几步了,然后先给几个调试的常用命令
b main :在主函数处设置断点
c : continue 一直运行直到遇到断点停下
n : 下一步
s :进入函数内部
si :到下一个汇编代码
display /x $esp :显示esp寄存器的值
layout src :调试时显示c源代码
layout split :调试时显示c源代码和汇编代码
x/10wa 0xc000e000 :显示从 0xc000e000开始的10个单位的内存的值
p $eax :显示$eax的值
pintos栈底是较大的内存地址,栈指针是较小的内存地址
所以push的时候esp是每次减小4的,pop的时候esp是每次加4的
首先,如上图所示,ebp上方ee60和ee5c处的两个值是cur=running_thread() 和 next=next_thread_to_run()函数得到的值,ebp=ee68是函数schedule的栈底的指针,上面已经放好了算好的现在线程的地址和下一将要运行的线程的地址 ,这些是进入switch_thread()函数的准备工作
接着准备进入switch_thread函数内部,接下去都看汇编代码,一开始esp在40处,为什么esp=40会到ebp=68上面这么多,中间空了这么多呢,中间空出来的部分应该是schedule函数自己的一个栈变量的空间吧,现在是switch_thread函数,栈空间应该是从47--2c,其中47--40 8个字节两个int型的空间放了swithc_thread的两个参数,至于怎么找到参数的,还记得上面ee60和ee5c处的两个值吗,就是将这两个值复制过来就好了,看汇编的代码应该能看懂了吧,准备工作,即参数入栈完成,可以进入函数了
四个push应该没有问题,保存当前线程的寄存器的值嘛 这时esp到了2c了,不过有一点要注意,每次调用新函数时,esp会先减少4的,图中是从40跳到3c,然后再进行操作
好了,继续
接下来是四行汇编,切换在这时完成
SWITCH_CUR 被定义为 20,5个单元
SWITCH_NEXT 被定义为 24 ,6个单元
1. movl SWITCH_CUR(%esp), %eax 将 esp+20的值移到eax寄存器中 esp=2c ,eax保存了当前线程的地址
2. movl %esp, (%eax,%edx,1) 将esp的值放到 eax+edx的内存中 即当前线程首地址再偏移edx=24的一个位置是恒定的存放esp的位置,这里可以解释线程被切换后再回到该线程它是怎么知道自己下一步该怎么走了,从这个位置找出esp就可以继续啦
3. movl SWITCH_NEXT(%esp), %ecx 将 esp+24的值移到ecx寄存器中 esp=2c,ecx保存了下一个线程的地址
4. movl (%ecx,%edx,1), %esp 将ecx+edx 的值移到esp中,将下一个线程的esp的值放如寄存器esp中,当当当,线程切换了!!
恩,切换完成了,请看下图,看看是如何完成后续工作的 现在esp=4fec咯,接下去还是只写内存最后两位了,方便一点
4个pop,将栈中寄存器的值pop到对应的四个寄存器中,恩,应该很容易看懂吧,接着遇到一个ret操作 esp会加4 ,还记得上面esp进入函数时 esp减了4吗,这两处是对应的
esp到了e8 了
再把保存原线程地址的eax放到 ec处,esp到ec了,接下去进入thread_schedule_tail函数了,它有个prev参数,不就是刚刚放入的eax嘛,对吧。
进入函数时esp减4 ,esp到e8了
这时会出现一个频繁的操作序列 push ebp ; ebp=esp;这时做什么呢,我们知道,当在一个函数中进入另一个函数内部时,由于每个函数都有独立的栈空间,有就是说,有独立的ebp和esp了,那么你得先将自己的ebp保存好,才能放心地往下走嘛!
接着 esp会突然加到bc,因为要调用running_thread函数了,接下去一样的步骤, 进入时esp-4,保存ebp,恩,esp到b4
然后esp又加到了9c,因为刚进来这个函数,又要到下一个函数pg_round_page了,将这时的esp赋值给了b0位置的内存,b0这个位置的值又给了eax寄存器,eax寄存器里面的值再赋给9c这个地址,绕了一圈,其实就是9c这个地址的值是9c,是pg_round_page的参数,好了,进入pg_round_page,这里对eax和ffff000与操作,得到esp所在页的初始地址,即当前线程的初始地址,接下去该返回了,逐层返回吧,首先pop ebp ,ebp变成running_thread的ebp,b4了,看到保存ebp的作用了吧,可以快速返回上一层函数
每次ret操作时和上面一样 esp要加4
有一个leave操作挺特别的,是 esp=ebp;pop ebp的简写吧,上网找了一下好像是这样的,恩,退了两次 ebp到e4了,esp是栈顶端,bc,不要忘记eax里面的值还是当前线程的初始地址哦,把eax放到ebp上面一个位置,名字是cur,它就是cur=running_thread()的结果了,上面这么多其实就是为了实现这一句话。。。。终于搞定了,现在可以看到,成功切换了线程后,cur也变成了next线程的值了,切换到此结束,分析的差不多了!
搞了一个下午和晚上终于通过调试弄清了线程切换的本质了,还是挺值得!