MIT-JOS系列7:用户环境(三)

Part B:缺页异常,断点异常和系统调用

到目前位置我们以及你实现了内核基本的异常处理,现在要在此基础上利用异常处理进行系统调用。

处理缺页异常

缺页异常(page fault,中断向量14)是我们在本实验和往后会大量使用的一个重要例子。当缺页异常发生时,处理器将导致故障的线性地址(虚拟地址)存放在特殊寄存器cr2中。在trap.c中,提供了一个用于缺页异常的函数page_fault_handler()

修改trap_dispatch(),调用page_fault_handler()处理缺页异常

处理断点异常

断点异常(breakpoint,中断向量3)常被调试器用来在程序的断点处临时用int 3代替原来的语句。在JOS中我们会把这个异常作为一个伪系统调用以便任何用户环境都能调用到JOS的内核monitor。

修改trap_dispatch(),在断点异常时调用monitor()

这两节的代码实现如下:

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch (tf->tf_trapno)
    {
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        case T_BRKPT:
            monitor(tf);
            break;

        default:
            // Unexpected trap: The user process or the kernel has a bug.
            print_trapframe(tf);
            if (tf->tf_cs == GD_KT)
                panic("unhandled trap in kernel");
            else {
                env_destroy(curenv);
                return;
            }
    }
}

对于断点异常,为了能让他在用户态被正常调用,还要对其权限进行修改:

SETGATE(idt[T_BRKPT], 1, GD_KT, t_brkpt, 3);

否则,用户将无权调用int $3,在断点异常产生时将再产生一个异常protection fault(trap 13),就像上一个实验最后做的那样

系统调用

用户进程利用系统调用请求内核为它完成一些操作。当用户进程发起系统调用,处理器进入内核态,处理器和内核将保存当前用户进程的上下文状态,内核执行相应代码实现系统调用,然后返回继续执行用户进程的代码。用户如何向内核发起系统调用以及某个特定系统调用的具体用途在不同的操作系统中有很多不同的实现方式

在JOS系统中,我们使用int指令,它会出发一个处理器的中断。特别的,我们使用int $0x30作为系统调用

我们定义其中断向量为48(0x30),然后需要在中断向量表中初始化它的中断号和入口函数。这个中断不会被硬件触发

用户程序会通过寄存器向内核传递系统调用号和参数,这样内核就不需要从用户的堆栈或指令流中获取参数了

  • 系统调用号放在%eax
  • 参数(最多五个)分别放在`%edx, %ecx, %ebx, %edi, %esi`
  • 内核的返回值放在%eax

例如在hello.c中,跟进cprintf可以发现它是最终利用lib/syscall.c中的syscall(),将需要打印的字符串地址、字符串长度放在寄存器中传给内核,并传递系统调用号num告诉内核想要做什么,请求内核帮助完成打印。

补充代码完成系统调用:

  • trapentry.S中增加系统调用的入口

    TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)

  • trap.c中初始化IDT表

    SETGATE(idt[T_SYSCALL], 0, GD_KT, t_syscall, 3);

  • trap_dispatch()中对系统调用补充调用syscall()的代码,向它传递正确的参数,并正确接收返回值
  • 实现syscall(),为所有inc/syscall.h中的系统调用号实现系统调

trap_dispatch()的补充代码如下:

case T_SYSCALL:
    tf->tf_regs.reg_eax = syscall(tf->tf_regs.reg_eax,
                                  tf->tf_regs.reg_edx,
                                  tf->tf_regs.reg_ecx,
                                  tf->tf_regs.reg_ebx,
                                  tf->tf_regs.reg_edi,
                                  tf->tf_regs.reg_esi);
    break;

syscall()实现如下:

int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    // Call the function corresponding to the 'syscallno' parameter.
    // Return any appropriate return value.
    // LAB 3: Your code here.

    // panic("syscall not implemented");
    switch (syscallno) {
        case SYS_cputs:
            sys_cputs((char*)a1, a2);
            break;
        case SYS_cgetc:
            return sys_cgetc();
        case SYS_getenvid:
            return sys_getenvid();
        case SYS_env_destroy:
            return sys_env_destroy(a1);
        case NSYSCALLS:
        default:
            return -E_INVAL;
    }
    return 0;
}

用户模式启动

用户进程从lib/entry.S开始运行。在进行一些初始化后,调用lib/libmain.clibmain(),它设置用户环境指向当前环境,设置用户进程的名称,并调用umain()进入用户进程的主函数。修改libmain(),使

  • thisenv指向envs数组中的当前环境(lib/entry.S已经将envs指向了UENVS,这是之前做内存映射时envs数组映射的位置)
  • 提示:利用系统调用sys_getenvid()
  • ENVX根据envid计算出envenvs中的偏移

