MIT6.828 | Lec 5: Isolation mechanisms

LINK

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.asmwrite()库函数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
break *0xd42
x/3i
	0x10 in eax is the system call number for write
info reg
    cs=0x1b, B=1011 -- CPL=3 => user mode
    esp and eip are low addresses -- user virtual addresses
x/4x $esp
    ebf is return address -- in printf
    2 is fd
    0x3f7a is buffer on the stack
    1 is count
    i.e. write(2, 0x3f7a, 1)
x/c 0x3f7a

内核入口,INT指令:

1
2
3
4
stepi
info reg
	cs=0x8 -- CPL=3 => kernel mode

INTeipesp 改变至高内核地址

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
x/19x $esp #   19 words at top of kernel stack:
    ss
    esp
    eflags
    cs
    eip
    err    -- INT saved from here up
    trapno
    ds
    es
    fs
    gs
    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有多糟糕
  • 内核必须采取用户进程的对抗视图
  • 不信任用户堆栈
  • 检查参数
  • 页表限制了用户程序可以读/写的内存