ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Bochs源码分析 - 17:分析Bochs对于int指令中interrupt类型的实现

2021-07-27 12:30:31  阅读:233  来源: 互联网

标签:17 selector descriptor 源码 Bochs interrupt new BX stack


前言

        在《x86/x64编程体系探索及编程》的第207页,其举了一个使用中断服务例程的例子,我们现在来分析其源码以及探究bochs是如何实现的(重点探究int指令)。

代码分析

        其首先设置好调用set_user_interrupt_handler来调用中断向量,内容如下:

        mov esi, SYSTEM_SERVICE_VECTOR // 0x40
        mov edi, system_service        // lib
        call set_user_interrupt_handler

        set_user_interrupt_handler 地址只有一个 jmp 指令,跳转到 __set_user_interrupt_handler,在该函数中先调用sidt来获取idt表地址,存储到 [___idt__pointer] 所指向的内存中,之后根据esi作为索引找到对应的值,将 system_service 存储进去,这很好理解的。

set_user_interrupt_handler:     jmp     DWORD __set_user_interrupt_handler

;------------------------------------------------------
; set_user_interrupt_handler(int vector, void(*)()handler)
; input:
;       esi: vector,  edi: handler
;------------------------------------------------------
__set_user_interrupt_handler:
        sidt [__idt_pointer]        
        mov eax, [__idt_pointer + 2]
        mov [eax + esi * 8 + 4], edi                           ; set offset [31:16]
        mov [eax + esi * 8], di                                ; set offset [15:0]
        mov DWORD [eax + esi * 8 + 2], kernel_code32_sel       ; set selector
        mov WORD [eax + esi * 8 + 5], 0E0h | INTERRUPT_GATE32  ; Type=interrupt gate, P=1, DPL=3
        ret

        system_service函数中存在一个__system_service函数,在这里直接从系统服务表中获取对应的值,然后直接call进去即可。

system_service:                 jmp     DWORD __system_service

;-------------------------------------------------------
; system_service(): 系统服务例程,使用中断0x40号调用进入 
; input:
;                eax: 系统服务例程号
;--------------------------------------------------------
__system_service:
        mov eax, [system_service_table + eax * 4]
        call eax                                ; 调用系统服务例程
        iret

;******** 系统服务例程函数表 ***************
system_service_table:
        dd __puts                                       ; 0 号
        dd __read_gdt_descriptor                        ; 1 号
        dd __write_gdt_descriptor                       ; 2 号
        dd 0                                            ; 3 号
        dd 0                                            ; 4 号
        dd 0                                            ; 5 号
        dd 0                                            ; 6 号
        dd 0
        dd 0
        dd 0

        之后的__puts函数向video内存中写入对应的值,而不是使用bios来输出,这些关于外设的我们可能之后分析,现在这不是重点。

__write_char:
        push ebx
        mov ebx, video_current
        or si, 0F00h
        cmp si, 0F0Ah                                ; LF
        jnz do_wirte_char
        call __get_current_column

Bochs源码分析

        先用IDA来逆向,找出其调用int指令的地址,如下。

         之后通过bochs-dbg定位到该处,然后在visual studio中设置对应的软件断点。

         如下代码是当遇到int指令时所产生的替换指令,这部分还是很好理解的。注意其type为BX_SOFTWARE_INTERRUPT,含义是软件所触发的中断,我们之后分析interrupt(..)函数时会用到。

void BX_CPP_AttrRegparmN(1) BX_CPU_C::INT_Ib(bxInstruction_c *i)
{

  Bit8u vector = i->Ib();

    ...
    ...


  interrupt(vector, BX_SOFTWARE_INTERRUPT, 0, 0);

  BX_INSTR_FAR_BRANCH(BX_CPU_ID, BX_INSTR_IS_INT,
                      FAR_BRANCH_PREV_CS, FAR_BRANCH_PREV_RIP,
                      BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].selector.value, RIP);

}

        interrupt(..)函数主要完成两件事:发生中断事件的类型,CPU当前所在的模式。现在我们正在32位保护模式下,因此走的是 protected_mode_int(vector, soft_int, push_error, error_code) 这个函数,我们继续往下分析。


void BX_CPU_C::interrupt(Bit8u vector, unsigned type, bx_bool push_error, Bit16u error_code)
{
  ....

  bx_bool soft_int = 0;
  switch(type) {
    ...
    case BX_SOFTWARE_EXCEPTION:
      soft_int = 1;
      break;
    ....
  }
  ...

  if (long_mode()) {
    long_mode_int(vector, soft_int, push_error, error_code);
  }
  else
  {
    // software interrupt can be redirected in v8086 mode
    if (type != BX_SOFTWARE_INTERRUPT || !v8086_mode() ||!v86_redirect_interrupt(vector))
    {
      if(real_mode()) {
        real_mode_int(vector, push_error, error_code); 
      }
      else {
        protected_mode_int(vector, soft_int, push_error, error_code); // <---
      }
    }
  }


  RSP_COMMIT;

    ....

  BX_CPU_THIS_PTR EXT = 0;
}

