MIT-JOS系列:问题汇总
-
页目录表基址(保存在CR3)为物理地址,但GDT表基址(保存在GDTR)为线性地址,为什么?
A:在开启了分页后,除了
cr3
,所有地址都要经过MMU自动进行虚拟地址到物理地址的转换,这个是无法绕过的。在进入kernel初期,GDTR
放着的还是boot时载入的gdt表的地址(这时候是物理),随后载入简易页表开启保护和分页模式,此后GDTR
存放的地址都视为虚拟地址。由于kernel初期的简易页表同时映射了虚拟地址的0x00000000-0x00400000到物理地址0x00000000-0x00400000,所以继续使用存放着低地址的GDTR
,地址转换不会出错。而对于cr3
,是指明物理页面的地址,用来找物理页表的,就必须使用物理地址,也是不可避免的。 -
载入简易页表并开启分页模式后,寻址的过程发生了什么变化?为什么GDTR中一开始存放的物理地址在这个过程中没有改变,却还是能找到正确的GDT表进行地址变换?
A:之前在
boot
阶段开启了保护模式并载入GDT表到GDTR
。由于载入是在保护模式开启之前,即仍处于实地址模式,因此载入到GDTR
中的地址是GDT表的物理地址,保护模式开启后被视为虚拟地址,但此时还没开启分页模式,因此虚拟地址等同于物理地址。在kern/entry.S
中开启分页模式后,对所有地址都认为是虚拟地址,并通过页表转换得到真实的物理地址。简易页表同时建立了虚拟内存0x00000000-0x00400000到物理地址0x00000000-0x00400000的映射,因此GDTR
中虽然存放的是物理地址一直没有变过,但在分段、分页后仍然可以作为虚拟地址找到正确的物理地址 -
在
mem_init()
中通过lcr3(PADDR(kern_pgdir));
更改了页表目录,但此时GDTR
放的gdt表地址还是旧的物理地址(比如我打印得到的0x7c4c),新的页表目录没有映射这段低虚拟地址空间。在lcr3
前可以通过x/10x *0x7c4c
访问到这段内存,但在lcr3
操作后之后就访问不了了,这时候又还没载入新的gdt地址(直到env_init()
的env_init_percpu
才载入),那么这期间内是怎么通过gdt进行逻辑地址到线性地址的转换,为什么程序还能正常运行?**A:问了老师,确实存在如上的问题。在2007版的JOS操作系统实验中,kernel的
entry.S
中重新载入过GDT表基地址到GDTR
保证寻址的正确,但2016版更新GDTR
确实晚了,可能是考虑不严谨。至于为什么还能正常寻到址,推测可能是CPU做了缓存,暂时免去了这部分的地址转换和页面查找 -
为什么启动新进程的函数
env_run()
永不返回?A:这个“永不返回”不是指它没有返回值或在函数里死循环,而是指它利用
iret
指令跳转去执行别的代码了,而且永远不会回到这里了 -
运行环境时调用了
env_pop_tf()
恢复环境的重要寄存器的值,什么时候push进去的?为什么是pop?如何pop?A:
env_pop_tf()
将tf
的内容视为栈,当前栈顶设置为tf
的第一个字段,pop过程能顺序访问tf
中的每个数据,从逐个出栈tf
里面的数据到相应的寄存器-
movl %0,%%esp
将当前栈指针指向输入变量tf
-
popal
恢复所有r32
寄存器,即tf.PushRegs
里的东西 -
pop出
tf_es
赋值到es
-
pop出
tf_ds
赋值到ds
-
跳过
tf_trapno
和tf_err
,使esp
指向tf_eip
-
调用
iret
,跳转到用户进程执行代码调用
iret
时:从esp
指向的栈中顺序出栈eip, cs, eflags(标志寄存器), esp, ss
赋值到相应寄存器,然后程序跳转到cs:eip
处继续执行,因此这个函数正常状态下不会返回
-
-
load_icode()
函数中数据复制的时候用到了ph->va
,这个虚拟地址是参数环境的,因此要先设置cr3
,才能保证正确访问到这个环境的虚拟地址。感觉这个时候设置cr3
有点早了,不是应该env_run()
时设置吗?没有更好的解决办法吗?A:查了下都是这么做的。。用完了记得恢复就完事儿了
-
在
trap_init()
中,为什么extern char t_divide[]
导出代码段的标号,或者void t_divide()
声明一个函数,都能得到正确的中断处理程序入口的偏移?A:标号处相当于函数定义,并用
.global
导出为全局函数定义。函数的声明会关联到函数的定义,因此两种方式都可以 -
_alltraps
为什么压栈esp
作为参数?A:回顾MIT-JOS系列3:启动内核中关于栈的说明,函数
call
向栈中压入函数的参数,call
时压栈eip
以便函数返回时恢复执行接下来的代码,call
后压栈ebp
,当前esp
赋值给ebp
,然后做一些奇奇怪怪的寄存器操作,比如esi,edi
,看不懂,它们在这个问题里不重要(。进入函数后栈大概长这样:
高地址:
参数n
...
参数1
eip
ebp(旧)
...
低地址进入函数后,当前
ebp
值为旧ebp+4
(因为push
了一个ebp
),因此ebp[0]
:旧ebp
ebp[1]
:eip
ebp[2]
:参数1
...函数进入后会根据函数头找参数的个数,
trap
函数只有一个参数,需要一个指向trapframe
结构体的指针,即一个trapframe
结构体的首地址,这个地址就是push
进去的esp
的值,然后tf
赋值为这个地址,就能用tf->xxx
取到其他值。注意这里要的是地址而不是直接的值,所以push
进去了esp
(esp
指向栈最后一个push
进入的参数,最后的指令为pushal
,因此esp
指向tf.tf_regs.reg_edi
) -
在向GDT表中写入TSS描述符时,为什么是
GD_TSS0>>3
?并且gdt表中能看到设置其他GDT段描述符的时候也偏移了三位?这个右移三位是什么意思?A:观察gdt表的结构是
struct Segdesc gdt[]
,表中每一项是一个段描述符,占8个字节。GD_KT
,GD_TSS0
等用的都是段描述符的地址,它们应该是对齐到8字节边界的,即最低3位是0。在利用gdt表的索引填入gdt表时,索引是0,1,2,3...的连续整数,因此把地址右移三位转化成索引 -
为每个cpu初始化tss描述符的时候,为什么是
gdt[(GD_TSS0>>3)+id]
?A:对每个cpu而言,gdt表中只有tss描述符是不一样的,因为要指向它们各自的内核栈。tss描述符在gdt的最后项,因此根据其id顺序累加写入gdt表即可。之后用
ltr
载入tss描述符到寄存器的时候,载入的地址(tss描述符首地址)为((GD_TSS0>>3)+id)<<3=GD_TSS0+(id<<3)
-
为什么各用户进程使用相同的虚拟地址?比如用户栈都在同一个虚拟地址?
A:用户进程的地址是相互独立的(UTOP之下),每个用户空间被分配内存时,它们被映射到新的空闲页面中,对于用户进程A和B而言,它们的栈虽然虚拟地址相同,但物理页面不同,所以不会相互冲突;因此
fork
的实验中父进程向子进程复制页面数据的时候要先映射子进程的物理页面到自己的地址空间,才能开始拷贝 -
中断和异常概念整理
中断:由处理器外部异步地引发(硬件引发,中断号都在32-255之间)
异常:由处理器内部同步地引发(中断号0-31都是异常,32-255部分
int
指令引发的中断是异常,比如系统调用,这部分很少)异常包括:
- 故障(fault):中断返回后重新执行引起故障的指令(例如页面错误,经过修复后重新访问该页面)
- 陷阱(trap):中断返回后执行下一条指令(一般用于用户自发地陷入内核态)
- 中止(abort):严重的系统错误,程序中止
在设置中断向量表中的中断门时,包括两个概念:
- 中断门(Interrupt gate):进入中断后寄存器
eflags
的IF
位自动清零以屏蔽中断,返回用户态恢复寄存器时恢复IF
位 - 陷阱门(Trap gate):进入中断后不改变
IF
位
需要注意的是,这里的中断门和陷阱门与上述“中断”与异常中的”陷阱“是两个概念,是在设置中断向量表时自定义的两个行为。若想内核的逻辑比较简单,就可以禁止中断嵌套,把所有的中断描述符都设置为”中断门“;若设计的内核比较复杂,就可以允许部分中断的中断嵌套,设置它为”陷阱门“
x86系统预留中断类型表格中给出的type是异常的type,与这个中断描述符是中断门还是陷阱门无关
对这些概念不必要有非常明确的指定术语区分,自己能按自己的理解分清楚中断、异常,分清楚异常类型(中断恢复后的行为)以及中断门和陷阱门的区分(进入中断时的行为)就行