代码:thisenv = envs + ENVX(sys_getenvid());

之后,libmain()调用user/hello.c中的umain(),执行程序hello。在libmain()完整之前在qemu执行内核,打印“hello world”后发生了缺页异常,就是因为程序还没完善,hello.c中的thisenv->env_id指向0

缺页异常和内存保护

内存保护是操作系统中至关重要的一个特性,它保证一个进程中的错误不会破坏其他进程或系统内核。

操作系统通常依赖硬件支持来实现内存保护。操作系统始终能让硬件知道哪些虚拟地址是有效的、哪些是无效的,当进程需要访问无效的地址或它无权访问某地址时,操作系统会在导致错误的指令处终止程序,进入内核态,并将错误信息报告给内核。如果这个异常是可修复的,那么内核修复这个异常,程序继续执行;如果这个异常无法被修复,程序将被终止。

举一个可修复的错误的例子:考虑一个可自动扩展的堆栈。很多操作系统中,内核为用户程序分配单个页面作为栈区,如果程序想要访问这个栈区以外的空间而触发异常,操作系统自动为其分配更大的空间保证用户程序继续执行。通过这种做法,操作系统实际上只分配程序需要的栈内存量,但程序会觉得自己能在任意大的堆栈下执行。

系统调用也为内存保护带来问题:大部分系统调用接口让用户程序传递一个指针参数给内核。这些指针指向的是用户读写的缓冲区。通过这种方式,系统调用在执行时就可以读写这些指针指向的数据。但是这里有两个问题:

  • 在内核中发生的缺页异常可能比用户态下发生的缺页异常严重的多。如果内核在操纵它自己的数据结构时发生了缺页异常,这是一个内核BUG,异常处理程序应该终止整个系统的运行。但如果这个指针来自用户,操作系统就需要分辨出这个异常是在引用用户数据时造成的
  • 内核比用户拥有更高的访问权限。用户程序传递给内核的指针指向的数据可能是内核有权访问但用户无权访问的,因此内核必须小心读写指针指向的数据,防止将重要信息泄露给用户

针对这两点,我们之前写的中断处理和系统调用还存在一些问题:

  • 处理缺页异常时没有针对发生异常的是用户态还是内核态做特殊处理(利用tf_cs检测执行权)
  • 系统调用sys_cputs()中没有进行权限检测(实现user_mem_check()

接下来就需要完善这些函数保证系统调用的安全性

pmap.c中实现user_mem_check()

int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
    // LAB 3: Your code here.
    uintptr_t begin = ROUNDDOWN((uintptr_t)va, PGSIZE);
    uintptr_t end = begin + ROUNDUP(len, PGSIZE);
    uintptr_t *pte = NULL;
    while (begin < end) {
        pte = pgdir_walk(env->env_pgdir, (uintptr_t*)begin, 0);
        if (begin < ULIM && pte && (*pte & perm) == perm) {
            begin += PGSIZE;
            continue;
        } else {
            if (begin < (uintptr_t)va)
                user_mem_check_addr = (uintptr_t)va;
            else
                user_mem_check_addr = begin;
            return -E_FAULT;
        }
    }

    return 0;
}

这里由于将va向下4k对齐,因此需要检查begin < (uintptr_t)va,如果在va首地址处就发现权限不对,要正确返回va的地址而不是begin

page_fault_handler()中增加检查是否内核发生了缺页异常:

void
page_fault_handler(struct Trapframe *tf)
{
    uint32_t fault_va;

    // Read processor's CR2 register to find the faulting address
    fault_va = rcr2();

    // Handle kernel-mode page faults.

    // LAB 3: Your code here.
    if ((tf->tf_cs & 0x11) == 0)
        panic("kernel page fault at %x.\n", fault_va);
    // We've already handled kernel-mode exceptions, so if we get here,
    // the page fault happened in user mode.

    // Destroy the environment that caused the fault.
    cprintf("[%08x] user fault va %08x ip %08x\n",
        curenv->env_id, fault_va, tf->tf_eip);
    print_trapframe(tf);
    env_destroy(curenv);
}

sys_cputs()中检查用户是否具有权限读写相应内存:

static void
sys_cputs(const char *s, size_t len)
{
    // Check that the user has permission to read memory [s, s+len).
    // Destroy the environment if not.

    // LAB 3: Your code here.
    user_mem_assert(curenv, s, len, PTE_P);
    // Print the string supplied by the user.
    cprintf("%.*s", len, s);
}

