Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

前言

        所谓“保护模式”,重点是“保护”,可保护的是什么呢?答案是:内存中的数据与代码。有几种保护手段呢?段保护与页保护。这篇文章我尝试结合intel手册来重新梳理一下intel保护模式中的段保护有关机制。

保护模式的两种保护机制

        The memory management facilities of the IA-32 architecture are divided into two parts: segmentation and paging. Segmentation provides a mechanism of isolating individual code, data, and stack modules so that multiple programs (or tasks) can run on the same processor without interfering with one another. Paging provides a mechanism for implementing a conventional demand-paged, virtual-memory system where sections of a program’s execution environment are mapped into physical memory as needed. Paging can also be used to provide isolation between multiple tasks. When operating in protected mode, some form of segmentation must be used. There is no mode bit to disable segmentation. The use of paging, however, is optional.

         上面是intel卷三 3.1节 关于“保护模式内存管理”的摘要,注意黑体和下划线部分,intel明确指出其内存管理被分成两部分,段式管理(segmentation)和页式管理(paging)。另外,段式管理是必须强制选用的,而页式管理是可选的。

        如下图,是intel说明的保护模式内存管理机制。其首先通过段保护确定具体的线性内存地址,然后通过页保护来确定对应的物理地址。其实在有关资料中很多人都注重Intel的分页机制而忽视分段机制,我猜想很可能是因为各个段的Base都为零,不容易觉察到段保护的存在,段更重要的是其权限检查机制。

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

 段描述符

        一个段由其段描述符所描述,其不同的段类型存在不同的段描述符,但其可以认为由三部分组成:段权限(Access),段机制(Base),段线程(Limit)。

        Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

        当然,因为其历史遗留问题,其实际的段描述符数据结构往往相当难看,是为了兼容有关结构体,如下图。 Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

         段描述符分为类,系统段描述符和代码-数据段描述符(其由S位),其中系统段描述符分为分为六类,存储在LDT或GDT表中。值得注意的是,要区分段描述符与门描述符,门描述存储在IDT表中,门描述符中存储着段选择子,通过段选择子可以从GDT/LDT表中找到段描述符。      Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

        

段寄存器

        CPU通过段寄存器来管理段描述符,比如我们如果调用汇编指令 "mov [0x1100],eax",其隐含着 "ds:[0x1100]",该ds就是段寄存器。

        段寄存器由两部分组成:可见部分(段选择子)+不可见部分(段描述符)。可以想一下,如果每次内存访问都需要从GDT表中查找对应的段描述符,这速度得多慢啊!!不要忘记,段描述符也在内存中哦,CPU访问自己的寄存器的速度显然比去访问内存的速度快多了!!!

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

        下面是段选择子的结构,这部分是可见部分,我们可以直接看到的。我们在实体机上没有办法看到不可见部分,唯一的办法就是通过段选择子的偏移从GDT表中查找。

 Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

加载代码段寄存器的四种方法

        现在来看《x86/x64体系探索及编程》的10.5.5.4节 - 代码段寄存器的加载。书中邓志老师是这样描述的:

代码段寄存器的加载非常复杂,这是保护模式下最为复杂的一个环节,不但涉及控制权的转移,也涉及权限的检查,以及stack的切换,某些情况下还涉及任务切换。

        文章所提及的大体内容如下,其中关于"一致代码段"和"非一致代码段"的描述之前还是不太确定,当时做过实验,可是很久了都忘记了...

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

EX10-1代码解读

         ex10-1就是尝试通过调用tss selector来加载段寄存器(上图加载方式的第三种),我们简单说一下汇编代码,然后主要精力放在分析bochs源码上。   

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

        其核心流程如下,这里值得区分far pointer的情况,当call 一个数字时,则表示使用tss段来加载cs寄存器。下面代码很简单,我们就不详述tss结构了,可以查阅手册。简单来说,获取tss_base,设置好对应值(重点tss_task_handler);然后将tss段描述符的权限修改为3(允许用户权调用);然后调用retf来返回用户代码段,之后调用call 0x50来使用tss描述符进行任务切换。

// tss段描述符在gdt表索引0x50处.
tss_sel                         equ     0x50

// 初始化tss段内容
        mov esi, tss_sel
        call get_tss_base
        mov DWORD [eax + 32], tss_task_handler          ; ���� EIP ֵΪ tss_task_handler
        mov DWORD [eax + 36], 0                         ; eflags = 0
        mov DWORD [eax + 56], KERNEL_ESP                ; esp
        mov WORD [eax + 76], KERNEL_CS                  ; cs
        mov WORD [eax + 80], KERNEL_SS                  ; ss
        mov WORD [eax + 84], KERNEL_SS                  ; ds
        mov WORD [eax + 72], KERNEL_SS                  ; es

// 修改tss的dpl权限为3,可以从三环调用过去
        mov esi, tss_sel
        call read_gdt_descriptor
        or edx, 0x6000                                  ; TSS desciptor DPL = 3
        mov esi, tss_sel
        call write_gdt_descriptor

