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

2021/7/27 12:35:52

本文主要是介绍Bochs源码分析 - 17:分析Bochs对于int指令中interrupt类型的实现,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

        在《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,我们慢慢来分析,搞懂其内部实际调用情况即可。



这篇关于Bochs源码分析 - 17:分析Bochs对于int指令中interrupt类型的实现的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程