免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 8092 | 回复: 14

[FreeBSD] 推荐一篇关于FreeBSD内核如何在保护模式下管理IA32处理器的文章 [复制链接]

论坛徽章:
0
发表于 2006-04-08 08:28 |显示全部楼层
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


Table of contents
1. PREREQUISITE KNOWLEDGE....................................................................................2
2. INFORMATION SOURCES ..........................................................................................2
3. THE INTERRUPT DESCRIPTOR TABLE (IDT) ............................................................2
3.1 The IDT definition............................................................................................2
3.2 Setting entries in the IDT .................................................................................3
3.3 A look inside the IDT with KernView ..............................................................4
3.4 Making the IDT active .....................................................................................5
4. SYSCALL HANDLING ...............................................................................................6
4.1 The INT 0x80 interrupt handler.......................................................................6
4.2 Syscall dispatching...........................................................................................8
4.3 The copyin() function .....................................................................................10
5. THE GLOBAL DESCRIPTOR TABLE (GDT).............................................................12
5.1 The GDT definition ........................................................................................12
5.2 Setting up the descriptors in the GDT............................................................13
5.3 Making the GDT active..................................................................................16
5.4 A look inside the GDT with KernView...........................................................17
5.5 Segment selector values in an ordinary user mode program ........................21
6. TASK SWITCHING ..................................................................................................22
6.1 The cpu_switch() function..............................................................................22
7. VIRTUAL PAGING ..................................................................................................29
7.1 The page fault handler ...................................................................................29
7.2 Virtual paging and task switching .................................................................32
8 THE LOCAL DESCRIPTOR TABLE (LDT).................................................................32
8.1 A quick glance at the LDT .............................................................................32
9. MISCELLANEOUS ..................................................................................................34
9.1 The uiomove() function ..................................................................................34

原文链接:

http://vidstrom.net/papers/freebsdprotmode.pdf

我将在本贴中陆续给出中译版本。

[ 本帖最后由 雨丝风片 于 2006-4-8 08:37 编辑 ]

