Topic:
- 用户/内核 隔离
xv6
系统调用
多进程驱动的关键需求:
Multiplexing
Isolation
隔离是其中最受约束的要求
Interaction / Sharing
什么是隔离:
强制分离包含失败的进程的影响
进程是通常意义上隔离的单位
隔离避免进程间的破坏或监视
读写内存,使用100%的CPU负载,修改文件修饰符,&c
防止恶意进程或因bug通过接口干扰操作系统
恶意进程可能会试图欺骗 h/w 或内核
内核使用硬件机制作为进程隔离的一部分:
- 用户/内核 模式标志
- 地址空间
- 时间片
- 系统调用接口
硬件的用户/内核模式:
- 控制指令能否访问特权h/w
- 在x86上调用
CPL
(%cs
寄存器的低两位) CPL = 0
:内核模式 —— 特权CPL = 3
:用户模式 —— 无特权x86
CPL
保护与隔离相关的许多处理器寄存器- 访问
I/O
端口 - 访问控制寄存器 (
eflags
、%cs4
、...) - 包括
%cs
本身 - 间接影响内存访问权限
- 以上所有由内核正确设置
- 每个严谨的微处理器都有某种用户/内核标志
如何进行系统调用:
Q:用户程序使用系统调用的设计能否可行?
set CPL = 0
jmp sys_open
缺点:CPL = 0
的命令由用户指定
Q:如果组合指令设置CPL = 0,但是需要立即跳转到内核中的某个位置?
缺点:用户可能会跳转至内核
x86的方案:
只有少数允许的内核入口点 (vector
)
INT
指令设置 CPL = 0
并跳转到入口点
但用户代码不能以其他方式修改 CPL
或跳转到内核中的其他位置
系统调用在返回用户代码之前设置 CPL = 3
- 也是一个组合指令(不能单独设置CPL和jmp)
结果:定义明确的用户与内核的概念
- CPL = 3:执行用户代码
- CPL = 0:从内核代码中的入口点开始执行
如何隔离进程内存?
想法:“地址空间”
给每个进程的代码,变量,堆,栈设置它可以访问的内存,以避免该进程访问其他内存(内核或进程)
如何创建隔离的地址空间?
xv6在内存管理单元(MMU)中使用x86“分页硬件”
MMU翻译(或“映射”)程序发出的每个地址
CPU -> MMU -> RAM
|
pagetable
VA -> PA
MMU转换所有内存引用:用户和内核,指令和数据指令仅使用VAs,而不使用PA
kernel为每个进程设置不同的页表,每个进程的页表只允许访问该进程的RAM
如何实现 xv6 系统调用
xv6进程/堆栈图:
- 用户进程; 内核线程
- 用户堆栈; 内核堆栈
- 两种机制:
- 在用户/内核之间切换
- 在内核线程之间切换
- 陷阱框架
- 内核函数调用...
- struct context
简化 xv6 用户/内核 虚拟地址空间的设置:
...
80000000: kernel
user stack
user data
00000000: user instructions
内核配置MMU,只为每个进程的下半部分地址空间提供用户代码访问权限
但内核(高)映射对于每个进程都是相同的
系统调用入口:
在用户空间执行,(sh.asm
,write()
库函数)
|
|
内核入口,INT
指令:
|
|
INT
将 eip
和 esp
改变至高内核地址
eip
在哪里?
在内核提供的向量 - 只有用户可以去的地方,因此用户程序无法凭借CPL = 0
跳转到内核中随机位置。
x/6wx $eip
INT
保存一些用户寄存器:err
,eip
,cs
,eflags
,esp
,ss
为什么 INT
仅保存这些寄存器?
这些寄存器由 INT
重写
INT
做了什么:
- 切换到当前进程的内核堆栈
- 在内核堆栈上保存了一些用户寄存器
- 设置CPL = 0
- 开始在内核提供的“向量”执行
esp
来自哪里?
内核告诉h/w
创建进程时要使用的内核堆栈
为什么INT
干扰保存用户状态?
应该保存多少状态?
透明度 vs 速度
将剩余的用户寄存器保存在内核堆栈中
trapasm.S alltraps
pushal
入栈了8个寄存器:eax
.. edi
|
|
当系统调用返回时,这些用户状态最终将恢复,同时内核C代码有时需要读/写x86.h
定义的struct trapframe
中保存的值。
为什么用户寄存器保存在内核堆栈而不是用户堆栈中?
entering kernel C code:
the pushl %esp creates an argument for trap(struct trapframe *tf)
now we're in trap() in trap.c
print tf
print *tf
kernel system call handling:
device interrupts and faults also enter trap()
trapno == T_SYSCALL
myproc()
struct proc in proc.h
myproc()->tf -- so syscall() can get at call # and arguments
syscall() in syscall.c
looks at tf->eax to find out which system call
SYS_write in syscalls[] maps to sys_write
sys_write() in sysfile.c
arg*() read write(fd,buf,n) arguments from the user stack
argint() in syscall.c
proc->tf->esp + xxx
restoring user registers:
syscall() sets tf->eax to return value
back to trap()
finish -- returns to trapasm.S
info reg -- still in kernel, registers overwritten by kernel code
stepi to iret
info reg
most registers hold restored user values
eax has write() return value of 1
esp, eip, cs still have kernel values
x/5x $esp
saved user state: eip, cs, eflags, esp, ss
IRET pops those user registers from the stack
and thereby re-enters user space with CPL=3
摘要:
- 用户/内核转换的复杂设计
- 这个设计中的bug有多糟糕
- 内核必须采取用户进程的对抗视图
- 不信任用户堆栈
- 检查参数
- 页表限制了用户程序可以读/写的内存