You've successfully subscribed to The Daily Awesome
Great! Next, complete checkout for full access to The Daily Awesome
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.
MIT6.828 | Lec 5: Isolation mechanisms

MIT6.828 | Lec 5: Isolation mechanisms

. 6 min read

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

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指令:

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

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有多糟糕
  • 内核必须采取用户进程的对抗视图
  • 不信任用户堆栈
  • 检查参数
  • 页表限制了用户程序可以读/写的内存