原文地址:https://www.cnblogs.com/sssaltyfish/p/10705231.html

时间: 2024-08-03 06:03:54

MIT-JOS系列7:用户环境(三)的相关文章

MIT-JOS系列5:用户环境(一)

Part A:用户环境和异常处理 用户环境创建 本节中我们将实现一些内核的基本工具来支持受保护的用户进程的运行.我们将增加JOS内核的功能,为它增加一些数据结构来追踪用户进程的一些信息:创建一个单一用户的环境,并在其中加载运行一个程序.我们也会使JOS内核处理用户进程做出的任何系统调用和它导致的任何异常 内核利用ENV数据结构来记录每一个环境的信息.目前我们只创建单一的用户环境,以后再在此基础上设计多用户环境 在kern/env.c中,内核维护以下三个关于环境的全局变量: struct Env

Intellij Idea系列之Tomcat环境的搭建(三)

Intellij Idea系列之Tomcat环境的搭建(三) 一. 编写背景 Intellij Idea在刚上手的时候很多人吐槽,"god, 这么难用的IDE有谁用呀?",的确,Intellij在某些环境的搭建上对于初学者来说,是个费时费力的事.我身边的很多朋友和同事在学Intellij Idea的时候,一开始都是抱着高昂的热情来学习,在编写简单的代码的时候,都会由衷的感叹,果然比Eclipse好用,但是当他们学到tomcat环境的搭建的时候,大半都放弃了,不过那还是好几年前的事情了.

Android系列之网络(三)----使用HttpClient发送HTTP请求(分别通过GET和POST方法发送数据)

[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4006009.html 联系方式:[email protected] [系列]Android系列之网络:(持续更新) Android系列之网络(一)----使用HttpClient发送HTTP请求(通过get方法获取数据) Android系列之网络(二)----HTTP请求头与响应头 Android

MIT 操作系统实验 MIT JOS lab4

MIT JOS lab4 写在前面的碎碎念~ : 经历了LAB 3的洗礼,死磕到了lab 4. 这里还是首先向各位为JOS 实验做过笔记,写过博客,把自己实验代码托管到JOS上面的先行者们致敬! 如果没有这么好的开源环境, 这么好的东西学不来. 珍惜, 不用嘴. Doing is better than saying! -----------------------------------------------------------------------------------------

系统环境变量和用户环境变量

环境变量分为系统环境变量和用户环境变量. 你所说的环境变量是指系统环境变量,对所有用户起作用 而用户环境变量只对当前用户起作用. 例如你要用java,那么你把java的bin目录加入到path变量下面,那么它就是系统环境变量,所用用户登陆,在命令行输入java都会有java的帮助信息出来.而如果你在某个用户的变量下面新建一个变量,那么它就只对这个用户有用,当你以其他用户登陆时这个变量就和不存在一样. 这个问题在linux下面就相对好理解一些,系统变量都在/etc/profile文件里面,而用户的

MIT 操作系统实验 MIT JOS lab3

MIT JOS lab3 Allocating the Environments Array In lab 2, you allocated memory in  mem_init()  for the  pages[]  array, which is a table the kernel uses to keep track of which pages are free and which are not. You will now need to modify  mem_init()  

MIT 操作系统实验 MIT JOS lab2

MIT JOS lab2 在/kern/kdebug.c 里面会看到这段代码,关注下面的__STAB_BEGIN__ 那段代码 __STAB_BEGIN__   __STAB_END__相关定义在/kern/kernel.ld里面 下面我给出了kernel.ld的主要内容 ENTRY(_start) SECTIONS { /* Link the kernel at this address: "." means the current address */ . = 0xF0100000

用户环境变量

/etc/profile:此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置./etc/bashrc:为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取.~/.bash_profile:每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!默认情况下,他设置一些环境变量,执行用户的.bashrc文件.~/.bashrc:该文件包

Linux Shell系列教程之(三)Shell变量

本文是Linux Shell系列教程的第(三)篇,更多shell教程请看:Linux Shell系列教程 Shell作为一种高级的脚本类语言,也是支持自定义变量的.今天就为大家介绍下Shell中的变量相关知识. 为使Shell编程更有效,系统提供了一些Shell变量.Shell变量可以保存诸如路径名.文件名或者一个数字这样的变量名. Shell将其中任何设置都看做文本字符串.有两种变量,本地和环境.严格地说可以有 4种,但其余两种是只读的,可以认为是特殊变量,它用于向Shell脚本传递参数. 定