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