浅析trapframe与context的原理与区别

TRAPFRAME与CONTEXT的区别

在ucore操作系统中,trapframecontext是很容易混淆的两个结构,他们都会出现在线程的调度中。实际上,结构体trapframe用于切换优先级、页表目录等,而context则是用于轻量级的上下文切换。从技术上来看,两者的区别在于context仅仅能够切换普通寄存器,而trapframe可以切换包括普通寄存器、段寄存器以及少量的控制寄存器。

*在看后续内容之前,你需要提前了解汇编语言、栈、C函数调用的实现。

TRAPFRAME结构体

trapframe定义如下——

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

这个结构体中,依次储存了——

  • 目标寄存器
  • gs, fs, es, ds 段寄存器
  • trap_no, err 用于储存中断信息
  • eip, cs, eflags 用于储存陷阱(trap)返回后的目的地址
  • esp, ss 在权限发生变化时,用于指示新的栈的位置

有两个地方使用了trapframe,一个是中断调用,另一个是进程切换。两者对于trapframe的使用有相似之处,但也并不完全相同。

中断调用中使用TRAPFRAME

trapframe在中断中,在前期负责中断信息的储存,后期负责中断的恢复。同时,trapframe结构体是位于栈中的,其生成和使用都是通过栈的pushpop命令实现的,这将在后面详细解释。

中断发生时,下面代码,将一系列信息压到栈中。这些信息在后续的,trap(struct trapframe *tf)函数中,被对齐到了tf结构体中。

.globl __alltraps                                                                                                                  
__alltraps:                                                                                                                        
    # push registers to build a trap frame                                                                                         
    # therefore make the stack look like a struct trapframe                                                                        
    pushl %ds                                                                                                                      
    pushl %es                                                                                                                      
    pushl %fs                                                                                                                      
    pushl %gs                                                                                                                      
    pushal                                                                                                                         
                                                                                                                                   
    # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl GD_KDATA, %eax     movw %ax, %ds     movw %ax, %es      # push %esp to pass a pointer to the trapframe as an argument to trap()     pushl %esp      # call trap(tf), where tf=%esp     call trap</code></pre> <!-- /wp:code -->  <!-- wp:paragraph --> 中断处理完成后,需要恢复原来的运行状态,此时,按顺序将之前push的所有信息pop出来即可。 <!-- /wp:paragraph -->  <!-- wp:code --> <pre class="wp-block-code"><code>    # call trap(tf), where tf=%esp     call trap      # pop the pushed stack pointer     popl %esp      # return falls through to trapret... .globl __trapret __trapret:     # restore registers from stack     popal      # restore %ds, %es, %fs and %gs     popl %gs     popl %fs     popl %es     popl %ds      # get rid of the trap number and error code     addl0x8, %esp
    iret

当然,倘若读者认为trapframe仅仅像这样中规中矩的实现信息的保存,那就太小看他了。我们发现,在调用call trap之后,有一句popl %esp,而后续恢复的信息完全是基于该%esp进行定位的,那么在中断处理内存中,如果我们强行修改%esp成为我们希望接下来运行的代码段的trap描述,那么经过__trapret代码恢复trapframe后,你就可以让程序跳转到任何你希望的地方。

比如下面代码就实现了内核态到用户态的切换。

    if (tf->tf_cs == KERNEL_CS) {                                                                                              
        cprintf("T_SWITH_TOU\n");                                                                                              
        userframe =  *tf;                                                                                                      
        userframe.tf_cs = USER_CS;                                                                                             
        userframe.tf_ds = USER_DS;                                                                                             
        userframe.tf_es = USER_DS;                                                                                             
        userframe.tf_ss = USER_DS;                                                                                             
        //userframe.tf_fs = USER_DS;                                                                                           
        userframe.tf_eflags |= FL_IOPL_MASK;                                                                                   
        userframe.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;                                                        
        *((uint32_t *)tf - 1) = (uint32_t)&userframe;                                                                     
    } 

其中*((uint32_t *)tf - 1)这个位置的值就是之后popl %esp恢复的%esp的值。

进程切换中CONTEXT的作用

context的结构体定义如下,可以看到,其中储存了所有的用户寄存器的值。

