在《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
先用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; }
该函数内容有点多,不过没关系,我们慢慢来分解。
还记得我们之前分析的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,我们慢慢来分析,搞懂其内部实际调用情况即可。