date: 2019-02-18
tags: OS 6.828
这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。
多个进程同时运行导致了对操作系统的3项主要要求:
而这其中isolation是最不好完成的要求。
那么isolation要完成什么呢?
kernel用硬件机制来辅助得到process isolation
先来说硬件上的user / kernel mode flag。
在x86上叫CPL,是%cs
的后两位
CPL=0 -- kernel mode
CPL=3 -- user mode
x86 CPL保护了很多寄存器,包括:
%cs4
),包括%cs
自己如何用system call来切换CPL,考虑如下几个问题
可不可以设置一个system call直接
set CPL=0
这样肯定不行,因为用户可以直接设置CPL,就没有办法保护内核了。
所以x86给出的答案是:
这样就有well-defined notion of user vs kernel,不会出现再kernel mode运行user code,更不会有user mode里运行kernel code。
之后来说如何隔离进程内存,也就是address space。
那么如何建立isolated address spaces呢?
xv6用的是x86的memory management unit(MMU)里的 "paging hardware",MMU会把所以地址进行翻译:
CPU -> MMU -> RAM
|
pagetable
VA -> PA
MMU会对所有memory reference进行翻译:user and kernel。
instructions and data
最后来说xv6的system call是如何实现的
xv6的process / stack diagram:
简单的xv6的user/kernel virtual address-space设置:
FFFFFFFF:
...
80000000: kernel
user stack
user data
00000000: user instructions
kernel通过设置MMU来让user code只能访问到lower half。每个进程会有不同的address space,但是kernel的mapping都是一样的。
下面来看看xv6的代码层面是如何完成的调用一个system call并返回的。
我们选择的system call是sh.asm
里的write
,注意这里note有误,应该是b * 0x0d32
而不是0d42
。而且运行x/3i
的结果和sh.asm
里面记录的内容也不一样。。。神奇...正常运行的代码应该是:
00000d32 <write>:
SYSCALL(write)
d32: b8 10 00 00 00 mov $0x10,%eax
d37: cd 40 int $0x40
d39: c3 ret
其中0x10
是write的system call number。这里还有一个疑问,就是系统是怎么在启动之后自动调用shell的,没太明白。下面就是按照lecture中的要求进行调试:
(gdb) b * 0x0d32
Breakpoint 1 at 0xd32
(gdb) c
Continuing.
...
(gdb) info reg
eax 0x3f7a 16250
ecx 0x24 36
edx 0x0 0
ebx 0x24 36
esp 0x3f4c 0x3f4c
ebp 0x3f98 0x3f98
esi 0x11b9 4537
edi 0x0 0
eip 0xd32 0xd32
eflags 0x216 [ PF AF IF ]
cs 0x1b 27
ss 0x23 35
ds 0x23 35
es 0x23 35
fs 0x0 0
gs 0x0 0
可以看到这时的%cs
是0x1b
也就是CPL=3,处于user mode。
(gdb) x/4x $esp
0x3f4c: 0x00000ea5 0x00000002 0x00003f7a 0x00000001
esp
的这4个值分别是ebf
(return address?这里存疑...),2是fd,0x3f7a
是buffer的地址,1是count,对应的就是write(2, 0x3f7a, 1)
(gdb) x/c 0x3f7a
0x3f7a: 36 '$'
就是说buffer里面存的就是要写出的$
。如果继续往下运行,运行两步之后
(gdb) info reg
eax 0x10 16
ecx 0x24 36
edx 0x0 0
ebx 0x24 36
esp 0x8dffefe8 0x8dffefe8
ebp 0x3f98 0x3f98
esi 0x11b9 4537
edi 0x0 0
eip 0x80105d49 0x80105d49 <vector64+2>
eflags 0x216 [ PF AF IF ]
cs 0x8 8
ss 0x10 16
ds 0x23 35
es 0x23 35
fs 0x0 0
gs 0x0 0
%cs
变为8,也就是CPL=0,进入了kernel模式,且注意eip
已经进入了kernel memory里面,esp
已经在kernel stack中。
(gdb) x/6wx $esp
0x8dffefe8: 0x00000000 0x00000d39 0x0000001b 0x00000216
0x8dffeff8: 0x00003f4c 0x00000023
可以看到INT
指令把一些之前的register放在堆栈保存起来了,保存在了kernel stack。保存的register包括err
, eip
, cs
, eflags
, esp
, ss
。之所以会进行保存,是因为INT
会overwrite这些register。
总结来说INT
做了如下的内容:
前文我们知道eip
是给定的kernel-supplied vector,那么esp
来源于哪里呢?
为什么INT需要保存用户状态?应该保存多少状态?
而保存剩余的register用的是trapasm.S
里头的alltraps
函数。
# vectors.S sends all traps here.
.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
# Call trap(tf), where tf=%esp
pushl %esp
call trap
addl $4, %esp
# Return falls through to trapret...
.globl trapret
trapret:
popal
popl %gs
popl %fs
popl %es
popl %ds
addl $0x8, %esp # trapno and errcode
iret
这个函数就是现在eip
对应的0x80105d49
对应的位置。所以堆栈里面存有的是19words:
ss
esp
eflags
cs
eip
err -- INT saved from here up
trapno
ds
es
fs
gs
eax..edi
这些都是之后会被恢复的。同时有的时候kernel的C code需要对这些尽心读写,就通过x86.h
中的struct trapframe
进行操作。
可以看到调用了trap
函数(在trap.c
文件中),其中pushl %esp
就是trap
函数的参数tf
。进入这个函数之后,有:
(gdb) print tf
$1 = (struct trapframe *) 0x8dffefb4
(gdb) print *tf
$2 = {edi = 0, esi = 4537, ebp = 16280, oesp = 2382360532, ebx = 36, edx = 0,
ecx = 36, eax = 16, gs = 0, padding1 = 0, fs = 0, padding2 = 0, es = 35,
padding3 = 0, ds = 35, padding4 = 0, trapno = 64, err = 0, eip = 3385,
cs = 27, padding5 = 0, eflags = 534, esp = 16204, ss = 35, padding6 = 0}
trap
函数的代码如下:
void
trap(struct trapframe *tf) {
if(tf->trapno == T_SYSCALL){
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}
...
进入trap函数的不只有system call,还有interrupt和fault,所以会先判断一下tf->trapno
是不是T_SYSCALL
。在本次运行的代码里,的确是(注意T_SYSCALL
是60就是INT
调用的0x40
)。
判断完就会运行myproc()
(函数在proc.c
中,函数返回的struct proc
在proc.h
中)。myproc()->tf = tf;
会被当成当前的system call的单数。注意这个myproc
函数很重要,返回的永远都是当前的process,所以可以在其他函数中通过调用它达到共享参数的功能:
// Disable interrupts so that we are not rescheduled
// while reading proc from the cpu structure
struct proc*
myproc(void) {
struct cpu *c;
struct proc *p;
pushcli();
c = mycpu();
p = c->proc;
popcli();
return p;
}
然后就调用sycall()
,在syscall.c
中
void
syscall(void) {
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
用curproc->tf->eax
得到当前的system call的序号,这里是0x10
,对应的函数是sys_write
,而sys_write
在sysfile.c
中,
int
sys_write(void) {
struct file *f;
int n;
char *p;
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0)
return -1;
return filewrite(f, p, n);
}
上面的arg*
函数会从user stack中读取write(fd, buf, n)
的参数,例如argint
函数在syscall.c
中:
// Fetch the nth 32-bit system call argument.
int
argint(int n, int *ip) {
return fetchint((myproc()->tf->esp) + 4 + 4*n, ip);
}
就是从myproc()
中读出整数。等写入完了,也就是sys_write
中的filewrite
返回了,就开始一层一层返回,最终要还原user register。我们来一步一步看这个。
syscall
函数把filewrite
的返回值设为了curproc->tf->eax
,之后就一步一步没有操作得返回到了trapasm.S
。之后一连串直接运行到iret
。在运行iret
之前,就已经把除了int
保存的那5个register以外的都恢复了,iret
会把eip
, cs
, eflags
, esp
, ss
这5个还原,因为还原了cs
,所以也把模式 还原到了CPL=3,user mode。从而完成了
(gdb) info reg
eax 0x3f7a 16250
ecx 0x20 32
edx 0x0 0
ebx 0x20 32
esp 0x3f4c 0x3f4c
ebp 0x3f98 0x3f98
esi 0x11ba 4538
edi 0x0 0
eip 0xd32 0xd32
eflags 0x216 [ PF AF IF ]
cs 0x1b 27
ss 0x23 35
ds 0x23 35
es 0x23 35
fs 0x0 0
gs 0x0 0
还原之后的register(不是很知道这些许的不同是什么意思,不过不同的都不是iret
还原的,应该就是调用system call所需要产生的变化吧)。
总结一下: