从进程到内核---ring3到ring0

这次我们用中断来实现从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;


 



你可能感兴趣的:(从进程到内核---ring3到ring0)