操作系统 ucorelab1

mac2024-12-20  21

操作系统 ucorelab1

实验目的

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作。为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。lab1提供了一个非常小的bootloader和ucore OS,整个bootloader执行代码小于512个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS,我们的目的在于了解到: 计算机原理

CPU的编址与寻址: 基于分段机制的内存管理 CPU的中断机制 外设:串口/并口/CGA,时钟,硬盘

Bootloader软件

编译运行bootloader的过程 调试bootloader的方法 PC启动bootloader的过程 ELF执行文件的格式和加载 外设访问:读硬盘,在CGA上显示字符串 ucore OS软件

编译运行ucore OS的过程 ucore OS的启动过程 调试ucore OS的方法 函数调用关系:在汇编级了解函数调用栈的结构和处理过程 中断管理:与软件相关的中断处理 外设管理:时钟 实验步骤

**

练习1 理解通过make生成执行文件的过程

问题1: 操作系统镜像文件ucore.img是如何一步一步生成的?****** 实验过程:

先进入文件路径: 执行make:make “k=” 观察过程 (1)通过GCC编译器将Kernel目录下的.c文件编译成OBJ目录下的.o文件。 (2)ld命令根据链接脚本文件kernel.ld将生成的*.o文件,链接成BIN目录下的kernel文件。 (3)通过GCC编译器将boot目录下的.c,.S文件以及tools目录下的sign.c文件编译成OBJ目录下的*.o文件。 (4)ld命令将生成的*.o文件,链接成BIN目录下的bootblock文件。 (5)dd命令将dev/zero, bin/bootblock,bin/kernel 写入到bin/ucore.img 问题2:

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

实验过程:

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

练习2

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

修改lab1/tools/gdbinit ,内容为: 然后在 lab1执行: 在gdb的调试界面,执行如下命令:

来单步跟踪

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

