内存分段机制的一个主要应用在于实现操作系统的多任务,它为应用程序提供了两个关键抽象:一个独立的逻辑控制流,一个私有的地址空间。本文将针对进程的创建和调度进行分析和实验,从而更深刻的理解分段机制。有关调试环境的建立见前文:从linux0.11引导代码小窥内存分段机制
进程调度初始化(sched_init函数)
在引导代码执行结束后,执行序列将跳转到main函数,执行一系列的初始化工作,其中就有对任务0的初始化过程,其代码包含在kernel/sched.c中的sched_init函数中:
void sched_init(void) { int i; struct desc_struct * p; if (sizeof(struct sigaction) != 16) panic("Struct sigaction MUST be 16 bytes"); /*建立第0号任务的TSS,LDT描述符表项 */ set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); p = gdt+2+FIRST_TSS_ENTRY; for(i=1;i<NR_TASKS;i++) { task[i] = NULL; p->a=p->b=0; p++; p->a=p->b=0; p++; } /* Clear NT, so that we won't have troubles with that later on */ __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); ltr(0); /*将任务0的TSS加载到任务寄存器tr*/ lldt(0); /*将局部描述符表加载到局部描述符表寄存器*/ outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ outb_p(LATCH & 0xff , 0x40); /* LSB */ outb(LATCH >> 8 , 0x40); /* MSB */ set_intr_gate(0x20,&timer_interrupt); outb(inb_p(0x21)&~0x01,0x21); set_system_gate(0x80,&system_call); } |
set_tss_desc函数在include\asm\system.h中定义:
/*对8字节的描述符各个字节进行设置 */ #define _set_tssldt_desc(n,addr,type) \ __asm__ ("movw $104,%1\n\t" \ "movw %%ax,%2\n\t" \ "rorl $16,%%eax\n\t" \ "movb %%al,%3\n\t" \ "movb $" type ",%4\n\t" \ "movb $0x00,%5\n\t" \ "movb %%ah,%6\n\t" \ "rorl $16,%%eax" \ ::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \ "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \ ) /*0x89为TSS描述符的属性,0x82为LDT描述符的属性 */ #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89") #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82") |
上面那段汇编代码即对GDT一个8字节描述符表项的各个字节进行设置,设置完毕后每个描述符的内容如下表:
系统段 描述符 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
addr高8位 | 0x0089或0x0082 | addr低24位 | 0x0068(段界限) |
0x0089表示可用的386TSS(0x9),0x0082表示可用的LDT(0x2)。
init_task为全局变量,其初始化为INIT_TASK,INIT_TASK宏定义如下:
#define INIT_TASK \ /* state etc */ { 0,15,15, \ /* signals */ 0,{{},},0, \ /* ec,brk... */ 0,0,0,0,0,0, \ /* pid etc.. */ 0,-1,0,0,0, \ /* uid etc */ 0,0,0,0,0,0, \ /* alarm */ 0,0,0,0,0,0, \ /* math */ 0, \ /* fs info */ -1,0022,NULL,NULL,NULL,0, \ /* filp */ {NULL,}, \ {0,0}, \ /* ldt */ {0x9f,0xc0fa00}, \ /*代码长640k,界限粒度4k字节,基址0x0 */ {0x9f,0xc0f200}, \ /*数据长640k,界限粒度4k字节,基址0x0 */ }, \ /*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\ 0,0,0,0,0,0,0,0, \ 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \ _LDT(0),0x80000000, \ {} \ }, \ } |
在这段代码中我们关心的是ldt和tss的设置,每个任务的ldt表有3个表项,第一项未使用,第二项为code段,第三项为data 段。在任务0的描述符表设置完毕,这两个字段的地址也就成为任务0的TSS0描述符和LDT0描述符的内容。
ltr和lldt函数用于将描述符表项在GDT表中的索引加载到相应寄存器中,以任务0为例,在执行完这两个函数之后,tr寄存器中的值为4*8 = 0x20,ldt寄存器中的值为5*8 = 0x28。
下面对sched_init函数进行调试验证上述内容。首先在内核编译后产生的System.map文件中找到该函数的地址:0x72bc。启动bochsdbg,在0x72bc处设置断点,命令行如下:
<bochs:1> b 0x72bc
<bochs:2> c
(0) Breakpoint 1, 0x72bc in ?? ()
Next at t=16800742
(0) [0x000072bc] 0008:000072bc (unk. ctxt): push ebp ; 55
<bochs:3> u /50
……
000072ce: ( ): mov word ptr ds:0x5cd8, 0x68 ; 66c705d85c00006
800
000072d7: ( ): mov word ptr ds:0x5cda, ax ; 66a3da5c0000
000072dd: ( ): ror eax, 0x10 ; c1c810
000072e0: ( ): mov byte ptr ds:0x5cdc, al ; 8805dc5c0000
000072e6: ( ): mov byte ptr ds:0x5cdd, 0x89 ; c605dd5c000089
000072ed: ( ): mov byte ptr ds:0x5cde, 0x0 ; c605de5c000000
000072f4: ( ): mov byte ptr ds:0x5cdf, ah ; 8825df5c0000
000072fa: ( ): ror eax, 0x10 ; c1c810
000072fd: ( ): add eax, 0xffffffe8 ; 83c0e8
00007300: ( ): mov word ptr ds:0x5ce0, 0x68 ; 66c705e05c00006
800
00007309: ( ): mov word ptr ds:0x5ce2, ax ; 66a3e25c0000
0000730f: ( ): ror eax, 0x10 ; c1c810
00007312: ( ): mov byte ptr ds:0x5ce4, al ; 8805e45c0000
00007318: ( ): mov byte ptr ds:0x5ce5, 0x82 ; c605e55c000082
0000731f: ( ): mov byte ptr ds:0x5ce6, 0x0 ; c605e65c000000
00007326: ( ): mov byte ptr ds:0x5ce7, ah ; 8825e75c0000
0000732c: ( ): ror eax, 0x10 ; c1c810
……
0000737a: ( ): mov eax, 0x20 ; b820000000
0000737f: ( ): ltr ax ; 0f00d8
00007382: ( ): mov eax, 0x28 ; b828000000
00007387: ( ): lldt ax ; 0f00d0
……
<bochs:5> b 0x7387
<bochs:6> c
(0) Breakpoint 2, 0x7387 in ?? ()
Next at t=16801469
(0) [0x00007387] 0008:00007387 (unk. ctxt): lldt ax ; 0f00d0
<bochs:7> dump_cpu
……
cs:s=0x8, dl=0x7ff, dh=0xc09a00, valid=1
ss:s=0x10, dl=0xfff, dh=0xc09300, valid=7
ds:s=0x10, dl=0xfff, dh=0xc09200, valid=7
es:s=0x10, dl=0xfff, dh=0xc09300, valid=5
fs:s=0x10, dl=0xfff, dh=0xc09300, valid=1
gs:s=0x10, dl=0xfff, dh=0xc09300, valid=1
ldtr:s=0x28, dl=0x84640068, dh=0x8201, valid=1
tr:s=0x20, dl=0x847c0068, dh=0x8901, valid=1
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
……
0x000072d7 – 0x0000732c即对GDT表的TSS0和LDT0表项的内容进行设置。第0x0000737a – 0x00007387即对tr寄存器和ldt寄存器进行设置,完成设置后,以cs段寄存器值0x8为例,bit0到bit1位表示特权级为0,bit2位TI字段位0,表示高13位组成的的指是指向GDT表的索引(如果TI字段为1,表示高13位组成的值是指向LDT表的索引)。
启动任务0(move_to_user_mode宏)
在完成一系列的初始化工作之后,内核将切换到用户模式任务0中继续执行。其代码即宏move_to_user_mode,代码如下:
#define move_to_user_mode() \ __asm__ ("movl %%esp,%%eax\n\t" \ "pushl $0x17\n\t" \ "pushl %%eax\n\t" \ "pushfl\n\t" \ "pushl $0x0f\n\t" \ /*压入任务0的代码段(cs)选择符*/ "pushl $1f\n\t" \ /*压入标号1的地址,作为iret的返回地址*/ "iret\n" \ /*切换到任务0,开始执行其指令序列 */ "1:\tmovl $0x17,%%eax\n\t" \ "movw %%ax,%%ds\n\t" \ "movw %%ax,%%es\n\t" \ "movw %%ax,%%fs\n\t" \ "movw %%ax,%%gs" \ :::"ax") |
在进程0的LDT表中设置的代码段和数据段的基址都为0,这与内核代码段和数据段的基址一致,在压栈过程中,压入的返回地址就是内核代码执行序列的地址,与之前内核执行序列的最关键的区别在于:段寄存器中的选择符为任务0独有LDT表的索引,而不再是指向GDT 表的索引。LDT表的基址通过ldlt寄存器来查找:在初始化或任务切换过程中,把描述符对应任务LDT的描述符的选择子装入LDTR,处理器根据装入LDTR可见部分的选择子,从GDT中取出对应的描述符,并把LDT的基地址、界限和属性等信息保存到LDTR的不可见的高速缓冲寄存器中。
下面对move_to_user_mode宏进行调试验证。首先在Systemp.map文件中找到main函数地址0x664c,通过查看汇编指令流找到move_to_user_mode宏的位置,命令行如下:
<bochs:1> b 0x664c
<bochs:2> c
(0) Breakpoint 1, 0x664c in ?? ()
Next at t=16769622
(0) [0x0000664c] 0008:0000664c (unk. ctxt): push ebp ; 55
<bochs:3> u /70
……
00006753: ( ): mov eax, esp ; 89e0
00006755: ( ): push 0x17 ; 6a17
00006757: ( ): push eax ; 50
00006758: ( ): pushfd ; 9c
00006759: ( ): push 0xf ; 6a0f
0000675b: ( ): push 0x6761 ; 6861670000
00006760: ( ): iretd ; cf
00006761: ( ): mov eax, 0x17 ; b817000000
00006766: ( ): mov ds, ax ; 668ed8
00006769: ( ): mov es, ax ; 668ec0
0000676c: ( ): mov fs, ax ; 668ee0
0000676f: ( ): mov gs, ax ; 668ee8
00006772: ( ): add esp, 0xc ; 83c40c
……
<bochs:4> b 0x6761
<bochs:5> c
(0) Breakpoint 1, 0x6761 in ?? ()
Next at t=16878984
(0) [0x00006761] 000f:00006761 (unk. ctxt): mov eax, 0x17 ; b8170000
00
<bochs:6> dump_cpu
……
eip:0x6761
cs:s=0xf, dl=0x9f, dh=0xc0fa00, valid=1
ss:s=0x17, dl=0x9f, dh=0xc0f200, valid=1
ds:s=0x0, dl=0x0, dh=0x0, valid=0
es:s=0x0, dl=0x0, dh=0x0, valid=0
fs:s=0x0, dl=0x0, dh=0x0, valid=0
gs:s=0x0, dl=0x0, dh=0x0, valid=0
ldtr:s=0x28, dl=0x84640068, dh=0x8201, valid=1
tr:s=0x20, dl=0x847c0068, dh=0x8901, valid=1
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
……
这些调试信息有3个地方值得注意。首先是eip寄存器的指针指向iretd的下一条指令地址处。其次是此时的代码段描述符为0xf,bit0到bit1位表示特权级为3,bit2位TI字段位1,表示高13位组成的的指1是指向LDT表的第1项索引(从0开始)。最后是ldtr的值:s=0x28表示该描述符在GDT表的位置为0x28/8=5,8字节描述符的值为0x00 0x0082 0x018464 0x0068,即dl与dh 的组合,它表示LDT表的地址为0x00018464(线性地址),查看内存可知代码段和数据段的基址和段限长,命令行如下:
<bochs:7> xp /6 0x018464
[bochs]:
0x00018464 <bogus+ 0>: 0x00000000 0x00000000 0x0000009f
0x00c0fa00
0x00018474 <bogus+ 16>: 0x0000009f 0x00c0f200
这些值即之前分析到的init_task.task.ldt所设置的值。
创建子进程(fork函数)
fork函数是一个系统调用,用于创建子进程。Linux中所有进程都是进程0的子进程。关于对系统函数的调用过程将在以后的文章进行阐述。这里仅对其辅助函数进行分析,这些函数位于kernel/fork.c中。copy_process函数用于创建并复制父进程的代码段和数据段以及环境,代码如下:
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) { struct task_struct *p; int i; struct file *f; /*在物理内存上找到一个未被占用的页面 */ p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; task[nr] = p; *p = *current; /* NOTE! this doesn't copy the supervisor stack */ /* !以下省略对p的某些字段的初始化代码 */ if (last_task_used_math == current) __asm__("clts ; fnsave %0"::"m" (p->tss.i387)); /*设置新任务代码和数据段基址、限长并复制页表 */ if (copy_mem(nr,p)) { task[nr] = NULL; free_page((long) p); return -EAGAIN; } for (i=0; i<NR_OPEN;i++) if (f=p->filp[i]) f->f_count++; if (current->pwd) current->pwd->i_count++; if (current->root) current->root->i_count++; if (current->executable) current->executable->i_count++; /*设置子进程在GDT表的TSS和LDT描述符 */ set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); p->state = TASK_RUNNING; /* do this last, just in case */ return last_pid; } |
copy_men函数将设置新任务的代码和数据段基址、限长并复制页表。代码如下:
int copy_mem(int nr,struct task_struct * p) { unsigned long old_data_base,new_data_base,data_limit; unsigned long old_code_base,new_code_base,code_limit; /*取得任务0的LDT表中的代码段和数据段的基址和段限长 */ code_limit=get_limit(0x0f); data_limit=get_limit(0x17); old_code_base = get_base(current->ldt[1]); old_data_base = get_base(current->ldt[2]); if (old_data_base != old_code_base) panic("We don't support separate I&D"); if (data_limit < code_limit) panic("Bad data_limit"); /*设置被创建进程的基址 */ new_data_base = new_code_base = nr * 0x4000000; p->start_code = new_code_base; set_base(p->ldt[1],new_code_base); set_base(p->ldt[2],new_data_base); /*将新进程的线性地址内存页对应到实际物理地址内存页面 */ if (copy_page_tables(old_data_base,new_data_base,data_limit)) { free_page_tables(new_data_base,data_limit); return -ENOMEM; } return 0; } |
Linux0.11内核将整个4G的地址空间划分成64块,供64个进程使用,子进程和父进程的代码段或数据段的地址由于选择不同的段基址将使得它们在逻辑上是分离的(分页机制和写时复制可能使得任务0和任务1的代码或数据存放在同一物理页面)。
进程调度(schedule函数)
schedule函数实现进程调度,位于kernel/sched.h中,代码如下:
void schedule(void) { int i,next,c; struct task_struct ** p; /* check alarm, wake up any interruptible tasks that have got a signal */ /*对所有进程进行检测,唤醒任何一个已经得到信号的任务 */ for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) { if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1<<(SIGALRM-1)); (*p)->alarm = 0; } if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state==TASK_INTERRUPTIBLE) (*p)->state=TASK_RUNNING; } /* this is the scheduler proper: */ /*根据进程的时间片和优先权来选择随后要执行的任务 */ while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } if (c) break; for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } /*cpu切换到新进程执行 */ switch_to(next); } |
switch_to宏代码完成cpu切换任务的工作,这也是我们研究内存分段机制的重点,它的代码位于include/linux/sched.h中,代码如下:
#define switch_to(n) {\ struct {long a,b;} __tmp; \ __asm__("cmpl %%ecx,_current\n\t" \ "je 1f\n\t" \ "movw %%dx,%1\n\t" \ "xchgl %%ecx,_current\n\t" \ "ljmp %0\n\t" \ /*此处完成任务切换*/ "cmpl %%ecx,_last_task_used_math\n\t" \ "jne 1f\n\t" \ "clts\n" \ "1:" \ ::"m" (*&__tmp.a),"m" (*&__tmp.b), \ "d" (_TSS(n)),"c" ((long) task[n])); \ } |
任务切换的具体操作见下图:
图1:任务切换操作示意图(摘自Linux内核完全注释)
接下来将通过调试验证切换过程,首先在System.map文件中找到schedul的地址:0x6b8c,启动bochsdgb,找到switch_to宏中ljmp指令的位置,命令行如下:
<bochs:1> b 0x6b8c
<bochs:2> c
(0) Breakpoint 1, 0x6b8c in ?? ()
Next at t=16886214
(0) [0x00006b8c] 0008:00006b8c (unk. ctxt): push ebp ; 55
<bochs:3> u /100
……
00006c6b: ( ): cmp dword ptr ds:0x1919c, ecx ; 390d9c910100
00006c71: ( ): jz .+0x6c8a ; 7417
00006c73: ( ): mov word ptr ss:[ebp+0xfffffffc], dx ; 668955f
c
00006c77: ( ): xchg dword ptr ds:0x1919c, ecx ; 870d9c910100
00006c
7d: ( ): jmp far ss:[ebp+0xfffffff8] ; ff6df8
00006c80: ( ): cmp dword ptr ds:0x191a0, ecx ; 390da0910100
00006c86: ( ): jnz .+0x6c8a ; 7502
00006c88: ( ): clts ; 0f06
……
<bochs:4> b 0x6c7d
<bochs:5> c
(0) Breakpoint 2, 0x6c7d in ?? ()
Next at t=16886886
(0) [0x00006c7d] 0008:00006c7d (unk. ctxt): jmp far ss:[ebp+0xfffffff8] ; ff6df8
0x00006c7d处的jmp far指令跳转的地址为段选择符:偏移值,其中段选择符为ss:[ebp+0xfffffff8]的32位到47位,偏移值为ss:[ebp+0xfffffff8]的0位到31位,如果段选择符为任务状态段选择符TSS,cpu将自动切换进程,此时cpu会把所有寄存器的状态保存到当前任务寄存器TR中的TSS段选择符所指向的当前任务数据结构的tss结构中,然后把新任务状态段选择符所指向的新任务数据结构中tss结构中的寄存器信息恢复到cpu中,系统就正式运行新切换的任务了。通过调试查看这一个过程,命令行如下:
<bochs:6> dump_cpu
……
ebp:0x1915c
……
eip:0x6c7d
……
ldtr:s=0x28, dl=0x84640068, dh=0x8201, valid=1
tr:s=0x20, dl=0x847c0068, dh=0x8901, valid=1
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
……
<bochs:7> print-stack
00019148 [00019148] 0003
0001914c [0001914c] 0000
00019150 [00019150] 0ffc
00019154 [00019154] 1f248
00019158 [00019158] 0030
0001915c [0001915c] 1f248
00019160 [00019160] 6ca4
00019164 [00019164] 743b
00019168 [00019168] 0003
0001916c [0001916c] 3e400
00019170 [00019170] 1fae4
00019174 [00019174] 0017
00019178 [00019178] 0017
0001917c [0001917c] 0017
00019180 [00019180] 67a1
00019184 [00019184] 000f
%ebp寄存器的值为0x1915c,ljmp跳转的地址为0x30:0x1f248,%gdtr的基址为0x5cb8,段描述符的地质为0x5cb8+0x30 = 0x5ce8,取出该描述符进行判断:
<bochs:8> x /2 0x5ce8
[bochs]:
0x00005ce8 <bogus+ 0>: 0xf2e80068 0x000089ff
这段调试信息告诉我们:这个描述符是一个基地址为0x00fff2e8,段限长为0x68的可用的386TSS描述符。因此cpu将自动进行进程切换。Cpu将取出从地址0x00fff2e8开始的0x68个字节内容对接下来要执行的进程tss的设置:
<bochs:9> x /26 0x00fff2e8
[bochs]:
0x00fff2e8 <bogus+ 0>: 0x00000000 0x01000000 0x00000010
0x00000000
0x00fff2f8 <bogus+ 16>: 0x00000000 0x00000000 0x00000000
0x00000000
0x00fff308 <bogus+ 32>: 0x0000677c 0x00000616 0x00000000
0x0003e400
0x00fff318 <bogus+ 48>: 0x00000021 0x00000003 0x0001f248
0x0001f248
0x00fff328 <bogus+ 64>: 0x00000000 0x00000ffc 0x00000017
0x0000000f
0x00fff338 <bogus+ 80>: 0x00000017 0x00000017 0x00000017
0x00000017
0x00fff348 <bogus+ 96>: 0x00000038 0x80000000
对这些调试信息按照tss字段的顺序排列得出下表:
任 | BIT31—BIT16 | BIT15—BIT1 | BIT0 | Offset | Data |
0000000000000000 | 链接字段 | 0 | 0x00000000 | ||
ESP0 | 4 | 0x01000000 | |||
0000000000000000 | SS0 | 8 | 0x00000010 | ||
ESP1 | 0CH | 0x00000000 | |||
0000000000000000 | SS1 | 10H | 0x00000000 | ||
ESP2 | 14H | 0x00000000 | |||
0000000000000000 | SS2 | 18H | 0x00000000 | ||
CR3 | 1CH | 0x00000000 | |||
EIP | 20H | 0x0000677c | |||
EFLAGS | 24H | 0x00000616 | |||
EAX | 28H | 0x00000000 | |||
ECX | 2CH | 0x0003e400 | |||
EDX | 30H | 0x00000021 | |||
EBX | 34H | 0x00000003 | |||
ESP | 38H | 0x0001f248 | |||
EBP | 3CH | 0x0001f248 | |||
ESI | 40H | 0x00000000 | |||
EDI | 44H | 0x00000ffc | |||
0000000000000000 | ES | 48H | 0x00000017 | ||
0000000000000000 | CS | 4CH | 0x0000000f | ||
0000000000000000 | SS | 50H | 0x00000017 | ||
0000000000000000 | DS | 54H | 0x00000017 | ||
0000000000000000 | FS | 58H | 0x00000017 | ||
0000000000000000 | GS | 5CH | 0x00000017 | ||
0000000000000000 | LDTR | 60H | 0x00000038 | ||
I/O许可位图偏移 | 000000000000000 | T | 64H | 0x80000000 |
切换后的各个寄存器将按照以上表对应的值进行赋值,继续调试,来观察一下这个切换过程,命令行如下:
<bochs:10> n
Next at t=16886887
(0) [0x0000677c] 000f:0000677c (unk. ctxt): test eax, eax ; 85c0
<bochs:11> dump_cpu
eax:0x0
ebx:0x3
ecx:0x3e400
edx:0x21
ebp:0x1f248
esi:0x0
edi:0xffc
esp:0x1f248
eflags:0x616
eip:0x677c
cs:s=0xf, dl=0x9f, dh=0x4c0fa00, valid=1
ss:s=0x17, dl=0x9f, dh=0x4c0f300, valid=1
ds:s=0x17, dl=0x9f, dh=0x4c0f300, valid=1
es:s=0x17, dl=0x9f, dh=0x4c0f300, valid=1
fs:s=0x17, dl=0x9f, dh=0x4c0f300, valid=1
gs:s=0x17, dl=0x9f, dh=0x4c0f300, valid=1
ldtr:s=0x38, dl=0xf2d00068, dh=0x82ff, valid=1
tr:s=0x30, dl=0xf2e80068, dh=0x89ff, valid=1
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
dr0:0x0
dr1:0x0
dr2:0x0
dr3:0x0
dr6:0xffff0ff0
dr7:0x400
tr3:0x0
tr4:0x0
tr5:0x0
tr6:0x0
tr7:0x0
cr0:0x8000001b
cr1:0x0
cr2:0x0
cr3:0x0
cr4:0x0
inhibit_mask:0
done
这个切换过程也就一目了然了:6个段寄存器的低三位都为1,表明这个段描述符是局部描述符表的索引;局部描述符表的地址由全局描述符表提供,即0x5cb8 + 0x38地址处的描述符;%eip中的地址作为切换后的新进程的执行序列;通用寄存器的值从切换进程的tss结构中取得;ldtr的值由cpu自动加载,从tss结构中取得。
后记
终于完成了内存分段机制的分析,对于内存分段机制的理解实际上可以看成怎么将一个二维数组映射成一位数组,如果需要通过进程的局部描述符表来寻址,则可以把分段机制看成怎么将一个三维数组映射成一位数组,就是这么简单!(如果说错了,不要怪我^-^)