// 使用retf指令返回三环
        push DWORD user_data32_sel | 0x3
        push esp
        push DWORD user_code32_sel | 0x3        
        push DWORD user_entry
        retf

// 直接调用tss描述符
    call tss_sel:0 --> call 0x50


tss_task_handler:
        jmp do_tss_task
tmsg1        db '---> now, switch to new Task,', 0        
tmsg2        db 'CPL:', 0
do_tss_task:
        mov esi, tmsg1
        call puts

        ....
        ....

        clts
        iret   

bochs对于call far机制的实现

        我们现在来分析bochs代码是如何处理 call tss_sel的,首先用IDA定位该代码。

        这里存在一个坑!!IDA对这部分反汇编分析不完整,定位不到,这里尝试了各种方法(反汇编,字符串搜索,硬编码搜索...)找了快一个小时才找到。说下思路,首先,定位retf指令,其跳转到用户代码时连续4个push,之后retf,特征比较明显。

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

        之后对该地址进行反汇编,可以看到这个 call 0x50:0h这个,此时ida还显示标红。然后我们将其硬编码写到x64windbg中看看反汇编情况,并且不支持反汇编。

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

         定位到0x9119这个地址,之后老方法,定位到该指令的实现代码。

        其首先进入 BX_CPU_C::CALL32_Ap(..) 这个函数,其代码实现如下,值已经在注释中给出,可以看到其调用call_far32(..)这个函数。

void BX_CPP_AttrRegparmN(1) BX_CPU_C::CALL32_Ap(bxInstruction_c *i)
{
  BX_ASSERT(BX_CPU_THIS_PTR cpu_mode != BX_MODE_LONG_64);

  Bit16u cs_raw = i->Iw2();   // 0x50
  Bit32u disp32 = i->Id();    // 0x00

  call_far32(i, cs_raw, disp32);

  BX_NEXT_TRACE(i);
}

         在call_far32(...)这个函数中,其先判断是否是保护模式,如果是则调用call_protected这个函数,我们跟进这个函数继续来进行分析。

void BX_CPU_C::call_far32(bxInstruction_c *i, Bit16u cs_raw, Bit32u disp32)
{
   
  ...

  if (protected_mode()) {
    call_protected(i, cs_raw, disp32);
  }
  else {
    ....
  }

  ....

  BX_INSTR_FAR_BRANCH(BX_CPU_ID, BX_INSTR_IS_CALL,
                      FAR_BRANCH_PREV_CS, FAR_BRANCH_PREV_RIP,
                      BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].selector.value, EIP);
}

         段选择子为50,其索引为 0x50 >> 3 = 0xA,这个使用计算器快速算出。Bochs中一个selector数据结构来描述这个值,如下:

selector	0x0019f3c4 {value=0x0050 index=0x000a ti=0x00 '\0' ...}	const bx_selector_t *
		value	0x0050	unsigned short
		index	0x000a	unsigned short
		ti	0x00 '\0'	unsigned char
		rpl	0x00 '\0'	unsigned char

call_protected(..)函数部分分析

        其call_protected(..)代码关于这章主题的核心部分如下,其中删减了很多与这篇文章无关的知识。其中对于段选择子的解析最重要的是上面三个函数:parse_selector-拆解段选择子,获取上面的那个数据结构;fetch_raw_descriptor 从gdt/ldt表中获取原生的数据结构(存储在dword1,dowrd2中);parse_descriptor(...)来判断这部分函数。

        之后其通过cs_descriptor.segment判断是否是正常的代码段描述符,如果是直接调用barch_far(...),否则其会通过 gate_descirptor.type 来判断门描述符的类型,这里很明显判断出属于TSS任务门描述符。

BX_CPU_C::call_protected(bxInstruction_c *i, Bit16u cs_raw, bx_address disp)
{
   
    ...

  parse_selector(cs_raw, &cs_selector);
  fetch_raw_descriptor(&cs_selector, &dword1, &dword2, BX_GP_EXCEPTION);
  parse_descriptor(dword1, dword2, &cs_descriptor);


  if (cs_descriptor.segment)   // normal segment
  {
    ....  
      // load code segment descriptor into CS cache
      // load CS with new code segment selector
      // set RPL of CS to CPL
      branch_far(&cs_selector, &cs_descriptor, disp, CPL);
      RSP = temp_rsp;
  }else{
    ....

    switch (gate_descriptor.type) {
      case BX_SYS_SEGMENT_AVAIL_286_TSS:
      case BX_SYS_SEGMENT_AVAIL_386_TSS:
        // SWITCH_TASKS _without_ nesting to TSS
        task_switch(i, &gate_selector, &gate_descriptor,
          BX_TASK_FROM_CALL, dword1, dword2);  <--- 
        return;

       ....
    }
    ...
  }
}

parse_selector(cs_raw, &cs_selector)函数分析

        该函数内容如下,其代码很短,但可以说实现的相当漂亮,使用 > 以及 '&' 运算。说实话之前搞Windows内核也写过类似程序来解析selector,但写的看起来贼难看,也不好记,要学习这种写法。