[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常 修改 gdbinit文件: 得到如下结果,断点正常 [练习2.3] 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。 (1)单步跟踪。 输入两次si。 (2)使用meld对比bootasm.S和bootlock.asm的代码 是相同的。

练习2.4 自己找一个bootloader或内核中的代码位置,设置断点并进行测试** (1)查看bootloader,选择0x7c06位置。 *

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

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

.globl start start: .code16 # 使用16位模式编译 cli # 禁用中断 cld # 清除方向标志 # 建立重要的数据段寄存器(DS,ES,SS)。 xorw %ax, %ax # ax清0 movw %ax, %ds # ds清0 movw %ax, %es # es清0 movw %ax, %ss # ss清0 ***

为何开启A20,以及如何开启A20

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

如何初始化GDT表

载入GDT表

lgdt gdtdesc # 载入GDT表 ```进入保护模式 cro的第0位为1表示处于保护模式 ```powershell movl %cr0, %eax # 加载cro到eax orl $CR0_PE_ON, %eax # 将eax的第0位置为1 movl %eax, %cr0 # 将cr0的第0位置为1

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

ljmp $PROT_MODE_CSEG, $protcseg # $PROT_MODE_CSEG的值为0x80

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

.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 # 设置栈指针

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

call bootmain

如何使能和进入保护模式

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

练习4:分析bootloader加载ELF格式的OS的过程 bootloader如何读取硬盘扇区

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

在这里插入代码片 ```#include <defs.h> #include <x86.h> #include <elf.h> /* ********************************************************************* * This a dirt simple boot loader, whose sole job is to boot * an ELF kernel image from the first IDE hard disk. * * DISK LAYOUT * * This program(bootasm.S and bootmain.c) is the bootloader. * It should be stored in the first sector of the disk. * * * The 2nd sector onward holds the kernel image. * * * The kernel image must be in ELF format. * * BOOT UP STEPS * * when the CPU boots it loads the BIOS into memory and executes it * * * the BIOS intializes devices, sets of the interrupt routines, and * reads the first sector of the boot device(e.g., hard-drive) * into memory and jumps to it. * * * Assuming this boot loader is stored in the first sector of the * hard-drive, this code takes over... * * * control starts in bootasm.S -- which sets up protected mode, * and a stack so C code then run, then calls bootmain() * * * bootmain() in this file takes over, reads in the kernel and jumps to it. * */ /* waitdisk - wait for disk ready */ 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); //获取数据 } /* * * readseg - read @count bytes at @offset from kernel into virtual address @va,might copy more than asked. * */ 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); } } bootloader如何加载ELF格式的OS 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用。 校验e_magic字段。 根据偏移量分别把程序段的数据读取到内存中。 ElF结构定义 ```powershell 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:

#define SECTSIZE 512 #define ELFHDR ((struct elfhdr *)0x10000) /* 引导-引导加载器的条目 */ 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:实现函数调用堆栈跟踪函数 栈结构: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191107200253697.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Zsb3RsZW4=,size_16,color_FFFFFF,t_70) ebp表示上一层。 eip表示第一个局部变量。 实现函数: ```powershell print_stackframe(void) { int i,j; uint32_t ebp=read_ebp(); uint32_t eip=read_eip(); for(i=0;i<STACKFRAME_DEPTH&&ebp!=0;i++){ cprintf("ebp:0x%08x eip:0x%08x\n",ebp,eip); uint32_t *args=(uint32_t *)ebp+2; cprintf("参数:"); for(j=0;j<4;j++){ cprintf("0x%08x ", args[j]); } cprintf("\n"); print_debuginfo(eip-1); eip=((uint32_t *)ebp)[1]; ebp=((uint32_t *)ebp)[0]; } }

直接涉及函数:

kdebug.c中read_eip():

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

x86.h中read_ebp() :

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

kdebug.c中print_debuginfo(uintptr_t eip):

void print_debuginfo(uintptr_t eip) { struct eipdebuginfo info; if (debuginfo_eip(eip, &info) != 0) { cprintf(" <unknow>: -- 0x%08x --\n", eip); } else { char fnname[256]; int j; for (j = 0; j < info.eip_fn_namelen; j ++) { fnname[j] = info.eip_fn_name[j]; } fnname[j] = '\0'; cprintf(" %s:%d: %s+%d\n", info.eip_file, info.eip_line, fnname, eip - info.eip_fn_addr); } }

执行make qemu:

Special kernel symbols: entry 0x00100000 (phys) etext 0x001032cf (phys) edata 0x0010ea16 (phys) end 0x0010fd20 (phys) Kernel executable memory footprint: 64KB ebp:0x00007b08 eip:0x001009a6 参数:0x00010094 0x00000000 0x00007b38 0x00100092 kern/debug/kdebug.c:307: print_stackframe+21 ebp:0x00007b18 eip:0x00100ca1 参数:0x00000000 0x00000000 0x00000000 0x00007b88 kern/debug/kmonitor.c:125: mon_backtrace+10 ebp:0x00007b38 eip:0x00100092 参数:0x00000000 0x00007b60 0xffff0000 0x00007b64 kern/init/init.c:48: grade_backtrace2+33 ebp:0x00007b58 eip:0x001000bb 参数:0x00000000 0xffff0000 0x00007b84 0x00000029 kern/init/init.c:53: grade_backtrace1+38 ebp:0x00007b78 eip:0x001000d9 参数:0x00000000 0x00100000 0xffff0000 0x0000001d kern/init/init.c:58: grade_backtrace0+23 ebp:0x00007b98 eip:0x001000fe 参数:0x001032fc 0x001032e0 0x0000130a 0x00000000 kern/init/init.c:63: grade_backtrace+34 ebp:0x00007bc8 eip:0x00100055 参数:0x00000000 0x00000000 0x00000000 0x00010094 kern/init/init.c:28: kern_init+84 ebp:0x00007bf8 eip:0x00007d68 参数:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d67 -- ++ setup timer interrupts (THU.CST) os is loading ...

输出从顶向下,实际执行顺序相反,从bootmain开始一步步调用函数,info.eip_file、info.eip_line和fnname分别表示调用发生的文件、调用发生所在行和调用函数名。

练习6:完善中断初始化和处理 (需要编程) 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

向量表结构: 由各项相加可知,一个表项的占64bit,即8字节,其中0 ~ 15位和48 ~ 63位分别为偏移量的低16位和高16位,两者拼接为偏移量,16~31位为段选择器。

使用段选择符中的偏移值在GDT(全局描述符表) 或 LDT(局部描述符表)中定位相应的段描述符。 利用段描述符校验段的访问权限和范围,以确保该段是可以访问的并且偏移量位于段界限内。 利用段描述符中取得的段基地址加上偏移量,形成一个线性地址。

[练习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"); } } }

make qemu一下子:

[练习7] —扩展练习 Challenge 1(需要编程)

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

[练习8] —扩展练习 Challenge 2(需要编程)

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

最新回复(0)