ucore lab1

来源:互联网 发布:沙盘软件多开器 编辑:程序博客网 时间:2024/06/06 00:54

[练习1]

[练习1.1] 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

输入make V= (对每条命令进行了精简)

+ cc kern/init/init.c           //编译init.c      gcc -c kern/init/init.c -o obj/kern/init/init.o+ cc kern/libs/readline.c       //编译readline.c      gcc -c kern/libs/readline.c -o       obj/kern/libs/readline.o+ cc kern/libs/stdio.c          //编译stdlio.c      gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o+ cc kern/debug/kdebug.c        //编译kdebug.c      gcc -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o+ cc kern/debug/kmonitor.c      //编译komnitor.c      gcc  -c kern/debug/kmonitor.c -o               obj/kern/debug/kmonitor.o+ cc kern/debug/panic.c         //编译panic.c      gcc  -c kern/debug/panic.c -o obj/kern/debug/panic.o+ cc kern/driver/clock.c        //编译clock.c      gcc  -c kern/driver/clock.c -o obj/kern/driver/clock.o+ cc kern/driver/console.c      //编译console.c      gcc -c kern/driver/console.c -o       obj/kern/driver/console.o+ cc kern/driver/intr.c         //编译intr.c      gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o+ cc kern/driver/picirq.c       //编译prcirq.c      gcc -c kern/driver/picirq.c -o       obj/kern/driver/picirq.o+ cc kern/trap/trap.c           //编译trap.c      gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o+ cc kern/trap/trapentry.S      //编译trapentry.S      gcc -c kern/trap/trapentry.S -o       obj/kern/trap/trapentry.o+ cc kern/trap/vectors.S        //编译vectors.S      gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o+ cc kern/mm/pmm.c              //编译pmm.c      gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o+ cc libs/printfmt.c            //编译printfmt.c      gcc -c libs/printfmt.c -o obj/libs/printfmt.o+ cc libs/string.c              //编译string.c      gcc -c libs/string.c -o obj/libs/string.o+ ld bin/kernel                 //链接成kernel      ld -o bin/kernel        obj/kern/init/init.o      obj/kern/libs/readline.o       obj/kern/libs/stdio.o     obj/kern/debug/kdebug.o       obj/kern/debug/kmonitor.o obj/kern/debug/panic.o       obj/kern/driver/clock.o   obj/kern/driver/console.o       obj/kern/driver/intr.o    obj/kern/driver/picirq.o      obj/kern/trap/trap.o      obj/kern/trap/trapentry.o       obj/kern/trap/vectors.o   obj/kern/mm/pmm.o        obj/libs/printfmt.o       obj/libs/string.o+ cc boot/bootasm.S             //编译bootasm.c     gcc  -c boot/bootasm.S -o obj/boot/bootasm.o+ cc boot/bootmain.c            //编译bootmain.c     gcc -c boot/bootmain.c -o obj/boot/bootmain.o+ cc tools/sign.c               //编译sign.c    gcc -c tools/sign.c -o obj/sign/tools/sign.o    gcc -O2 obj/sign/tools/sign.o -o bin/sign+ ld bin/bootblock              //根据sign规范生成bootblock    ld -m  elf_i386 -nostdlib -N -e start -Ttext 0x7C00     obj/boot/bootasm.o  obj/boot/bootmain.o    -o obj/bootblock.o     //创建大小为10000个块的ucore.img,初始化为0,每个块为512字节dd if=/dev/zero of=bin/ucore.img count=10000    //把bootblock中的内容写到第一个块dd if=bin/bootblock of=bin/ucore.img conv=notrunc    //从第二个块开始写kernel中的内容dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

从上面并没有看到:根据sign规范生成bootblock的命令

查看makefile文件找到:

@$(call totarget,sign) $(call outfile,bootblock)         $(bootblock)

所以从上面可以看出ucore.img的生成过程:

1 编译所有生成bin/kernel所需的文件2 链接生成bin/kernel3 编译bootasm.S  bootmain.c  sign.c 4 根据sign规范生成obj/bootblock.o5 生成ucore.img

