nachos3.4线程的栈结构和相关汇编解析

我在前面一篇nachos入门的介绍中提到了nachos的线程切换是和汇编相关的,而且也涉及到其线程的栈结构,所以由于篇幅,之前就没有详细说明,这两天把多级队列反馈算法实现之后,又仔细了研究了下。

nachos版本:3.4

工具:Win7下 SourceInsight or  Linux下KScope

 

nachos中线程的切换是在Scheduler这个类的Run函数中实现的:

void Scheduler::Run (Thread *nextThread) { Thread *oldThread = currentThread; oldThread->CheckOverflow(); // check if the old thread // had an undetected stack overflow currentThread = nextThread; // switch to the next thread currentThread->setStatus(RUNNING); // nextThread is now running DEBUG('t', "Switching from thread /"%s/" to thread /"%s/"/n", oldThread->getName(), nextThread->getName()); // This is a machine-dependent assembly language routine defined // in switch.s. You may have to think // a bit to figure out what happens after this, both from the point // of view of the thread and from the perspective of the "outside world". SWITCH(oldThread, nextThread); 

其中关键的一个函数就是SWITCH函数,在这个函数中具体实现了切换的行为。注释告诉我们,这个函数的实现在switch.s中,然后是又机器相关的,所以我们去switch.s中查看的时候,一定要找对应自己机器的,多半都是i386吧,下面就是386中的汇编代码:

SWITCH: movl %eax,_eax_save # save the value of eax movl 4(%esp),%eax # move pointer to t1 into eax movl %ebx,_EBX(%eax) # save registers movl %ecx,_ECX(%eax) movl %edx,_EDX(%eax) movl %esi,_ESI(%eax) movl %edi,_EDI(%eax) movl %ebp,_EBP(%eax) movl %esp,_ESP(%eax) # save stack pointer movl _eax_save,%ebx # get the saved value of eax movl %ebx,_EAX(%eax) # store it movl 0(%esp),%ebx # get return address from stack into ebx movl %ebx,_PC(%eax) # save it into the pc storage movl 8(%esp),%eax # move pointer to t2 into eax movl _EAX(%eax),%ebx # get new value for eax into ebx movl %ebx,_eax_save # save it movl _EBX(%eax),%ebx # retore old registers movl _ECX(%eax),%ecx movl _EDX(%eax),%edx movl _ESI(%eax),%esi movl _EDI(%eax),%edi movl _EBP(%eax),%ebp movl _ESP(%eax),%esp # restore stack pointer movl _PC(%eax),%eax # restore return address into eax movl %eax,4(%esp) # copy over the ret address on the stack movl _eax_save,%eax ret 

这个汇编代码的思路非常清晰,前面半部分是保存t1线程的各种相关寄存器信息,其中这里nachos的机制是将所有数据都保存到t1线程指针的内存地址对应偏移的位置,比如ebx寄存器的值保存在_EBX 加上eax寄存器地址的位置,其中eax里面已经被t1(4%esp)指向的内存地址赋值,下面的各种寄存器的值也是一样的道理。

从8(%esp),%eax  开始,就开始将t2线程的寄存器的值一个个写到当前的寄存器当中,最后一点是将t2的PC值写到栈顶,然后利用ret命令跳转到PC指向的地址,从而开始执行t2线程里面的下一条指令(用PC表示),这是汇编指令中常见的一个策略。而且在接下来的线程栈结构的分析,我们可以看到这里加4并不会覆盖其他有有用的信息。

nachos的线程的栈是在StackAllocate函数中分配的:

void Thread::StackAllocate (VoidFunctionPtr func, int arg) { stack = (int *) AllocBoundedArray(StackSize * sizeof(int)); #ifdef HOST_SNAKE // HP stack works from low addresses to high addresses stackTop = stack + 16; // HP requires 64-byte frame marker stack[StackSize - 1] = STACK_FENCEPOST; #else // i386 & MIPS & SPARC stack works from high addresses to low addresses #ifdef HOST_SPARC // SPARC stack must contains at least 1 activation record to start with. stackTop = stack + StackSize - 96; #else // HOST_MIPS || HOST_i386 stackTop = stack + StackSize - 4; // -4 to be on the safe side! #ifdef HOST_i386 // the 80386 passes the return address on the stack. In order for // SWITCH() to go to ThreadRoot when we switch to this thread, the // return addres used in SWITCH() must be the starting address of // ThreadRoot. *(--stackTop) = (int)ThreadRoot; #endif #endif // HOST_SPARC *stack = STACK_FENCEPOST; #endif // HOST_SNAKE machineState[PCState] = (int) ThreadRoot; machineState[StartupPCState] = (int) InterruptEnable; machineState[InitialPCState] = (int) func; machineState[InitialArgState] = arg; machineState[WhenDonePCState] = (int) ThreadFinish; }  

这里还是机器相关的,我主要讲一下i386的代码。AllocBoundedArray函数这里就不贴出来了,有兴趣的同学自己可以看下,就是new一块内存地址出来,然后这块地址赋给stack指针,表示栈的栈底。stackTop 在stack指针的基础上加上栈的大小然后减去4,这样就保证了栈的有效地址是StackSize大小。下面紧接着是栈顶自减,然后赋值为ThreadRoot,这里就把上面提到的4个字节的空间给省出来了,而ThreadRoot也是所有线程Fork之后最开始执行的地址。下面的一系列machineState的赋值可以看到把线程本身相关的一些重要函数都存在了模拟的寄存器中,这里的InterruptEnable,和ThreadFinish比较简单,这是简单的开中断和调用线程自己的Finish函数,func是我们要函数执行的主体。我重点分析下ThreadRoot这个函数,这个函数也是汇编实现的:

/* void ThreadRoot( void ) ** ** expects the following registers to be initialized: ** eax points to startup function (interrupt enable) ** edx contains inital argument to thread function ** esi points to thread function ** edi point to Thread::Finish() */ ThreadRoot: pushl %ebp movl %esp,%ebp pushl InitialArg call *StartupPC call *InitialPC call *WhenDonePC # NOT REACHED movl %ebp,%esp popl %ebp ret  

从这里我们就可以看出nachos一个线程生来病死的全部过程了,这里call的都是那些常量的地址存放的作为指针的函数,StartupPC就是InterruptEnable函数存放的地址,InitialPCState存放的是func,也就是线程函数的主体,执行完这个之后就可以finish了,这就是控制了线程全部生命周期的函数。

你可能感兴趣的:(thread,linux,function,汇编,assembly,HP)