2023-02-15 16:21阅读: 162评论: 0推荐: 0

MIT6.828_JOS中断与系统调用

MIT6.828_JOS中断与系统调用

异常、中断、系统调用

异常和中断都是指"受保护的控制转移方法",都会使处理器从用户态转移为内核态

按照intel的定义,异常和中断的区别为:

中断是用来处理CPU外部的硬件请求(比如键盘、鼠标等),异常则用来处理CPU在执行过程中自己遇到的错误(比如除以零错误、缺页错误)。

Exceptions

  • Processor detected. These are further classified as faults, traps, and aborts.
  • Programmed. The instructions INTO, INT 3, INT n, and BOUND can trigger exceptions. These instructions are often called "software interrupts"(软件中断), but the processor handles them as exceptions.

x86体系的CPU中,异常的产生伴随着中断向量号的产生,以及系统调用也会产生中断信号,系统调用是一种特殊的异常。

因此我们可以用同一套硬件机制处理这三种事件(interrupt、system call 和 exception)

其中,来自硬件的中断被称作硬中断,其余两个则称作软中断

硬件相关

单核CPU时代,8259A这个PIC(Programmable Interrupt Controller,可编程中断控制器)用来处理外部中断绰绰有余,即时有再多的硬件中断需要处理,我们也可以通过级联的方式对IRQ进行扩充。

但是进入多核时代,问题就来了,哪个处理器处理哪个中断呢?显然单单PIC已经不够用了。

APIC(Advanced Programmable Interrupt Controller)应运而生。APIC分成两个部分,一部分为LAPIC,它集成在每一个CPU的内部,且LAPIC具有一个ID寄存器,能够唯一标识系统中的某个CPU;另一部分则是IOAPIC,它与外部设备连接,接收它们的中断信号。

图源

image-20221103120434595

有了APIC后,中断相应的总体流程为:IOAPIC接受外设中断,将信号发给系统中的每一个LAPIC,这个信号中包含某个LAPIC的ID。最后每个LAPIC判断信号中的ID与自己是否符合,如果符合那么就将其上报至CPU处理,如果不是则忽略这个信号。

值得一提的是,每个LAPIC可以向自己的CPU发送时钟中断,时钟中断是不需要经过IOAPIC这个中转站的。

中断处理程序定位

前面说过,无论是硬件中断、异常还是系统调用,都是用的同一套硬件机制,无论它们以什么形式触发,都会产生中断向量号,这个值是中断处理流程得以成功的基础。那我们就从产生中断向量号之后看起,下图概括了从中断向量号到定位中断服务程序的流程:

图源

image-20230213211011953

  1. 当CPU要处理一个中断时,必然已经获取了它的中断向量号,这个中断向量号将作为索引在IDT中获取一个表项。(那么怎么找到IDT呢--其线性地址在IDTR寄存器中,操作系统会设置它的值)。
  2. 获取到的表项实际上是一个叫做门描述符的数据结构,每个门描述符保存了一个选择子,这个选择子指向了GDT中的一个段描述符。
  3. 与虚拟地址与线性地址的转换类似,通过段描述符内存放的的段基址以及门描述符内存放的偏移地址,最终定位到了中断服务程序

接着,简略介绍一下其中涉及到的数据结构。

IDTR寄存器共48位,其中存放中断描述符表的线性地址以及这张表的界限,它们的关系如下图所示。

image-20230213211829841

中断描述符表IDT最多存放256个门描述符,前32个用作CPU内部异常,其余的用作软中断(int 指令可以产生)或者硬中断(由外部设备产生)

门描述符有3种,任务门、中断门、陷阱门。

图源

image-20221027115007838

每个门描述符有8字节,里面存放中断服务程序的偏移地址和段选择子,这两个的作用是为了在分页机制下产生中断服务程序入口的线性地址,从而定位中断服务程序。

门描述符还有一个Type字段来标记自己是中断、陷阱还是任务门,中断门和陷阱门几乎完全相似,任务门则很少使用。

