- 论坛徽章:
- 0
|
我先翻译了必备知识、信息源、中断描述符表(IDT)、syscall的处理四个部分,其余部分将陆续补充过来。
A guide to how the FreeBSD kernel manages the IA32 processors in Protected Mode
(c) 2004, Arne Vidstrom, http://vidstrom.net
Version 1.0 : 2004-06-17
1、必备知识
本篇指南假定读者已经熟悉IA32处理器在保护模式下的工作方式、C程序设计语言,以及AT&T语法的IA32汇编语言程序设计。此外还假定读者已经有一些关于FreeBSD用户模式编程和内核内部工作机制的知识。
2、信息源
在编写这篇指南时所使用的主要信息源就是FreeBSD内核源代码本身。位于http://fxr.watson.org/的FreeBSD内核交叉引用可以很方便地搜索源代码,非常有价值,不过本文中的代码片断都取自一台FreeBSD4.9的/usr/src目录树。
此外,McKusick、Bostic、Karels和Quarterman编写的【The Design and Implementation of the 4.4 BSD Operating System】这本书作为对内核的综合性论述,也非常有用。
IA32信息方面所使用的参考资料是Intel的【IA-32 Intel Architecture Software Developer’s Manual】,以及Tom Shanley的【Protected Mode Software Architecture】这本书。
3、中断描述符表(IDT)
3.1 IDT的定义
在src/sys/i386/i386/machdep.c中:
- static struct gate_descriptor idt0[NIDT];
- struct gate_descriptor *idt = &idt0[0];
复制代码
IDT被定义为大小为NIDT的gate_descriptor结构体数组。常数NIDT是在src/sys/i386/include/segments.h中定义的,表示IDT中的最大中断数目。gate_descriptor结构体则在同一文件中定义如下:
- struct gate_descriptor {
- unsigned gd_looffset:16 ;
- unsigned gd_selector:16 ;
- unsigned gd_stkcpy:5 ;
- unsigned gd_xx:3 ;
- unsigned gd_type:5 ;
- unsigned gd_dpl:2 ;
- unsigned gd_p:1 ;
- unsigned gd_hioffset:16 ;
- } ;
复制代码
gate_descriptor是一个可以用来表示中断门描述符、陷阱门描述符和任务门描述符的通用结构。
3.2 设置IDT中的条目
IDT中的每个条目都是由setidt()函数设置的,这个函数可在src/sys/i386/i386/machdep.c文件中找到:
- void
- setidt(idx, func, typ, dpl, selec)
- int idx;
- inthand_t *func;
- int typ;
- int dpl;
- int selec;
- {
- struct gate_descriptor *ip;
-
- ip = idt + idx;
- ip->gd_looffset = (int)func;
- ip->gd_selector = selec;
- ip->gd_stkcpy = 0;
- ip->gd_xx = 0;
- ip->gd_type = typ;
- ip->gd_dpl = dpl;
- ip->gd_p = 1;
- ip->gd_hioffset = ((int)func)>>16 ;
- }
复制代码
作为一个例子,我们可以来看看INT 0x80的中断处理程序,也就是syscall中断是如何设置的。针对0x80的setidt()调用可在src/sys/i386/i386/machdep.c文件中找到:
- setidt(0x80, &IDTVEC(int0x80_syscall),
- SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
复制代码
第一个参数是中断号,它将被作为IDT中的下标。
为了理解第二个参数,我们需要先看看IDTVEC到底是什么。它也是在同一个文件中定义的:
- #define IDTVEC(name) __CONCAT(X,name)
复制代码
__CONCAT宏可以在src/sys/sys/cdefs.h文件中找到:
- #define __CONCAT1(x,y) x ## y
- #define __CONCAT(x,y) __CONCAT1(x,y)
复制代码
我们可以看到,&IDTVEC(int0x80_syscall)这个参数可以被读出为&Xint0x80_syscall,这也就是中断处理程序的地址。
第三个参数是常数SDT_SYS386TGT,表示这个门是一个陷阱门。
第四个参数是常数SEL_UPL,可在src/sys/i386/include/segments.h文件中找到:
这是该陷阱门的DPL。3这个数值表示INT 0x80只能从ring 3中调用,换句话说,也就是只能从用户模式中调用。
最后,我们来看看第五个参数,GSEL(GCODE_SEL, SEL_KPL)。GSEL这个宏可在src/sys/i386/include/segments.h文件中找到:
- #define GSEL(s,r) (((s)<<3) | r)
复制代码
在同一文件中,我们找到了常数GCODE_SEL的定义:
我们还可以找到常数SEL_KPL的定义:
正如我们所看到的,GSEL(GCODE_SEL, SEL_KPL)可以被读出为:
- (((GCODE_SEL)<<3) | SEL_KPL)
复制代码
这是中断处理程序驻留位置的段选择符。选择符的3-15比特包含的是描述符表的下标。所以在这个例子中下标为1。在本文后面关于GDT的部分中将会看到,这就是内核代码段描述符的下标。继续,SEL_KPL这个表示内核模式(ring 0)的常数被加到选择符中,作为RPL。RPL表示创建该选择符的代码的特权级别,在这个例子中就是内核。最后,表指示符为0,意味着GDT将被使用。
3.3 使用KernView来查看IDT的内部
通过使用来自http://vidstrom.net/otools/kernview/的KernView工具,我们可以在一个正在运行的FreeBSD系统上查看内核所使用的各种各样的内存结构。下面就是从输出信息中摘录的片断:
- INT 80h:
- - Trap Gate Descriptor
- - DPL = 3
- - Segment Selector = 8h
- - Offset = c038f3c0h
复制代码
0x80这个条目是一个DPL=3的陷阱门,这和我们之前在内核代码中看到的情况是一致的。我们同时注意到段选择符是8。这同样是和我们所看到的内核代码是一致的,因为这个数值就是(((GCODE_SEL)<<3) | SEL_KPL),其中GCODE_SEL=1,而SEL_KPL=0。
3.4 激活IDT
在src/sys/i386/i386/machdep.c中:
- struct region_descriptor r_gdt, r_idt;
- . . .
- r_idt.rd_limit = sizeof(idt0) - 1;
- r_idt.rd_base = (int) idt;
- lidt(&r_idt);
复制代码
在src/sys/i386/include/segments.h中:
- struct region_descriptor {
- unsigned rd_limit:16;
- unsigned rd_base:32 __attribute__ ((packed));
- };
复制代码
r_idt变量被赋予了IDT的上限,按照规范就是尺寸减去1。它还被赋予了IDT的地址。r_idt变量的地址被作为一个入参传给了lidt()函数,这个函数由三行汇编代码构成。
在src/sys/i386/i386/support.s中:
- ENTRY(lidt)
- movl 4(%esp),%eax
- lidt (%eax)
- ret
复制代码
宏ENTRY可以在src/sys/i386/include/asm.h中找到:
- #define ENTRY(x) _ENTRY(x)
复制代码
在同一文件中我们还可以找到:
- #define _START_ENTRY .text; .p2align 2,0x90
- #define _ENTRY(x) _START_ENTRY; \
- .globl CNAME(x); .type CNAME(x),@function; CNAME(x):
复制代码
这就意味着ENTRY(lidt)可以被读出为:
- .text; .p2align 2,0x90; \
- .globl CNAME(lidt); .type CNAME(lidt),@function;
- CNAME(lidt):
复制代码
.p2align 2,0x90;告诉汇编程序这些指令应当按32比特边界对齐,而可能需要的填充值则是0x90,也即NOP指令。
.globl CNAME(lidt);使lidt成为一个全局可见的符号。
.type CNAME(lidt),@function;使该符号成为一个函数类型的符号。
现在我们可以来看看上面给出的汇编代码了。
这一行取出位于ESP+4的32位字,也就是r_idt变量的地址,把它存入EAX中。之所以需要+4,是为了跳过保存下来的EIP。最后,这个值将被加载到IDTR中。我们的新的IDT现在就已经激活了,函数也就返回了。
4. syscall的处理
4.1 INT 0x80中断处理程序
在src/sys/i386/i386/exception.s中:
- SUPERALIGN_TEXT
- IDTVEC(int0x80_syscall)
- subl $8,%esp
- pushal
- pushl %ds
- pushl %es
- pushl %fs
- mov $KDSEL,%ax
- mov %ax,%ds
- mov %ax,%es
- MOVL_KPSEL_EAX
- mov %ax,%fs
- movl $2,TF_ERR(%esp)
- FAKE_MCOUNT(13*4(%esp))
- MPLOCKED incl _cnt+V_SYSCALL
- call _syscall2
- MEXITCOUNT
- cli
- cmpl $0,_astpending
- je doreti_syscall_ret
- #ifdef SMP
- MP_LOCK
- #endif
- pushl $0
- subl $4,%esp
- movb $1,_intr_nesting_level
- jmp _doreti
复制代码
我们可以在src/sys/i386/include/asmacros.h中找到SUPERALIGN_TEXT:
- #define SUPERALIGN_TEXT .p2align 4,0x90
复制代码
它会告诉汇编程序把这些代码按16字节边界对齐,填充值为0x90,也即NOP指令。
接下来,代码会设置陷阱栈帧。我们可以在src/sys/i386/include/frame.h文件中找到这个栈帧的格式:
- struct trapframe {
- int tf_fs;
- int tf_es;
- int tf_ds;
- int tf_edi;
- int tf_esi;
- int tf_ebp;
- int tf_isp;
- int tf_ebx;
- int tf_edx;
- int tf_ecx;
- int tf_eax;
- int tf_trapno;
- /* below portion defined in 386 hardware */
- int tf_err;
- int tf_eip;
- int tf_cs;
- int tf_eflags;
- /* below only when crossing rings (e.g. user to kernel) */
- int tf_esp;
- int tf_ss;
- };
复制代码
当处理器执行INT 0x80指令的时候,它在进入中断处理程序之前首先会在堆栈上保存一些状态信息。因为我们现在需要完成从ring 3到ring 0的切换,所以处理器会自动地将栈改变为内核栈。它会把SS、ESP、EFlags、CS和EIP压到栈上。由于这个陷阱并没有与之关联的错误码,处理器就不会把它压到栈上。此外,我们也没有一个需要压到栈上的陷阱号。于是,我们将栈指针减去8字节:
接下来,我们需要把EAX、ECX、EDX、EBX、EAX压栈之前的ESP(也即所谓的ISP,Initial SP)、EBP、ESI以及EDI压到栈上。所有的这些都可以通过一条指令来完成:
最后再把DS、ES和FS压到栈上:
- pushl %ds
- pushl %es
- pushl %fs
复制代码
至此就已经完成了对陷阱栈帧的设置,我们接下来将把DS和ES指向内核数据段选择符:
- mov $KDSEL,%ax
- mov %ax,%ds
- mov %ax,%es
复制代码
MOVL_KPSEL_EAX那一行专门用于SMP(对称多处理器)内核,因此本指南予以忽略。
下面我们把FS也指向内核数据段描述符:
下面这行把2这个数值放到陷阱栈帧中的tf_err字段中。
FAKE_MCOUNT(13*4(%esp))这一行用于内核剖析,不在本指南范围之内。
MPLOCKED incl _cnt+V_SYSCALL这一行专门用于SMP,我们同样予以忽略。
最后,我们调用_syscall2函数:
这个函数实际上调用的是syscall2,这可以通过查看src/sys/i386/include/asnames.h文件中的#define得知:
- #define _syscall2 syscall2
复制代码
4.2 syscall分发
syscall2()函数太长了,不好全部都放在这里,因此我们只会看看其中最值得注意的地方。除特别指出外,这里给出的都是未经修改的原始代码。被略掉的行将使用三个圆点来表示。
- void
- syscall2(frame)
- struct trapframe frame;
- {
- caddr_t params;
- int i;
- struct sysent *callp;
- struct proc *p = curproc;
- register_t orig_tf_eflags;
- u_quad_t sticks;
- int error;
- int narg;
- int args[8];
- int have_mplock = 0;
- u_int code;
- . . .
- params = (caddr_t)frame.tf_esp + sizeof(int);
- code = frame.tf_eax;
- . . .
- callp = &p->p_sysent->sv_table[code];
- narg = callp->sy_narg & SYF_ARGMASK;
- /* Error handling has been cut away from the two lines below */
- i = narg * sizeof(int);
- copyin(params, (caddr_t)args, (u_int)i);
- . . .
- p->p_retval[0] = 0;
- . . .
- error = (*callp->sy_call)(p, args);
- switch (error) {
- case 0:
- . . .
- frame.tf_eax = p->p_retval[0];
- . . .
- break;
- . . .
- default:
- bad:
- . . .
- frame.tf_eax = error;
- . . .
- break;
- }
- . . .
- }
复制代码
首先,我们会找回在发起INT 0x80之前的ESP的值。这个值可以在陷阱栈帧中找到。因为处理器在ESP压栈之前首先将SS的值压到栈上了,我们就需要加上32比特来取得这个参数(PUSHL会将SS按32比特数值压栈)。
- params = (caddr_t)frame.tf_esp + sizeof(int);
复制代码
syscall号是在调用INT 0x80之前放到EAX中的:
syscall表是如何组织的不在本文范围之内,但下面的代码是把syscall函数的地址赋给callp而把syscall接收的参数个数赋给narg。
- callp = &p->p_sysent->sv_table[code];
- narg = callp->sy_narg & SYF_ARGMASK;
复制代码
接下来,会把这些参数从用户空间拷贝到内核空间中:
- copyin(params, (caddr_t)args, (u_int)i);
复制代码
copyin()函数是一个众所熟知的内核库函数,我们将在下一节中对它进行详细分析。
我们将迅速跳过接下来的大部分代码。最值得注意是下面这行:
- error = (*callp->sy_call)(p, args);
复制代码
这一行以进程指针和相关参数为入参调用处理这个syscall的函数。为了看看一个syscall函数到底是什么样子的,我们以open syscall为例:
- static int patched_open(struct proc *p, struct open_args *uap);
复制代码
最后,我们需要注意,syscall的返回值、错误码都是通过EAX留给调用者的。
[ 本帖最后由 雨丝风片 于 2006-4-8 08:36 编辑 ] |
|