BX_CPU_C::protected_mode_int(...)函数分析

        该函数内容有点多,不过没关系,我们慢慢来分解。

        还记得我们之前分析的idtr寄存器嚒,其存储着idt表的idt表的基质和限长。该函数上来先从该寄存器中来获取限长来进行对比,判断其是否超出限长。

  // interrupt vector must be within IDT table limits,
  // else #GP(vector*8 + 2 + EXT)
  if ((vector*16 + 15) > BX_CPU_THIS_PTR idtr.limit) {
    BX_ERROR(("interrupt(long mode): vector must be within IDT table limits, IDT.limit = 0x%x", BX_CPU_THIS_PTR idtr.limit));
    exception(BX_GP_EXCEPTION, vector*8 + 2);
  }

        我们现在提一下idt,曾经有一节我们分析过,在实模式下,idtr寄存器中存储着ivt表的地址,而ivt表中直接存储着中断处理函数的地址。但是我们现在是在保护模式,在idt表中存储着中断门描述符,而不是中断处理函数的地址。

         如果还是没有印象,下图是中断门描述符的属性,看到这个很容易理解,其中断处理函数存储在offset,很好查找与定位。

         结合上面这张表,我们重新回顾设置中断门描述符的代码,很好理解。首先offset被设置为 system_service 函数入口,将DPL设置为3,允许用户层代码进入,并且将Segment Selecotor设置为kernel_code32_sel,内核级代码段选择子。

        mov eax, [__idt_pointer + 2]
        mov [eax + esi * 8 + 4], edi                           ; set offset [31:16]
        mov [eax + esi * 8], di                                ; set offset [15:0]
        mov DWORD [eax + esi * 8 + 2], kernel_code32_sel       ; set selector
        mov WORD [eax + esi * 8 + 5], 0E0h | INTERRUPT_GATE32  ; Type=interrupt gate, P=1, DPL=3

        继续来分析protected_mode_int(..)函数,之后代码如下,其从idt表中解析出上述中断门描述符。(先来获取其值,然后调用parse_descriptor(..)函数解析)

  Bit64u desctmp1 = system_read_qword(BX_CPU_THIS_PTR idtr.base + vector*16);
  Bit64u desctmp2 = system_read_qword(BX_CPU_THIS_PTR idtr.base + vector*16 + 8);

    // ...

  Bit32u dword1 = GET32L(desctmp1);
  Bit32u dword2 = GET32H(desctmp1);
  Bit32u dword3 = GET32L(desctmp2);

  parse_descriptor(dword1, dword2, &gate_descriptor);

        之后来判断当前CPL是否满足中断门描述符所要求的权限(dpl),很好理解。

  // if software interrupt, then gate descriptor DPL must be >= CPL,
  // else #GP(vector * 8 + 2 + EXT)
  if (soft_int && gate_descriptor.dpl < CPL) {
    BX_ERROR(("interrupt(): soft_int && (gate.dpl < CPL)"));
    exception(BX_GP_EXCEPTION, vector*8 + 2);
  }

        之后这个很好理解,我们是BX_386_INTERRUPT_GATE,之后所有行为都是在case条件下完成的,当完成之后直接return结束函数运行。

  switch (gate_descriptor.type) {
  case BX_TASK_GATE:
    ....
  case BX_286_INTERRUPT_GATE:
  case BX_286_TRAP_GATE:
  case BX_386_INTERRUPT_GATE:
  case BX_386_TRAP_GATE:
    ..
 }

        之后来解析代码段选择子,这里是内核的代码段,这部分解析的函数我们之前已经分析过了,就不用再来继续分析了。

    parse_selector(gate_dest_selector, &cs_selector);

    // selector must be within its descriptor table limits
    // else #GP(selector+EXT)
    fetch_raw_descriptor(&cs_selector, &dword1, &dword2, BX_GP_EXCEPTION);
    parse_descriptor(dword1, dword2, &cs_descriptor);

        之后来进行常规的代码段权限检查,这些检查内容很好理解。

    // descriptor AR byte must indicate code seg
    // and code segment descriptor DPL<=CPL, else #GP(selector+EXT)
    if (cs_descriptor.valid==0 || cs_descriptor.segment==0 ||
        IS_DATA_SEGMENT(cs_descriptor.type) ||
        cs_descriptor.dpl > CPL)
    {
      BX_ERROR(("interrupt(): not accessible or not code segment cs=0x%04x", cs_selector.value));
      exception(BX_GP_EXCEPTION, cs_selector.value & 0xfffc);
    }

        当检查通过是,其会先来保存原来的ESP、SS、EIP、CS四个值,很好理解。

    Bit32u old_ESP = ESP;
    Bit16u old_SS  = BX_CPU_THIS_PTR sregs[BX_SEG_REG_SS].selector.value;
    Bit32u old_EIP = EIP;
    Bit16u old_CS  = BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].selector.value;

        然后来判断是否是一致代码段,这个我们已经在上篇文章中分析过了。