中断门和陷阱门的最大的区别是:通过中断门进入中断处理流程时,CPU将自动把EFLAGS控制寄存器的IF标志位置0,也就是说在中断处理流程中时,禁止CPU转向处理其他可屏蔽中断。而通过陷阱门进入中断流程则没有这样的限制。

啥是可屏蔽\不可屏蔽中断?

不可屏蔽中断就是指CPU内部产生的异常,这个中断信号是无论如何CPU都要去响应的。可屏蔽中断是指硬件发出的中断信号。

值得一提的是,JOS使用的门描述符全都是中断门,是否能够说JOS不支持内核抢占呢?考虑到JOS是一个微内核,似乎这样的影响不大。xv6则除了系统调用使用陷阱门外,其余也都使用中断门。linux(2.4版本)的话,外部的硬件中断使用中断门,其余大部分也都是陷阱门包括系统调用。

此外门描述符还有一些控制信息如DPL,当用户程序试图通过门进入内核时,CPL(也就是当前CS寄存器的DPL) 必须小于等于门描述符的DPL。

中断栈帧的构建流程

操作系统理论中讲,当进入系统调用时,操作系统应该保存现场,从系统调用中返回时,则恢复现场。那么到底怎样保存\恢复现场,现场具体是个啥?这个过程只有操作系统参与了吗?

首先,现场环境指的是当前CPU各寄存器的值,包括8个通用寄存器,6个段寄存器,ip指示寄存器,eflags标志寄存器。“保护现场环境”就是在进入中断时,将这些寄存的值push到内核栈上,形成了中断栈帧(TrapFrame):

image-20221027222725178

而“恢复现场”就是在退出中断时将这些保存的值pop到各寄存器中。

你可能会对上图的cpu->ts.esp0有疑问,为什么ss esp只有在特权级发生变化时才会被压入?

中断就是一种执行状态的改变,当我们从用户态转向内核态或者从内核态转向用户态时,CPU使用的栈是不一样的,也就是说esp的值是不同。但是如果没有涉及特权级变化,通常在内核态响应中断的情景下,cpu原本就在使用内核栈,因此不用做栈切换,

当内核态返回至用户态时,硬件自动帮我们在内核栈的trapframe中记录了用户栈的栈指针esp和栈选择子ss,直接使用iret指令自动切换返回用户栈。

但是从用户态进入内核态时,用户怎么知道内核栈在哪里呢?不,为了安全考虑,用户不能知道内核栈。但是硬件知道它有个TR寄存器,TR寄存器存放着TSS段选择子,根据 TSS 段选择子去 GDT 中索引 TSS 段描述符,从中获取 TSS ,而TSS中就存放着内核栈的ss和esp指针,示意图见下。因此中断进入内核态时,硬件将自动根据其TR寄存器把TSS中对应CPU的esp0加载到esp寄存器(就在这时完成了内核栈的切换),然后自动在内核栈保存原来的esp(即用户栈的栈顶地址)。

image-20230214195438545

那么TSS中初始的esp0、ss0内容由谁来设立,硬件如何知道TSS的位置信息?--- 这都是操作系统的功劳,我们将在下一节看到详细的代码细节。

总而言之,操作系统和硬件“通力合作”,在发生中断时,保证了整个系统的安全性。

现在再回到中断的压栈流程,从上面内核栈的图示我们可以将压栈流程分为3步。

先在这说明,中断处理程序在逻辑上分为两段,一为中断入口程序,二为正真的中断处理逻辑,下面我将称之为“实中断处理程序”。

  1. 中断发生,当通过特权级检查时,硬件自动压入ss、esp、cs:ip、eflags
  2. 硬件通过中断向量在IDT中索引其对应的中断门描述符表,找到了中断入口服务程序。中断入口程序压入错误号和中断向量号,其中中断向量号唯一标识一种内部中断处理程序。
  3. 入口程序调用_altrap()将剩余的寄存器现场保存在trapframe上。最后调用trap路由到实中断处理程序上

代码分析

首先,是涉及到的各种数据结构的初始化操作。IDT、GD这两个数据结构在初始化后分别将它们的地址加载到IDTR和GDTR寄存器中。