void parse_selector(Bit16u raw_selector, bx_selector_t *selector)
{
  selector->value = raw_selector;
  selector->index = raw_selector >> 3;
  selector->ti    = (raw_selector >> 2) & 0x01;
  selector->rpl   = raw_selector & 0x03;
}

fetch_raw_descriptor(...)函数分析

        这部分其实一点也不复杂,其通过offset = BX_CPU_THIS_PTR gdtr.base + index*8 来计算出其段描述符所在地址offset;然后调用system_read_qword(offset) 该函数来从内存地址中读取,这个函数我们之后在分页中还会重点分析,现在就不多说了。

        另外可以看到其GET32L与GET32H这两个宏,这种写法是非常值得我们学习的,以后在编写代码时要多多借鉴。

void BX_CPU_C::fetch_raw_descriptor(const bx_selector_t *selector,
                        Bit32u *dword1, Bit32u *dword2, unsigned exception_no)
{
  Bit32u index = selector->index;
  bx_address offset;
  Bit64u raw_descriptor;

  if (selector->ti == 0) { /* GDT */
    if ((index*8 + 7) > BX_CPU_THIS_PTR gdtr.limit) {
      BX_ERROR(("fetch_raw_descriptor: GDT: index (%x) %x > limit (%x)",
         index*8 + 7, index, BX_CPU_THIS_PTR gdtr.limit));
      exception(exception_no, selector->value & 0xfffc);
    }
    offset = BX_CPU_THIS_PTR gdtr.base + index*8;
  }
  else { /* LDT */
      // .... 
  }

  raw_descriptor = system_read_qword(offset);

  *dword1 = GET32L(raw_descriptor);
  *dword2 = GET32H(raw_descriptor);
}

#define GET32L(val64) ((Bit32u)(((Bit64u)(val64)) & 0xFFFFFFFF))
#define GET32H(val64) ((Bit32u)(((Bit64u)(val64)) >> 32))

parse_descriptor(...)函数解析

       这个函数就真正解析描述符了,自己之前也写过类似的,但写的很难看,杂乱无章。一定要来学习这种写法,之后写的话可以参考这份代码。

void parse_descriptor(Bit32u dword1, Bit32u dword2, bx_descriptor_t *temp)
{

  if (temp->segment) { /* data/code segment descriptors */
        ...
        ...
        ...
  }
  else { // system & gate segment descriptors
    switch (temp->type) {
      case BX_286_CALL_GATE:
      case BX_286_INTERRUPT_GATE:
      case BX_286_TRAP_GATE:
         ...
        break;

      case BX_386_CALL_GATE:
      case BX_386_INTERRUPT_GATE:
      case BX_386_TRAP_GATE:
        // param count only used for call gate
        ...
        break;

      case BX_TASK_GATE:
        temp->u.taskgate.tss_selector = dword1 >> 16;
        temp->valid = 1;
        break;

      case BX_SYS_SEGMENT_LDT:
      case BX_SYS_SEGMENT_AVAIL_286_TSS:
      case BX_SYS_SEGMENT_BUSY_286_TSS:
      case BX_SYS_SEGMENT_AVAIL_386_TSS:
      case BX_SYS_SEGMENT_BUSY_386_TSS:
        ...
        break;

      default: // reserved
        temp->valid    = 0;
        break;
    }
  }
}

  task_switch(...)函数分析

        bochs中这部分代码多大800多行,就不详细分析这部分了,感兴趣可以自行解读。其核心代码如下图,其从tss_base地址中读出新的。赋值,然后调用task_switch_load_selector(..)加载段选择子,调用完之后就可以返回

  newEIP    = system_read_dword(Bit32u(nbase32 + 0x20));
  ....

    
  BX_CPU_THIS_PTR prev_rip = EIP = newEIP;

  EAX = newEAX;
  ECX = newECX;
  EDX = newEDX;
  ....

  task_switch_load_selector(&BX_CPU_THIS_PTR sregs[BX_SEG_REG_DS],
      &ds_selector, raw_ds_selector, cs_selector.rpl);

 总结

        TSS切换在真正的操作系统中并不常用,我记得在Windows中只使用TSS来保存ESP0, EBP0,当进入内核时用来切换进内核栈,这不常用。但是我们在分析这个过程中知道了bochs如何解析加载存储段寄存器的(上面讲的三个函数)以及对于call_far指令大体的解析流程。

Bochs源码分析 - 15:bochs对于call far(tss_sel)指令的实现机制

         上面是我们这次分析的大体流程,这并不复杂。其中标星的函数之后我们还要重点来分析的函数。下一步我们尝试来分析 retf 与 iret指令的区别,之后继续来阅读《x86/x64体系探索及编程》。

        

上一篇:半导体放电管选型,厂家东沃,免费支持


下一篇:Python 第三方模块之 imgaug (图像增强)