[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

截取sign.c文件中的部分源码

    char buf[512];  //定义buf数组    memset(buf, 0, sizeof(buf));      // 把buf数组的最后两位置为 0x55, 0xAA    buf[510] = 0x55;      buf[511] = 0xAA;    FILE *ofp = fopen(argv[2], "wb+");    size = fwrite(buf, 1, 512, ofp);    if (size != 512) {       //大小为512字节        fprintf(stderr, "write '%s' error,                          size is %d.\n", argv[2], size);        return -1;    }

主引导扇区的规则如下:

1 大小为512字节2 多余的空间填03510个(倒数第二个)字节是0x55,4511个(倒数第一个)字节是0xAA。

[练习2]

[练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。

修改lab1/tools/gdbinit ,内容为:

set architecture i8086target remote :1234

然后在 lab1执行:

make debug

在gdb的调试界面,执行如下命令:

si

来单步跟踪

在gdb的调试界面,执行如下命令,来查看BIOS代码:

 x /2i $pc  //显示当前eip处的汇编指令

得到下面的截图:

这里写图片描述

[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常

修改 gdbinit文件:

set architecture i8086target remote :1234b *0x7c00cx/2i $pc

得到如下结果,断点正常
这里写图片描述

[练习2.3] 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

改写 makefile文件:

debug: $(UCOREIMG)        $(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D  $(BINDIR)/q.log -parallel stdio -hda $< -serial null"        $(V)sleep 2        $(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

然后再执行

make debug

得到q.log文件:

这里写图片描述

查看bootasm.S文件:

这里写图片描述

查看 bootblock.asm文件:

这里写图片描述

从上面的结果可以看到:

bootasm.S文件中的代码和bootblock.asm是一样的,对于q.log文件,断点之后的代码和bootasm.S,bootblock.asm是一样的。

[练习2.4] 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

修改gdbinit文件,在0x7c4a处设置断点 (调用bootmain函数处)

set architecture i8086target remote :1234break *0x7c4a

输入 make debug ,得到结果:

这里写图片描述

断点设置正常

练习3:分析bootloader进入保护模式的过程。

1 关中断和清除数据段寄存器

.globl startstart:.code16                                                 cli              //关中断                              cld              //清除方向标志                               xorw %ax, %ax    //ax清0                               movw %ax, %ds    //ds清0                                    movw %ax, %es    //es清0                                   movw %ax, %ss    //ss清0                             

[练习3.1] 为何开启A20,以及如何开启A20?

初始时A20为0,访问超过1MB的地址时,就会从0循环计数,将A20地址线置为1之后,才可以访问4G内存。A20地址位由8042控制,8042有2个有两个I/O端口:0x60和0x64。

打开流程:

  1. 等待8042 Input buffer为空;
  2. 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
  3. 等待8042 Input buffer为空;
  4. 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer;
seta20.1:            //等待8042键盘控制器不忙    inb $0x64, %al   //从0x64端口中读入一个字节到al中               testb $0x2, %al  //测试al的第2位    jnz seta20.1     //al的第2位为0,则跳出循环    movb $0xd1, %al  //将0xd1写入al中                             outb %al, $0x64  //将0xd1写入到0x64端口中                          seta20.2:            //等待8042键盘控制器不忙    inb $0x64, %al   //从0x64端口中读入一个字节到al中               testb $0x2, %al  //测试al的第2位    jnz seta20.2     //al的第2位为0,则跳出循环    movb $0xdf, %al  //将0xdf入al中                             outb %al, $0x60  //将0xdf入到0x64端口中,打开A20                  

[练习3.2] 如何初始化GDT表?

1 载入GDT表

 lgdt gdtdesc       //载入GDT表

2 进入保护模式:

通过将cr0寄存器PE位置1便开启了保护模式

cro的第0位为1表示处于保护模式

movl %cr0, %eax       //加载cro到eaxorl $CR0_PE_ON, %eax  //将eax的第0位置为1movl %eax, %cr0       //将cr0的第0位置为1

3 通过长跳转更新cs的基地址:

​ 上面已经打开了保护模式,所以这里需要用到逻辑地址。$PROT_MODE_CSEG的值为0x80

ljmp $PROT_MODE_CSEG, $protcseg.code32                          protcseg:

4 设置段寄存器,并建立堆栈

 movw $PROT_MODE_DSEG, %ax //                       movw %ax, %ds                                   movw %ax, %es                                    movw %ax, %fs                                    movw %ax, %gs                                    movw %ax, %ss                                    movl $0x0, %ebp  //设置帧指针 movl $start, %esp  //设置栈指针

5 转到保护模式完成,进入boot主方法

call bootmain //调用bootmain函数

[练习3.3] 如何使能和进入保护模式

将cr0寄存器置1

[练习4] 分析bootloader加载ELF格式的OS的过程。

[练习4.1] bootloader如何读取硬盘扇区的?

​ 读取扇区硬盘的代码:

bootloader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。
在上一个联系中我们的BootLoader已经成功的进入了保护模式,接下来我们要做的就是从硬盘读取并运行我们的OS。对于硬盘来说,我们知道是分成许多扇区的其中每个扇区的大小为512字节。读取扇区的流程我们通过查询指导书可以看到:
1、等待磁盘准备好;
2、发出读取扇区的命令;
3、等待磁盘准备好;
4、把磁盘扇区数据读到指定内存。
接下来我们需要了解下如何具体的从硬盘读取数据,因为我们所要读取的操作系统文件是存在0号硬盘上的,所以,我们来看一下关于0号硬盘的I/O端口:
这里写图片描述

static voidwaitdisk(void) { //如果0x1F7的最高2位是01,跳出循环    while ((inb(0x1F7) & 0xC0) != 0x40)        /* do nothing */;}/* readsect - read a single sector at @secno into @dst */static voidreadsect(void *dst, uint32_t secno) {    // wait for disk to be ready    waitdisk();    outb(0x1F2, 1);        //读取一个扇区    outb(0x1F3, secno & 0xFF);  //要读取的扇区编号    outb(0x1F4, (secno >> 8)&0xFF);//用来存放读写柱面的低8位字节     outb(0x1F5, (secno >> 16)&0xFF);//用来存放读写柱面的高2位字节          // 用来存放要读/写的磁盘号及磁头号    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);    outb(0x1F7, 0x20);       // cmd 0x20 - read sectors    // wait for disk to be ready    waitdisk();    // read a sector    insl(0x1F0, dst, SECTSIZE / 4); //获取数据}

一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的,具体参数见下表。一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
readseg简单包装了readsect,可以从设备读取任意长度的内容。

static void    readseg(uintptr_t va, uint32_t count, uint32_t offset) {        uintptr_t end_va = va + count;        va -= offset % SECTSIZE;        uint32_t secno = (offset / SECTSIZE) + 1;         // 加1因为0扇区被引导占用        // ELF文件从1扇区开始        for (; va < end_va; va += SECTSIZE, secno ++) {            readsect((void *)va, secno);        }    }

[练习4.2] bootloader是如何加载ELF格式的OS?

ELF定义:

/* file header */struct elfhdr {    uint32_t e_magic;     // must equal ELF_MAGIC    uint8_t e_elf[12];    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image    uint16_t e_machine;   // 3=x86, 4=68K, etc.    uint32_t e_version;   // file version, always 1    uint32_t e_entry;     // entry point if executable    uint32_t e_phoff;     // file position of program header or 0    uint32_t e_shoff;     // file position of section header or 0    uint32_t e_flags;     // architecture-specific flags, usually 0    uint16_t e_ehsize;    // size of this elf header    uint16_t e_phentsize; // size of an entry in program header    uint16_t e_phnum;     // number of entries in program header or 0    uint16_t e_shentsize; // size of an entry in section header    uint16_t e_shnum;     // number of entries in section header or 0    uint16_t e_shstrndx;  // section number that contains section name strings};
在这里我们只需要关注其中的几个参数,e_magic,是用来判断读出来的ELF格式的文件是否为正确的格式;e_phoff,是program header表的位置偏移;e_phnum,是program header表中的入口数目;e_entry,是程序入口所对应的虚拟地址。

宏定义:

#define ELFHDR          ((struct elfhdr *)0x10000) #define SECTSIZE        512

在bootmain函数中,

    void    bootmain(void) {        // 首先读取ELF的头部        readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);        // 通过储存在头部的幻数判断是否是合法的ELF文件        if (ELFHDR->e_magic != ELF_MAGIC) {            goto bad;        }        struct proghdr *ph, *eph;        // ELF头部有描述ELF文件应加载到内存什么位置的描述表,        // 先将描述表的头地址存在ph        ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);        eph = ph + ELFHDR->e_phnum;        // 按照描述表将ELF文件中数据载入内存        for (; ph < eph; ph ++) {            readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);        }        // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000        // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000        // 根据ELF头部储存的入口信息,找到内核的入口        ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();    bad:        outw(0x8A00, 0x8A00);        outw(0x8A00, 0x8E00);        while (1);    }

总结一下就是:

  1. 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用;
  2. 校验e_magic字段;
  3. 根据偏移量分别把程序段的数据读取到内存中。

[练习5]:实现函数调用堆栈跟踪函数

首先,可以通过read_ebp()read_eip()函数来获取当前ebp寄存器和eip 寄存器的信息。
然后通过ebp+12,ebp+16,ebp+20,ebp+24来输出4个参数的值,最后更新ebp:ebp=ebp[0],更新eip:eip=ebp[1]。直到ebp 对应地址的值为0(表示当前函数为bootmain)。

read_eip() 函数定义在kdebug.c中:

static __noinline uint32_tread_eip(void) {    uint32_t eip;    asm volatile("movl 4(%%ebp), %0" : "=r" (eip)); //内联汇编,读取(ebp-4)的值到变量eip    return eip;  //返回eip的值}

read_ebp() 函数定义在x86.h中:

static inline uint32_tread_ebp(void) {    uint32_t ebp;    asm volatile ("movl %%ebp, %0" : "=r" (ebp));  //内联汇编,读取edp寄存器的值到变量ebp    return ebp;  //返回ebp的值}

​ 实现函数如下:

voidprint_stackframe(void) {    uint32_t ebp = read_ebp(), eip = read_eip();  //获取ebp和eip的值    int i, j;    //#define STACKFRAME_DEPTH 20    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);         uint32_t *args = (uint32_t *)ebp + 2; //参数的首地址        for (j = 0; j < 4; j ++) {            cprintf("0x%08x ", args[j]); //打印4个参数        }        cprintf("\n");        print_debuginfo(eip - 1);  //打印函数信息        eip = ((uint32_t *)ebp)[1]; //更新eip        ebp = ((uint32_t *)ebp)[0]; //更新ebp    }}

执行 make qemu 得到输出:

这里写图片描述

最后一行的解释:

其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。(因为此时ebp对应地址的值为0)
bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。

[练习6]完善中断初始化和处理

[练习6.1]中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

一个表项的结构如下:

/*lab1/kern/mm/mmu.h*//* Gate descriptors for interrupts and traps */struct gatedesc {    unsigned gd_off_15_0 : 16;        // low 16 bits of offset in segment    unsigned gd_ss : 16;            // segment selector    unsigned gd_args : 5;            // # args, 0 for interrupt/trap gates    unsigned gd_rsv1 : 3;            // reserved(should be zero I guess)    unsigned gd_type : 4;            // type(STS_{TG,IG32,TG32})    unsigned gd_s : 1;                // must be 0 (system)    unsigned gd_dpl : 2;            // descriptor(meaning new) privilege level    unsigned gd_p : 1;                // Present    unsigned gd_off_31_16 : 16;        // high bits of offset in segment};

这里写图片描述

中断处理过程:

这里写图片描述

可以看到,中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成偏移量,

通过段选择子去GDT中找到对应的基地址,然后基地址加上偏移量就是中断处理程序的地址。

[练习6.2] 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。

SETGATE函数的实现:

#define SETGATE(gate, istrap, sel, off, dpl) {            \    (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \    (gate).gd_ss = (sel);                                \    (gate).gd_args = 0;                                    \    (gate).gd_rsv1 = 0;                                    \    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \    (gate).gd_s = 0;                                    \    (gate).gd_dpl = (dpl);                                \    (gate).gd_p = 1;                                    \    (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \}

宏定义和数组说明:

#define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text#define DPL_KERNEL    (0)#define DPL_USER    (3)#define T_SWITCH_TOK                121    // user/kernel switchstatic struct gatedesc idt[256] = {{0}};

idt_init函数的实现:

voididt_init(void) {    extern uintptr_t __vectors[];  //保存在vectors.S中的256个中断处理例程的入口地址数组    int i;   //使用SETGATE宏,对中断描述符表中的每一个表项进行设置    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { //IDT表项的个数    //在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。     SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);    }    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT,         __vectors[T_SWITCH_TOK], DPL_USER);     //建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。    lidt(&idt_pd);}

[练习6.3]请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数

首先加入 string.h头文件,为了使用memmove函数

void *memmove(void *dst, const void *src, size_t n);

定义变量:

struct trapframe switchk2u, *switchu2k;

结构体 trapframe

struct trapframe {    struct pushregs tf_regs;    uint16_t tf_gs;    uint16_t tf_padding0;    uint16_t tf_fs;    uint16_t tf_padding1;    uint16_t tf_es;    uint16_t tf_padding2;    uint16_t tf_ds;    uint16_t tf_padding3;    uint32_t tf_trapno;    /* below here defined by x86 hardware */    uint32_t tf_err;    uintptr_t tf_eip;    uint16_t tf_cs;    uint16_t tf_padding4;    uint32_t tf_eflags;    /* below here only when crossing rings, such as from user to kernel */    uintptr_t tf_esp;    uint16_t tf_ss;    uint16_t tf_padding5;} __attribute__((packed));

宏定义:

#define IRQ_OFFSET                32    #define IRQ_TIMER                 0#define IRQ_KBD                   1#define IRQ_COM1                  4#define T_SWITCH_TOU              120#define USER_CS        ((GD_UTEXT) | DPL_USER)#define USER_DS        ((GD_UDATA) | DPL_USER)#define KERNEL_DS    ((GD_KDATA) | DPL_KERNEL)#define TICK_NUM 100

print_ticks函数

static void print_ticks() {    cprintf("%d ticks\n",TICK_NUM);#ifdef DEBUG_GRADE    cprintf("End of Test.\n");    panic("EOT: kernel seems ok.");#endif}

trap_dispatch函数的实现:

static voidtrap_dispatch(struct trapframe *tf) {    char c;    switch (tf->tf_trapno) {    case IRQ_OFFSET + IRQ_TIMER:        ticks ++;        if (ticks % TICK_NUM == 0) {            print_ticks();        }        break; //下面的代码不用我们实现    case IRQ_OFFSET + IRQ_COM1:        c = cons_getc();        cprintf("serial [%03d] %c\n", c, c);        break;    case IRQ_OFFSET + IRQ_KBD:        c = cons_getc();        cprintf("kbd [%03d] %c\n", c, c);        break;    case T_SWITCH_TOU:        if (tf->tf_cs != USER_CS) {            switchk2u = *tf;            switchk2u.tf_cs = USER_CS;            switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;            switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;            switchk2u.tf_eflags |= FL_IOPL_MASK;            *((uint32_t *)tf - 1) = (uint32_t)&switchk2u;        }        break;    case T_SWITCH_TOK:        if (tf->tf_cs != KERNEL_CS) {            tf->tf_cs = KERNEL_CS;            tf->tf_ds = tf->tf_es = KERNEL_DS;            tf->tf_eflags &= ~FL_IOPL_MASK;            switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));            memmove(switchu2k, tf, sizeof(struct trapframe) - 8);            *((uint32_t *)tf - 1) = (uint32_t)switchu2k;        }        break;    case IRQ_OFFSET + IRQ_IDE1:    case IRQ_OFFSET + IRQ_IDE2:        break;    default:        if ((tf->tf_cs & 3) == 0) {            print_trapframe(tf);            panic("unexpected trap in kernel.\n");        }    }}

运行结果:
这里写图片描述

[练习7]

增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),
当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务

[练习8]

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路
是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。

原创粉丝点击