之前在MIT6.828_JOS启动流程提过,这两个数据结构在启动时都是临时的,临时IDT是BIOS建立的,临时GDT则是在内核启动代码的entry.S建立的,它们都在低地址处。现在,我们需要重建这两个数据结构,使其移动到内核的高地址处。

GDT的构建代码在env.c文件中,这个文件其实涉及到了进程创建的内容,但对这里的分析影响不大。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
struct Segdesc gdt[NCPU + 5] = { // 0x0 - unused (always faults -- for trapping NULL far pointers) SEG_NULL, // 0x8 - kernel code segment [GD_KT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 0), // 0x10 - kernel data segment [GD_KD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 0), // 0x18 - user code segment [GD_UT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 3), // 0x20 - user data segment [GD_UD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 3), [GD_TSS0 >> 3] = SEG_NULL }; struct Pseudodesc gdt_pd = { sizeof(gdt) - 1, (unsigned long) gdt };

struct Segdesc gdt[]这个数组一共有NCPU + 5项,其中NCPU为系统的CPU数量,前五项在定义数组时已经固定,后NCPU个项由各CPU自行设置(见下文trap_init_percpu函数)。

env_init_percpu函数则将其载入GDTR寄存器中:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
void env_init_percpu(void) { lgdt(&gdt_pd); // 加载全局描述符表 // .... }

idt的构建代码在trap.c文件中

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
struct Gatedesc idt[256] = { { 0 } }; void trap_init(void) { extern struct Segdesc gdt[]; // SETGATE(idt向量, 是否是陷阱门,选择子,中断处理函数指针, 特权级) // 设置CPU的内部中断,前32个向量都是留给CPU自己用的,外设的中断应该偏移32长度 SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_handler, 0); SETGATE(idt[T_DEBUG], 0, GD_KT, debug_handler, 0); SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0); SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 3); SETGATE(idt[T_OFLOW], 0, GD_KT, oflow_handler, 0); SETGATE(idt[T_BOUND], 0, GD_KT, bound_handler, 0); SETGATE(idt[T_ILLOP], 0, GD_KT, illop_handler, 0); SETGATE(idt[T_DEVICE], 0, GD_KT, device_handler, 0); SETGATE(idt[T_DBLFLT], 0, GD_KT, dblflt_handler, 0); SETGATE(idt[T_TSS], 0, GD_KT, tss_handler, 0); SETGATE(idt[T_SEGNP], 0, GD_KT, segnp_handler, 0); SETGATE(idt[T_STACK], 0, GD_KT, stack_handler, 0); SETGATE(idt[T_GPFLT], 0, GD_KT, gpflt_handler, 0); SETGATE(idt[T_PGFLT], 0, GD_KT, pgflt_handler, 0); SETGATE(idt[T_FPERR], 0, GD_KT, fperr_handler, 0); SETGATE(idt[T_ALIGN], 0, GD_KT, align_handler, 0); SETGATE(idt[T_MCHK], 0, GD_KT, mchk_handler, 0); SETGATE(idt[T_SIMDERR], 0, GD_KT, simderr_handler, 0); // 系统调用中断,系统调用号T_SYSCALL = 0x30 SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3); // 外部中断设置,偏移32个长度。常用的有 IDE、KBD、TIMER,分别表示来自磁盘、键盘、时钟的中断 SETGATE(idt[IRQ_OFFSET+IRQ_ERROR], 0, GD_KT, irq_error_handler, 3); SETGATE(idt[IRQ_OFFSET+IRQ_IDE], 0, GD_KT, irq_ide_handler, 3); SETGATE(idt[IRQ_OFFSET+IRQ_KBD], 0, GD_KT, irq_kbd_handler, 3); SETGATE(idt[IRQ_OFFSET+IRQ_SERIAL], 0, GD_KT, irq_serial_handler, 3); SETGATE(idt[IRQ_OFFSET+IRQ_SPURIOUS], 0, GD_KT, irq_spurious_handler, 3); //spurious假的,伪造的 SETGATE(idt[IRQ_OFFSET+IRQ_TIMER], 0, GD_KT, irq_timer_handler, 3); // Per-CPU setup trap_init_percpu(); }

trap_init函数将IDT数据结构构建出来,IDT项的偏移地址实际上存储的是函数的地址,指向中断处理程序。再调用trap_init_percpu()函数将其地址载入IDTR寄存器中

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
void trap_init_percpu(void) { // ... // Load the IDT lidt(&idt_pd); }

为了设置CPU的内核栈,我们也需要初始化TSS结构。TSS结构在JOS内核的struct CpuInfo cpus[NCPU]中,这个结构在OS启动时由多核启动相关代码初始化。CpuInfo具有TSS结构的字段,但是该字段的初始化也在trap_init_percpu函数中。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
// Per-CPU state struct CpuInfo { uint8_t cpu_id; // Local APIC ID; index into cpus[] below volatile unsigned cpu_status; // The status of the CPU struct Env *cpu_env; // The currently-running environment. struct Taskstate cpu_ts; // TSS结构体 }; // struct Taskstate { uintptr_t ts_esp0; // Stack pointers and segment selectors uint16_t ts_ss0; // after an increase in privilege level uint16_t ts_iomb; // I/O map base address // 还有不同的特权级 esp1 esp2 等,当JOS只用esp0 // 。。。 };

完整trap_init_percpu代码如下所示:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
void trap_init_percpu(void) { // 设置Tss的esp ss thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - thiscpu->cpu_id * (KSTKSIZE + KSTKGAP); thiscpu->cpu_ts.ts_ss0 = GD_KD; thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate); // 在GDT中设置后NCPUS个的段描述符 gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id] = SEG16(STS_T32A, (uint32_t) (&(thiscpu->cpu_ts)), sizeof(struct Taskstate) - 1, 0); gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id].sd_s = 0; // 加载TR寄存器 ltr(GD_TSS0 + (thiscpu->cpu_id<<3)); // Load the IDT lidt(&idt_pd); }

trap_init_percpu调用处理完GDT后,处理GDT的第0个项,其他项都已设置完全。

然后,看看实际的中断服务程序是啥,记住中断服务程序逻辑上分为入口程序和正真执行中断服务功能的代码。这里以缺页中断为例

主要代码在trapentry.S文件中:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
#define TRAPHANDLER(name, num) .globl name; /* 定义中断服务程序名称 */ .type name, @function; /* 符号种类为函数*/ .align 2; /* 对齐*/ name: /* 函数定义从这里开始 */ pushl $(num); jmp _alltraps /* 调用alltraps */ TRAPHANDLER(gpflt_handler, T_GPFLT)/* 使用宏定义中断处理函数 */ .global _alltraps _alltraps : pushl %ds pushl %es pushal movl $GD_KD, %edx movl %edx, %ds movl %edx, %es pushl %esp call trap

可以看到通过宏我们定义了gpflt_handler这个函数:

copy
  • 1
  • 2
  • 3
gpflt_handler: pushl $(num); /* 压入中断向量号*/ jmp _alltraps /* 调用alltraps */

仅仅是压入中断向量号然后调用alltraps即可,那错误码呢? 对于缺页中断,错误码由CPU自动压入。对于那些不会自动压入错误码的异常,通常的做法是手动压入错误码0,达成形式上的统一。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
#define TRAPHANDLER_NOEC(name, num) .globl name; .type name, @function; .align 2; name: pushl $0; /* 手动压入错误码0 */ pushl $(num); jmp _alltraps

gpflt_handler调用_alltraps,压入其余的寄存器后调用trap(),trap()则根据trapframe中的中断向量号,转入正真的缺页处理函数page_fault_handler中:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
void trap(struct Trapframe *tf) { // ... curenv->env_tf = *tf; // 将内核栈中的trapframe内容拷贝到struct cpu info的env_tf属性 tf = &curenv->env_tf; // 改变tf的指向,以便后序程序对中断栈帧的修改。 // ... trap_dispatch(tf); // ... } static void trap_dispatch(struct Trapframe *tf) { // Handle processor exceptions. int ret ; switch(tf->tf_trapno){ case T_PGFLT: page_fault_handler(tf); break; case ... case ... //... }

图解缺页中断流程

用户环境下产生PageFault异常举个例子,我会在图中记录栈的变化

  1. 硬件发现某个PDE或者PTE的PTE_P标志为0,产生PageFault异常,PageFault的向量号是13 , 硬件找到IDT中的第14个元素(向量号13)

    image-20230215113910321

  2. 硬件根据门描述符进行特权级检查,如果通过,则根据TR获得TSS中的esp0赋值给esp寄存器(此时完成栈的切换),并在内核栈中push 原来用户态的esp和ss

    image-20230215120304822

  3. IDT[13]中得到gpflt_handler的地址,栈中压入eflags、eip、cs。控制流转到pgflt_handler(即EIP寄存器指向pgflt_handler的入口地址),这里已经正式进入内核态,压入中断向量号和错误码。

    image-20230215120348991

  4. 调用_alltrap(),继续构建trapframe

    image-20230215142722001

  5. 将trapframe的地址压栈当作参数调用trap(*tf)

  6. trap函数做一些处理后调用trap_dispatch(*tf),检测tf中的trap_no, 发现这次引起中断的原因是pagefault

  7. 最后调用page_fault_handler()处理缺页异常

    copy
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    static void trap_dispatch(struct Trapframe *tf) { int ret ; switch(tf->tf_trapno){ case T_PGFLT: page_fault_handler(tf); break; }

具体的缺页异常处理函数page_fault_handler,之后的文章再补充。

而且有意思的是,JOS把缺页处理的流程放在了用户态,从这点可以隐约感到JOS的内核架构不是宏内核架构

系统调用的实现

有了前面的基础,系统调用的实现看起来就简单很多了,因为无论异常、硬件中断还是系统调用,用的都是同一套硬件机制。

系统调用与异常相比,要考虑一下几个点:

  • linux的系统调用数目超过了256个,如果一个系统调用对应一个中断向量号的话,显然是不够的。通常的解决办法是,让系统调用共用一个中断向量号,并使用系统调用号区分系统用。中断向量号对应syscall_hander,syscall_hander相当于“实”中断处理程序;在syscall_handler中,再根据系统调用号在syscalltable中找到对应的系统调用跳转执行。
  • 那么系统调用的系统调用号用什么传递?系统调用还有其他参数,比如printf("%s","hello")调用,涉及两个参数怎么传递?
    • 系统调用号用寄存器eax传递, 而且系统调用返回值也用eax存放
    • 系统调用的其他参数的传递有两种做法
      1. 使用寄存器传递
      2. 使用用户栈传递
    • JOS使用一种方法,xv6使用第二种方法,linux混用
      • 第一种方法的优点:快,寄存器直接存储参数比内存寻址块
      • 使用第一种方法时,寄存器数量是有限的,比如linux及jos中最多有5个参数通过寄存器传递。如果有6个或者更多那怎么传递?JOS未对这种情况做处理,因此JOS只实现了参数小于等于5的系统调用;linux中则将多余的参数使用用户栈传递。

接下来将从一段用户程序开始,从顶至上分析系统调用的流程。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
// hello, world #include <inc/lib.h> void umain(int argc, char **argv) { cprintf("hello, world\n"); cprintf("i am environment %08x\n", thisenv->env_id); }

关于cprintf

用户函数库定义了cprintf(), kern也定义了cprintf()函数;这里的cprintf显然是用户lib下的函数定义,最后会发起一个系统调用请求内核操作显存;但kern下的print就不一样了,它本身就在内核态运行,直接操作显存即可。

任何一个操作系统的print函数都很复杂,但在这里我们只需要知道它最后调用sys_cputs(const char *s, size_t len)就可以了,该函数定义在lib/syscall.c文件中,其中的sys_xxx函数都是用户进行系统调用的封装函数。

这里主要关注sys_cputs函数,它仅仅对syscall进行了封装,而syscall使用内联汇编的方式发起 int n指令,产生中断信号,代码如下:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
// lib/syscall.c void sys_cputs(const char *s, size_t len) { syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0); } static inline int32_t syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) { int32_t ret; asm volatile("int %1\n" // int 指令产生中断信号 : "=a" (ret) // =号 指定一个输出寄存器,这里是eax : "i" (T_SYSCALL), // 系统调用的向量号,作为上面int 指令的参数 "a" (num), // 输入寄存器eax存放num "d" (a1), // 下面同 "c" (a2), "b" (a3), "D" (a4), "S" (a5) : "cc", "memory"); // cc告诉GCC我们改变了flags 寄存器; //Using the "memory" clobber effectively forms a read/write memory barrier for the compiler. if(check && ret > 0) panic("syscall %d returned %d (> 0)", num, ret); return ret; }

发起中断信号后的流程和上一节一样,在构建trapframe的过程中,系统调用的参数一个一个地保存在这个结构中。我们直接跳转到中断流程的trap_dispatch中,它识别到tf_trapno是SYS_call, 因此把参数拿出来然后调用syscall,注意这里的syscall是内核代码,虽与syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)函数同名,但是特权级不同。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
static void trap_dispatch(struct Trapframe *tf) { int ret ; switch(tf->tf_trapno){ case T_PGFLT: // 缺页中断 page_fault_handler(tf); break; case T_SYSCALL: // 系统调用 ret = syscall(tf->tf_regs.reg_eax, // 取出trapframe中的参数并调用syscall 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); tf->tf_regs.reg_eax = ret;// 设置中断返回值 break; default: ... } }

内核的syscall,依据传递过来的系统调用号,执行相应的系统调用,JOS共实现了15个系统调用,我们这里关注 sys_cputs。sys_cputs最后调用内核的cprintf打印字符,这很简单。但是在这之上有一个user_mem_assert。这样做的目的是什么? ————答案还是“安全”,如果从用户传递过来的参数,操作系统不加以检查,很容易造成系统漏洞。

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
// inc\syscall.h enum { // JOS一共有15个系统调用号 SYS_cputs = 0, SYS_cgetc, SYS_getenvid, SYS_env_destroy, SYS_page_alloc, SYS_page_map, SYS_page_unmap, SYS_exofork, SYS_env_set_status, SYS_env_set_trapframe, SYS_env_set_pgfault_upcall, SYS_yield, SYS_ipc_try_send, SYS_ipc_recv, NSYSCALLS }; // kern\syscall.c int32_t syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) { switch (syscallno) { // 根据系统调用好调用不同的处理函数 case SYS_cputs: sys_cputs((const char*)a1, (size_t)a2); return 0; case SYS_cgetc : ... default: return -E_INVAL; } } static void sys_cputs(const char *s, size_t len) { user_mem_assert(curenv,s,len,PTE_U); // Print the string supplied by the user. cprintf("%.*s", len, s); }

user_mem_assert检查用户是否有它传递参数的相应访问权限?课程lab给出的三个用户程序,它们的参数都是不合理的,经过user_mem_assert检验后,否决了这些程序的运行:

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
// buggy_hello.c void umain(int argc, char **argv) { // 虚拟地址1, 不会映射到用户区域!用户没有权限! sys_cputs((char*)1, 1); } // buggy_hello2.c const char *hello = "hello, world\n"; void umain(int argc, char **argv) { // hello的虚拟地址 + 1024 * 1024 > data和text段的虚拟地址,而大于的这部分是还没有映射给用户程序的。所以还是没有权限。 sys_cputs(hello, 1024*1024); // 字符串长度设定为1024*1024 } // evilhello.c void umain(int argc, char **argv) { // 没啥好说的,0xf010000c > ULIM, 用户程序企图打印内核的代码段,这是绝对不允许的。 sys_cputs((char*)0xf010000c, 100); }

其实系统调用的整体流程与异常是差不多的,只不过中断信号的产生不同,且系统调用号多了一层路由而已。

image-20230215161528698

本文作者:别杀那头猪

本文链接:https://www.cnblogs.com/HeyLUMouMou/p/17123520.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   别杀那头猪  阅读(162)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起