ucore

练习一

练习1.1 操作系统镜像文件 ucore.img 是如何一步一步生成的?

 输入make V=

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
查看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文件中的部分源码
在这里插入图片描述

试验发现主引导扇区

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

练习二

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

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

 set architecture i8086 
 target remote :1234

然后在 lab1执行

make debug

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

 si    //单步跟踪

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

 x /2i $pc

得到下面的截图
在这里插入图片描述

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

修改 gdbinit文件

set architecture i8086
target remote :1234 
b *0x7c00 
c 
x/2i $pc 

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

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

1单步跟踪

输入两次 si
在这里插入图片描述

2使用meld对比 bootasm.S和bootlock.asm的代码

 meld +bootasm.S和bootlock.asm

在这里插入图片描述

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

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

在这里插入图片描述

 .globl start 
        start:
         .code16   //使用16位模式编译
            cli                                   //关中断
            cld                                   //清除方向标志
            xorw %ax, %ax                         // ax 清0
            movw %ax, %ds                         // ds 清0
            movw %ax, %es                         // es 清0
            movw %ax, %ss                         // ss 清
     # 

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

在这里插入图片描述

15seta20.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端口中
     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.进入保护模式

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

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

.code32                                             // 使用32位模式编译
protcseg:
    movw $PROT_MODE_DSEG, %ax                       // ax赋0x80
    movw %ax, %ds                                   // ds赋0x80
    movw %ax, %es                                   // es赋0x80
    movw %ax, %fs                                   // fs赋0x80
    movw %ax, %gs                                   // gs赋0x80
    movw %ax, %ss                                   // ss赋0x80
    movl $0x0, %ebp                                 // 设置帧指针
    movl $start, %esp                               // 设置栈指针  

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

call bootmain          //调用bootmain函数

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

将cr0寄存器置1,cro的第0位为1表示处于保护模式

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

bootloader如何读取硬盘扇区

等待磁盘准备好;
发出读取扇区的命令;
等待磁盘准备好;
把磁盘扇区数据读到指定内存。

我们来看一下关于0号硬盘的I/O端口:

在这里插入图片描述

static void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循环
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
        }
        /* 读节 - 将“secno”处的单个扇区读入“dst” */
        static void
readsect(void *dst, uint32_t secno) {
    // 等待磁盘准备就绪
    waitdisk();
// 用LBA模式的PIO(Program IO)方式来访问硬盘
    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-读为扇区
         // 等待磁盘准备就绪
waitdisk()insl(0x1F0, dst, SECTSIZE / 4); //获取数据
}

从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。

static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;
    // 转至分区边界
    va -= offset % SECTSIZE;
       // 从字节转换到扇区;内核从扇区1开始
    uint32_t secno = (offset / SECTSIZE) + 1; //加1因为0扇区被引导占用
    // 如果速度慢,我们一次就能读到很多扇区。
          // 我们会写更多的记忆,而不是要求,但这并不重要--我们以增序加载。
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }

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

从硬盘读了8个扇区数据到内存0x10000处;
校验e_magic字段
根据偏移量分别把程序段的数据读取到内存中
ELF定义:

struct elfhdr {
    uint32_t e_magic;     // 判断读出来的ELF格式的文件是否为正确的格式
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=可重定位,2=可执行,3=共享对象,4=核心映像
    uint16_t e_machine;   // 3=x86,4=68K等.
    uint32_t e_version;   // 文件版本,总是1
    uint32_t e_entry;       // 程序入口所对应的虚拟地址。
    uint32_t e_phoff;     // 程序头表的位置偏移
    uint32_t e_shoff;       // 区段标题或0的文件位置
    uint32_t e_flags;     // 特定于体系结构的标志,通常为0
    uint16_t e_ehsize;    // 这个elf头的大小
    uint16_t e_phentsize;    // 程序头中条目的大小
    uint16_t e_phnum;     // 程序头表中的入口数目
    uint16_t e_shentsize;     // 节标题中条目的大小
    uint16_t e_shnum;         // 节标题中的条目数或0
    uint16_t e_shstrndx;    // 包含节名称字符串的节号。
};

宏定义
bootmain.c:

void
bootmain(void) {
    // 从磁盘上读取第一页
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
    // 这是有效的ELF吗?
    if (ELFHDR->e_magic != ELF_MAGIC) { // 通过储存在头部的幻数判断是否是合法的ELF文件
        goto bad;
    }
    struct proghdr *ph, *eph;
    // 加载每个程序段(忽略ph标志)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); // 先将描述表的头地址存在ph
    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);
    /* do nothing */
    while (1);
}

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

------通过read_ebp()和read_eip()函数来获取当前ebp寄存器和eip 寄存器的信息
ebp表示上一层,eip表示第一个局部变量。
read_ebp()(在kdebug.c中)

read_ebp()
static __noinline uint32_t
read_eip(void) {
    uint32_t eip;
    asm volatile("movl 4(%%ebp), %0" : "=r" (eip)); //读取(ebp-4)的值到变量eip
    return eip;  //返回eip的值
}
 **re

ad_eip()(在 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开始一步步调用函数,bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。

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

中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

表结构

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

mmu.h中的SETGATE(gate,istrap,sel,off,dpl)**
#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;        
}
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);
}

make debug 执行验证

请编程完善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 print_ticks() {
    cprintf("%d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADE
    cprintf("End of Test.\n");
    panic("EOT: kernel seems ok.");
#endif
}

执行

make debug

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值