if(IS_CODE_SEGMENT_NON_CONFORMING(cs_descriptor.type) && cs_descriptor.dpl < CPL)

        这里关键的来了,其从TSS中获取ESP0,SS0。我们以前仅知道Windows只使用TSS结构体来保存SSP0与SS0,其实intel内部本来就使用这两个数据结构,其值就存储在这里面。

 // check selector and descriptor for new stack in current TSS
      get_SS_ESP_from_TSS(cs_descriptor.dpl, &SS_for_cpl_x, &ESP_for_cpl_x);

        绕过对ss数据段的权限检查和解析,下面就是调用函数准备新的栈,代码如下,很好理解。

      // Prepare new stack segment
      bx_segment_reg_t new_stack;
      new_stack.selector = ss_selector;
      new_stack.cache = ss_descriptor;
      new_stack.selector.rpl = cs_descriptor.dpl;
      // add cpl to the selector value
      new_stack.selector.value = (0xfffc & new_stack.selector.value) | new_stack.selector.rpl;

        现在重点来了,开始往栈中压入数据,可以看到其栈的结构。并且可以看到error_code并不一定必须压住栈,如果有就压,如果没有就不压!

        if (gate_descriptor.type>=14) { // 386 int/trap gate
          // push long pointer to old stack onto new stack
          write_new_stack_dword(&new_stack, temp_ESP-4,  cs_descriptor.dpl, old_SS);
          write_new_stack_dword(&new_stack, temp_ESP-8,  cs_descriptor.dpl, old_ESP);
          write_new_stack_dword(&new_stack, temp_ESP-12, cs_descriptor.dpl, read_eflags());
          write_new_stack_dword(&new_stack, temp_ESP-16, cs_descriptor.dpl, old_CS);
          write_new_stack_dword(&new_stack, temp_ESP-20, cs_descriptor.dpl, old_EIP);
          temp_ESP -= 20;

          if (push_error) {
            temp_ESP -= 4;
            write_new_stack_dword(&new_stack, temp_ESP, cs_descriptor.dpl, error_code);
          }

    ESP = temp_ESP;

        之后调用load_cs和load_ss这两个函数加载代码段寄存器的栈段寄存器。这个内容比较简单,直接对寄存器赋值即可,没有想的那么复杂。

      // load new CS:eIP values from gate
      // set CPL to new code segment DPL
      // set RPL of CS to CPL
      load_cs(&cs_selector, &cs_descriptor, cs_descriptor.dpl);

      // load new SS:eSP values from TSS
      load_ss(&ss_selector, &ss_descriptor, cs_descriptor.dpl);


BX_CPU_C::load_cs(bx_selector_t *selector, bx_descriptor_t *descriptor, Bit8u cpl)
{
    ...
  BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].selector = *selector;
  BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].cache = *descriptor;
  BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].selector.rpl = cpl;
  BX_CPU_THIS_PTR sregs[BX_SEG_REG_CS].cache.valid  = SegValidCache;
    ...
}

        最后这部分也非常重要,其EIP是gate描述符中的偏移地址。之后来清除标志位!!这个对我们帮助很大,尤其当我们一直记不清要清除哪些标志位时,看具体代码就很好记忆了。

    EIP = gate_dest_offset;

    // if interrupt gate then set IF to 0
    if (!(gate_descriptor.type & 1)) // even is int-gate
      BX_CPU_THIS_PTR clear_IF();
    BX_CPU_THIS_PTR clear_TF();
    BX_CPU_THIS_PTR clear_NT();
    BX_CPU_THIS_PTR clear_VM();
    BX_CPU_THIS_PTR clear_RF();

总结

        通过bochs代码,我们很好理清了当中断发生时具体的行为。注意,int除了可以触发interrupt类型事件还可以触发trap类型事件,这两种事件中存在着细微差异,我们之后分析到trap时会对比着来进行分析。

        下一篇文章我们将来尝试分析中断返回时使用的iret指令,与之对应的还存在一个retf,我们慢慢来分析,搞懂其内部实际调用情况即可。

标签:17,selector,descriptor,源码,Bochs,interrupt,new,BX,stack
来源: https://blog.csdn.net/WriteAnything_/article/details/119136357

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有