// Saved registers for kernel context switches.                                                                                    
// Don't need to save all the %fs etc. segment registers,                                                                          
// because they are constant across kernel contexts.                                                                               
// Save all the regular registers so we don't need to care                                                                         
// which are caller save, but not the return register %eax.                                                                        
// (Not saving %eax just simplifies the switching code.)                                                                           
// The layout of context must match code in switch.S.                                                                              
struct context {                                                                                                                   
    uint32_t eip;                                                                                                                  
    uint32_t esp;                                                                                                                  
    uint32_t ebx;                                                                                                                  
    uint32_t ecx;                                                                                                                  
    uint32_t edx;                                                                                                                  
    uint32_t esi;                                                                                                                  
    uint32_t edi;                                                                                                                  
    uint32_t ebp;                                                                                                                  
};                     

context结构体干的事情也很简单,可以用switch_to函数囊括,即保存一系列寄存器,并恢复一系列寄存器。在C++中,switch_to是拥有两个context结构体为参数的函数switch_to(&(prev->context), &(next->context)); ,其实现如下——

switch_to:                      # switch_to(from, to)

    # save from's registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    movl %esp, 4(%eax)          # save esp::context of from
    movl %ebx, 8(%eax)          # save ebx::context of from
    movl %ecx, 12(%eax)         # save ecx::context of from
    movl %edx, 16(%eax)         # save edx::context of from
    movl %esi, 20(%eax)         # save esi::context of from
    movl %edi, 24(%eax)         # save edi::context of from
    movl %ebp, 28(%eax)         # save ebp::context of from

    # restore to's registers
    movl 4(%esp), %eax          # not 8(%esp): popped return address already
                                # eax now points to to
    movl 28(%eax), %ebp         # restore ebp::context of to
    movl 24(%eax), %edi         # restore edi::context of to
    movl 20(%eax), %esi         # restore esi::context of to
    movl 16(%eax), %edx         # restore edx::context of to
    movl 12(%eax), %ecx         # restore ecx::context of to
    movl 8(%eax), %ebx          # restore ebx::context of to
    movl 4(%eax), %esp          # restore esp::context of to

    pushl 0(%eax)               # push eip

    ret

进程切换中TRAPFRAME的作用

那是不是进程的切换就可以直接用switch_to函数呢?答案是否定的,因为switch_to仅仅保存、恢复了普通寄存器,无法实现优先级跳转、段寄存器修改等等。这时,就要借助trapframe了。

由于switch_to函数跳转后,将调到context.eip位置。而这个跳转我们没法完全实现进程切换,所以我们可以将其设置为一个触发二级跳转的函数,forkret

proc->context.eip = (uintptr_t)forkret;
proc->context.esp = (uintptr_t)(proc->tf);

其中,forkret定义如下(current是当前进程,也就是进程切换的目标进程),forkret不同于switch_to,它尝试使用trapframe作为进程切换的手段,而相比于contexttrapframe的功能就强大多了。

static void
forkret(void) {
    forkrets(current->tf);
}

而forkrets定义如下——

.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

.globl forkrets
forkrets:
    # set stack to this new process's trapframe
    movl 4(%esp), %esp
    jmp __trapret

现在,又回到了中断恢复的那一段代码,而其中的逻辑也完全相同。最终,进程跳转目标进程的入口,而该入口的地址,被存放在proc->tf中。下面是kernel_threadtrapframe初始化代码,也能佐证最终调用函数入口fn被储存在了eip中。

// kernel_thread - create a kernel thread using "fn" function                                                                      
 // NOTE: the contents of temp trapframe tf will be copied to                                                                       
 //       proc->tf in do_fork-->copy_thread function                                                                                
 int                                                                                                                                
 kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {                                                                
     struct trapframe tf;                                                                                                           
     memset(&tf, 0, sizeof(struct trapframe));                                                                                      
     tf.tf_cs = KERNEL_CS;                                                                                                          
     tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;                                                                                    
     tf.tf_regs.reg_ebx = (uint32_t)fn;                                                                                             
     tf.tf_regs.reg_edx = (uint32_t)arg;                                                                                            
     tf.tf_eip = (uint32_t)kernel_thread_entry;                                                                                     
     return do_fork(clone_flags | CLONE_VM, 0, &tf);                                                                                
 }                                                                                                                                  

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据