ucore lab1

[练习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/kernel
3 编译bootasm.S  bootmain.c  sign.c 
4 根据sign规范生成obj/bootblock.o
5 生成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 多余的空间填0
3510个(倒数第二个)字节是0x55,
4511个(倒数第一个)字节是0xAA。

[练习2]

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

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

set architecture i8086
target remote :1234

然后在 lab1执行:

make debug

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

si

来单步跟踪

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

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

得到下面的截图:

这里写图片描述

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

修改 gdbinit文件:

set architecture i8086
target remote :1234
b *0x7c00
c
x/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 i8086
target remote :1234
break *0x7c4a

输入 make debug ,得到结果:

这里写图片描述

断点设置正常

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

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

.globl start
start:
.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到eax
orl $CR0_PE_ON, %eax  //将eax的第0位置为1
movl %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 void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循环
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
static void
readsect(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_t
read_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_t
read_ebp(void) {
    uint32_t ebp;
    asm volatile ("movl %%ebp, %0" : "=r" (ebp));  //内联汇编,读取edp寄存器的值到变量ebp
    return ebp;  //返回ebp的值
}

​ 实现函数如下:

void
print_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 switch
static struct gatedesc idt[256] = {{0}};

idt_init函数的实现:

void
idt_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 void
trap_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中软中断处理的设置语句拿过来。

  • 24
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

计算机的小粽子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值