这次我们用中断来实现从ring3到ring0的跳转,当我们用中断门实现从ring3到ring0的转移时,会从TSS加载ring0的堆栈STACKR0,然后将调用者ring3的ss、esp压入新堆栈STACKR0,然后EFLAGS压入堆栈,然后将当前ring3的cs和ip入栈,然后加载调用门中的新的cs和ip,使IF=0,开始执行中断服务函数。
那么首先我们要准备TSS,其结构如下:
typedef struct s_tss { u32 backlink; u32 esp0; /* stack pointer to use during interrupt */ u32 ss0; /* " segment " " " " */ u32 esp1; u32 ss1; u32 esp2; u32 ss2; u32 cr3; u32 eip; u32 flags; u32 eax; u32 ecx; u32 edx; u32 ebx; u32 esp; u32 ebp; u32 esi; u32 edi; u32 es; u32 cs; u32 ss; u32 ds; u32 fs; u32 gs; u32 ldt; u16 trap; u16 iobase; /* I/O位图基址大于或等于TSS段界限,就表示没有I/O许可位图 */ }TSS; TSS tss;
设置tss,初始化TSS段和ring0的堆栈段,然后用ltr()加载tss
tss.backlink = 0; tss.esp0 = 0xffff; tss.ss0 = 0x40; //TSS段,选择子0x38 _set_gdt_desc(&gdt[7],(u32)&tss,(base+sizeof(tss)),DA_386TSS); //DA_386TSS = 0x89 //ring0堆栈段,选择子0x40 _set_gdt_desc(&gdt[8],0,0xffffffff,DA_DRWA + DA_32); ltr();
ltr()的实现如下,其中0x38就是TSS段的选择子。
#define ltr() \ __asm__ ("movw $0x38,%%ax\n\t" \ "ltr %%ax\n\t" \ :::"ax")
中断服务函数如下:
#define leave() __asm__ ("leave\n\t" :::) #define iret() __asm__ ("iret\n\t" :::) #define DA_386TSS 0x89 void interrupt(void) { disp_str("B"); #if 1 leave(); outb(0x20,0x20); iret(); #else sti(); outb(0x20,0x20); move_to_user_mode(); #endif }
在中断服务函数中我们只打印一个‘B’,然后就跳回进程。我们可以仿照前面ring0到ring3的例子,就如9到10行那样,可是前面已将讲过进程的cs、ip等均已入栈,其实用iret直接返回更合适,就是5到7行那样,不过多了一个leave(),它的作用是什么?没有它不行吗?
leave指令相当于mov esp, ebp和pop ebp两条指令,一般配合使用的还有一个enter等价于push ebp和mov ebp, esp两条指令。至于为什么要用leave指令,我们还有好好分析一下testB函数,为了方便分析我们写一个test.c,其代码如下
#define iret() \ __asm__ ("movb $0x20,%%al\n\t" \ "out %%al,$0x20\n\t" \ "iret\n\t" \ :::) void disp_str(char *info); void testB(void) { disp_str("B"); iret(); }
然后gcc -S test.c -o test.s,
test.s:
.file "test.c" .section .rodata .LC0: .string "B" .text .globl testB .type testB, @function testB: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $24, %esp movl $.LC0, (%esp) call disp_str #APP # 10 "test.c" 1 movb $0x20,%al out %al,$0x20 iret # 0 "" 2 #NO_APP leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size testB, .-testB .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
看到了吗?在iret后面有一个leave和一个ret,c函数就是这样返回的,而在函数的一开始
pushl %ebp
movl %esp, %ebp
leave就是这两句的反操作,就是恢复esp和ebp寄存器,然后ret就返回了。可是在这之前就iret了,这时堆栈已经不是刚一进函数时的堆栈了所以会出错,我们手工加一个leave就万事大吉了。
看起来以后这种代码还是用汇编好一点。
下面是关于c语言栈帧的一些解释
C语言中,函数调用是通过栈帧(stack frame)来实现的。栈帧可以被用来传递参数、保存返回信息、存储局部变量等等。栈帧的一般结构如下图:
栈帧结构的两端由两个寄存器来指定。寄存器ebp指向当前的栈帧的底部(高地址),通常叫做帧指针;寄存器esp指向当前的栈帧的顶部(低址地),通常叫做栈指针。在函数执行过程中,栈指针esp会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp进行。
栈帧是由高地址向低地址延伸的,例如:
入栈操作:push $eax
等价于:esp = exp - 4; $eax -> [esp]
出栈操作:pop $eax
等价于:[esp] -> $eax; esp = esp - 4;