论坛徽章:
0
发表于 2006-04-08 08:34 |显示全部楼层
我先翻译了必备知识、信息源、中断描述符表(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中:

  1. static struct gate_descriptor idt0[NIDT];
  2. struct gate_descriptor *idt = &idt0[0];
复制代码


IDT被定义为大小为NIDT的gate_descriptor结构体数组。常数NIDT是在src/sys/i386/include/segments.h中定义的,表示IDT中的最大中断数目。gate_descriptor结构体则在同一文件中定义如下:

  1. struct gate_descriptor {
  2.         unsigned gd_looffset:16 ;
  3.         unsigned gd_selector:16 ;
  4.         unsigned gd_stkcpy:5 ;
  5.         unsigned gd_xx:3 ;
  6.         unsigned gd_type:5 ;
  7.         unsigned gd_dpl:2 ;
  8.         unsigned gd_p:1 ;
  9.         unsigned gd_hioffset:16 ;
  10. } ;
复制代码


gate_descriptor是一个可以用来表示中断门描述符、陷阱门描述符和任务门描述符的通用结构。

3.2 设置IDT中的条目

IDT中的每个条目都是由setidt()函数设置的,这个函数可在src/sys/i386/i386/machdep.c文件中找到:

  1. void
  2. setidt(idx, func, typ, dpl, selec)
  3.         int idx;
  4.         inthand_t *func;
  5.         int typ;
  6.         int dpl;
  7.         int selec;
  8. {
  9.         struct gate_descriptor *ip;
  10.    
  11.         ip = idt + idx;
  12.         ip->gd_looffset = (int)func;
  13.         ip->gd_selector = selec;
  14.         ip->gd_stkcpy = 0;
  15.         ip->gd_xx = 0;
  16.         ip->gd_type = typ;
  17.         ip->gd_dpl = dpl;
  18.         ip->gd_p = 1;
  19.         ip->gd_hioffset = ((int)func)>>16 ;
  20. }
复制代码


作为一个例子,我们可以来看看INT 0x80的中断处理程序,也就是syscall中断是如何设置的。针对0x80的setidt()调用可在src/sys/i386/i386/machdep.c文件中找到:


  1.         setidt(0x80, &IDTVEC(int0x80_syscall),
  2.                     SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
复制代码


第一个参数是中断号,它将被作为IDT中的下标。

为了理解第二个参数,我们需要先看看IDTVEC到底是什么。它也是在同一个文件中定义的:

  1. #define IDTVEC(name)    __CONCAT(X,name)
复制代码


__CONCAT宏可以在src/sys/sys/cdefs.h文件中找到:

  1. #define __CONCAT1(x,y) x ## y
  2. #define __CONCAT(x,y) __CONCAT1(x,y)
复制代码


我们可以看到,&IDTVEC(int0x80_syscall)这个参数可以被读出为&Xint0x80_syscall,这也就是中断处理程序的地址。

第三个参数是常数SDT_SYS386TGT,表示这个门是一个陷阱门。

第四个参数是常数SEL_UPL,可在src/sys/i386/include/segments.h文件中找到:

  1. #define SEL_UPL 3
复制代码


这是该陷阱门的DPL。3这个数值表示INT 0x80只能从ring 3中调用,换句话说,也就是只能从用户模式中调用。

最后,我们来看看第五个参数,GSEL(GCODE_SEL, SEL_KPL)。GSEL这个宏可在src/sys/i386/include/segments.h文件中找到:

  1. #define GSEL(s,r) (((s)<<3) | r)
复制代码


在同一文件中,我们找到了常数GCODE_SEL的定义:

  1. #define GCODE_SEL 1
复制代码


我们还可以找到常数SEL_KPL的定义:

  1. #define SEL_KPL 0
复制代码


正如我们所看到的,GSEL(GCODE_SEL, SEL_KPL)可以被读出为:

  1. (((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系统上查看内核所使用的各种各样的内存结构。下面就是从输出信息中摘录的片断:

  1. INT 80h:
  2.      - Trap Gate Descriptor
  3.      - DPL = 3
  4.      - Segment Selector = 8h
  5.      - 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中:

  1. struct region_descriptor r_gdt, r_idt;
  2. . . .
  3.         r_idt.rd_limit = sizeof(idt0) - 1;
  4.         r_idt.rd_base = (int) idt;
  5.         lidt(&r_idt);
复制代码


在src/sys/i386/include/segments.h中:

  1. struct region_descriptor {
  2.         unsigned rd_limit:16;
  3.         unsigned rd_base:32 __attribute__ ((packed));
  4. };
复制代码


r_idt变量被赋予了IDT的上限,按照规范就是尺寸减去1。它还被赋予了IDT的地址。r_idt变量的地址被作为一个入参传给了lidt()函数,这个函数由三行汇编代码构成。

在src/sys/i386/i386/support.s中:

  1. ENTRY(lidt)
  2.         movl 4(%esp),%eax
  3.         lidt (%eax)
  4.         ret
复制代码


宏ENTRY可以在src/sys/i386/include/asm.h中找到:

  1. #define ENTRY(x) _ENTRY(x)
复制代码


在同一文件中我们还可以找到:

  1. #define _START_ENTRY .text; .p2align 2,0x90
  2. #define _ENTRY(x) _START_ENTRY; \
  3.             .globl CNAME(x); .type CNAME(x),@function; CNAME(x):
复制代码


这就意味着ENTRY(lidt)可以被读出为:

  1. .text; .p2align 2,0x90; \
  2.             .globl CNAME(lidt); .type CNAME(lidt),@function;
  3.             CNAME(lidt):
复制代码


.p2align 2,0x90;告诉汇编程序这些指令应当按32比特边界对齐,而可能需要的填充值则是0x90,也即NOP指令。

.globl CNAME(lidt);使lidt成为一个全局可见的符号。

.type CNAME(lidt),@function;使该符号成为一个函数类型的符号。

现在我们可以来看看上面给出的汇编代码了。

  1. movl 4(%esp),%eax
复制代码


这一行取出位于ESP+4的32位字,也就是r_idt变量的地址,把它存入EAX中。之所以需要+4,是为了跳过保存下来的EIP。最后,这个值将被加载到IDTR中。我们的新的IDT现在就已经激活了,函数也就返回了。

  1. lidt (%eax)
  2. ret
复制代码


4. syscall的处理

4.1 INT 0x80中断处理程序

在src/sys/i386/i386/exception.s中:

  1.         SUPERALIGN_TEXT
  2. IDTVEC(int0x80_syscall)
  3.         subl $8,%esp
  4.         pushal
  5.         pushl %ds
  6.         pushl %es
  7.         pushl %fs
  8.         mov $KDSEL,%ax
  9.         mov %ax,%ds
  10.         mov %ax,%es
  11.         MOVL_KPSEL_EAX
  12.         mov %ax,%fs
  13.         movl $2,TF_ERR(%esp)
  14.         FAKE_MCOUNT(13*4(%esp))
  15.         MPLOCKED incl _cnt+V_SYSCALL
  16.         call _syscall2
  17.         MEXITCOUNT
  18.         cli
  19.         cmpl $0,_astpending
  20.         je doreti_syscall_ret
  21. #ifdef SMP
  22.         MP_LOCK
  23. #endif
  24.         pushl $0
  25.         subl $4,%esp
  26.         movb $1,_intr_nesting_level
  27.         jmp _doreti
复制代码


我们可以在src/sys/i386/include/asmacros.h中找到SUPERALIGN_TEXT:

  1. #define SUPERALIGN_TEXT .p2align 4,0x90
复制代码


它会告诉汇编程序把这些代码按16字节边界对齐,填充值为0x90,也即NOP指令。

接下来,代码会设置陷阱栈帧。我们可以在src/sys/i386/include/frame.h文件中找到这个栈帧的格式:

  1. struct trapframe {
  2.         int tf_fs;
  3.         int tf_es;
  4.         int tf_ds;
  5.         int tf_edi;
  6.         int tf_esi;
  7.         int tf_ebp;
  8.         int tf_isp;
  9.         int tf_ebx;
  10.         int tf_edx;
  11.         int tf_ecx;
  12.         int tf_eax;
  13.         int tf_trapno;
  14.         /* below portion defined in 386 hardware */
  15.         int tf_err;
  16.         int tf_eip;
  17.         int tf_cs;
  18.         int tf_eflags;
  19.         /* below only when crossing rings (e.g. user to kernel) */
  20.         int tf_esp;
  21.         int tf_ss;
  22. };
复制代码




当处理器执行INT 0x80指令的时候,它在进入中断处理程序之前首先会在堆栈上保存一些状态信息。因为我们现在需要完成从ring 3到ring 0的切换,所以处理器会自动地将栈改变为内核栈。它会把SS、ESP、EFlags、CS和EIP压到栈上。由于这个陷阱并没有与之关联的错误码,处理器就不会把它压到栈上。此外,我们也没有一个需要压到栈上的陷阱号。于是,我们将栈指针减去8字节:

  1. subl $8,%esp
复制代码


接下来,我们需要把EAX、ECX、EDX、EBX、EAX压栈之前的ESP(也即所谓的ISP,Initial SP)、EBP、ESI以及EDI压到栈上。所有的这些都可以通过一条指令来完成:

  1. pushal
复制代码


最后再把DS、ES和FS压到栈上:

  1. pushl %ds
  2. pushl %es
  3. pushl %fs
复制代码


至此就已经完成了对陷阱栈帧的设置,我们接下来将把DS和ES指向内核数据段选择符:

  1. mov $KDSEL,%ax
  2. mov %ax,%ds
  3. mov %ax,%es
复制代码


MOVL_KPSEL_EAX那一行专门用于SMP(对称多处理器)内核,因此本指南予以忽略。

下面我们把FS也指向内核数据段描述符:

  1. mov %ax,%fs
复制代码


下面这行把2这个数值放到陷阱栈帧中的tf_err字段中。

  1. movl $2,TF_ERR(%esp)
复制代码


FAKE_MCOUNT(13*4(%esp))这一行用于内核剖析,不在本指南范围之内。

MPLOCKED incl _cnt+V_SYSCALL这一行专门用于SMP,我们同样予以忽略。

最后,我们调用_syscall2函数:

  1. call _syscall2
复制代码


这个函数实际上调用的是syscall2,这可以通过查看src/sys/i386/include/asnames.h文件中的#define得知:

  1. #define _syscall2 syscall2
复制代码


4.2 syscall分发

syscall2()函数太长了,不好全部都放在这里,因此我们只会看看其中最值得注意的地方。除特别指出外,这里给出的都是未经修改的原始代码。被略掉的行将使用三个圆点来表示。

  1. void
  2. syscall2(frame)
  3. struct trapframe frame;
  4. {
  5.         caddr_t params;
  6.         int i;
  7.         struct sysent *callp;
  8.         struct proc *p = curproc;
  9.         register_t orig_tf_eflags;
  10.         u_quad_t sticks;
  11.         int error;
  12.         int narg;
  13.         int args[8];
  14.         int have_mplock = 0;
  15.         u_int code;
  16. . . .
  17.         params = (caddr_t)frame.tf_esp + sizeof(int);
  18.         code = frame.tf_eax;
  19. . . .
  20.         callp = &p->p_sysent->sv_table[code];
  21.         narg = callp->sy_narg & SYF_ARGMASK;
  22.         /* Error handling has been cut away from the two lines below */
  23.         i = narg * sizeof(int);
  24.         copyin(params, (caddr_t)args, (u_int)i);
  25. . . .
  26.         p->p_retval[0] = 0;
  27. . . .
  28.         error = (*callp->sy_call)(p, args);
  29.         switch (error) {
  30.         case 0:
  31. . . .
  32.                 frame.tf_eax = p->p_retval[0];
  33. . . .
  34.                 break;
  35. . . .
  36.         default:
  37. bad:
  38. . . .
  39.                 frame.tf_eax = error;
  40. . . .
  41.                 break;
  42.         }
  43. . . .
  44. }
复制代码


首先,我们会找回在发起INT 0x80之前的ESP的值。这个值可以在陷阱栈帧中找到。因为处理器在ESP压栈之前首先将SS的值压到栈上了,我们就需要加上32比特来取得这个参数(PUSHL会将SS按32比特数值压栈)。

  1. params = (caddr_t)frame.tf_esp + sizeof(int);
复制代码


syscall号是在调用INT 0x80之前放到EAX中的:

  1. code = frame.tf_eax;
复制代码


syscall表是如何组织的不在本文范围之内,但下面的代码是把syscall函数的地址赋给callp而把syscall接收的参数个数赋给narg。

  1. callp = &p->p_sysent->sv_table[code];
  2. narg = callp->sy_narg & SYF_ARGMASK;
复制代码


接下来,会把这些参数从用户空间拷贝到内核空间中:

  1. copyin(params, (caddr_t)args, (u_int)i);
复制代码


copyin()函数是一个众所熟知的内核库函数,我们将在下一节中对它进行详细分析。

我们将迅速跳过接下来的大部分代码。最值得注意是下面这行:

  1. error = (*callp->sy_call)(p, args);
复制代码


这一行以进程指针和相关参数为入参调用处理这个syscall的函数。为了看看一个syscall函数到底是什么样子的,我们以open syscall为例:

  1. static int patched_open(struct proc *p, struct open_args *uap);
复制代码


最后,我们需要注意,syscall的返回值、错误码都是通过EAX留给调用者的。

[ 本帖最后由 雨丝风片 于 2006-4-8 08:36 编辑 ]

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
发表于 2006-04-08 08:50 |显示全部楼层
老大这么早?
这个文章以前看过,对于task switch部分我感觉讲的不够清晰。

论坛徽章:
0
发表于 2006-04-08 08:57 |显示全部楼层
原帖由 gvim 于 2006-4-8 08:50 发表
老大这么早?
这个文章以前看过,对于task switch部分我感觉讲的不够清晰。


俺才刚发现这篇文章,还没看到task switch呢。

就我目前所看到的中断和系统调用部分来讲,这篇文章对于读者的“阅读基础”的假定确实不容忽视,对于基本概念一概不讲,上来就是代码分析。不过我觉得如果看过【Linux内核源代码情景分析】之后,再来看这篇文章,就很容易理解FreeBSD的实现了。只是不知task switch部分是否也是如此。

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
发表于 2006-04-08 09:00 |显示全部楼层
这部分,我到觉得大部分是对intel system programmer guide 的一个应用。

论坛徽章:
0
发表于 2006-04-08 11:53 |显示全部楼层
up,最好翻译完后作成PDF。

论坛徽章:
0
发表于 2006-04-09 00:07 |显示全部楼层
在arch handbook上面说“All hand per-process page tables can be constructed on the fly and are usually considered throwaway",我的理解就是在使用时创建,被认为是可以被丢弃的。上面还讲到是根据vm_map_t,vm_entry_t和vm_object_t创建的,不知道是如何创建的,能否详细讲一下?

论坛徽章:
0
发表于 2006-04-10 19:50 |显示全部楼层
原帖由 ocean390 于 2006-4-9 00:07 发表
在arch handbook上面说“All hand per-process page tables can be constructed on the fly and are usually considered throwaway",我的理解就是在使用时创建,被认为是可以被丢弃的。上面还讲到是根据vm_m ...



“All hard per-process page tables can be reconstructed on the fly and are usually considered throwaway”的意思应该是说FreeBSD的“硬件页表”不是一次性构造好的,而是随时都在动态变化的,当然一般来说也就是可被丢弃的。

关于这一点可以参考【Design Elements of the FreeBSD VM System】,其中有一段的内容如下:
Linux uses 'permanent' page tables that are not throw away, but does not need a pv_entry for each potentially mapped pte. FreeBSD uses 'throw away' page tables but adds in a pv_entry structure for each actually-mapped pte. I think memory utilization winds up being about the same, giving FreeBSD an algorithmic advantage with its ability to throw away page tables at will with very low overhead.


【FreeBSD操作系统设计与实现】中也有以下一段话:
Physical-to-virtual address mappings are not created at the time that the object is mapped; instead, their creation is delayed until the first reference to a particular page is made.



“Page tables are directly synthesized from the vm_map_t/vm_entry_t/vm_object_t hierarchy”,我觉得这里的“vm_entry_t”应该是“vm_map_entry”。我还没有看过和页表相关的代码,对于这个问题暂时还无法给出准确、详细的回答,回去继续研究一下。

论坛徽章:
0
发表于 2006-04-10 20:30 |显示全部楼层
我看了内核的代码,没有找到关于进程的页目录的分配,每个进程的页目录应该是不同的吧,因为在进程切换的时候改变了页目录的基址。好像在3G内核空间下面预留了4M的空间,不知道是做什么用的。在locore.s中设置了PTmap,PTD等,这些值就是设置为在4M的空间内,在pmap中有extern pt_entry_t PTmap[];extern pt_entry_t PTD[];在源文件中好像并没有定义。我猜测有可能页表就应该分配在这个4M内,也就是用户空间内,不知道对不对?这是不清楚页目录的地址,如果也在这些地址内的话,进行进程切换的时候并不能保证页目录在内存中。

论坛徽章:
0
发表于 2006-04-11 19:05 |显示全部楼层
原帖由 ocean390 于 2006-4-10 20:30 发表
我看了内核的代码,没有找到关于进程的页目录的分配,每个进程的页目录应该是不同的吧,因为在进程切换的时候改变了页目录的基址。好像在3G内核空间下面预留了4M的空间,不知道是做什么用的。在locore.s中设置了P ...


我现在琢磨“Page tables are directly synthesized from the vm_map_t/vm_entry_t/ vm_object_t hierarchy”这句话的意思应该是说一个页表条目的创建首先是因为进程访问了一个尚未映射的页面,从而产生了一个page fault。从而在进程的地址空间中,按照vm_map、vm_map_entry、vm_object的顺序找到这个未映射页所属的对象。为其分配物理页,并将对象中的未映射页的内容拷入物理页中,然后在建立到这个物理页的页表项。

一个进程的页表目录和页表究竟放在什么地方我现在也不知道。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP