实验1:系统软件启动过程 练习1:
(1) 操作系统镜像文件 ucore.img 是如何一步一步生成的?
在命令行中输入“make V=”
1、首先把C的源代码进行编译成为.o文件,也就是目标文件(红色方框内) 2、 ld命令将这些目标文件转变成可执行文件,比如此处的bootblock.out(绿色方框内) 3、dd命令把bootloder放到ucore.img count的虚拟硬盘之中 4、还生成了两个软件,一个是Bootloader,另一个是kernel。 (2)一个被系统认为是符合规范的硬盘主引导扇区的特征: 在/lab1/tools/sign.c中我们可以了解到
规范的硬盘引导扇区的大小为512字节,硬盘结束标志位55AA
练习2:
(1) 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行
改写Makefile文件
lab1-mon: $(UCOREIMG)
$(V)$(TERMINAL) -e \"$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -monitor stdio -hda $< -serial null\"
$(V)sleep 2
$(V)$(TERMINAL) -e \"gdb -q -x tools/lab1init\"
在调用qemu时增加-d in_asm -D q.log参数,便可以将运行的汇编指令保存在q.log
中。
(2) 在初始化位置0x7c00 设置实地址断点,测试断点正常。
在tools/gdbinit结尾加上
set architecture i8086
b *0x7c00 //在0x7c00处设置断点。 continue
x /2i $pc //显示当前eip处的汇编指令
(3) 将执行的汇编代码与bootasm.S 和 bootblock.asm 进行比较,看看二者是否一致。
Notice:在q.log中进入BIOS之后的跳转地址与实际应跳转地址不相符,汇编代码也
与bootasm.S 和 bootblock.asm不相同。
这是由于在gdb之中调试的原因,可以直接输入make debug,在生成的qemu虚拟
机之中进行调试可以看到在虚拟机中运行的汇编代码,之后再与bootasm.S 和 bootblock.asm 进行比较。
与bootasm.S和bootblock.asm中的代码相同。
练习3:分析bootlloader进入保护模式的过程(/lab1/boot/bootasm.S)
.globl start start:
.code16
# 关中断,并清除方向标志,即将 DF 置“0”,这样(E)SI 及(E)DI 的修改为增量 cli cld
# 清零各数据段寄存器:DS、ES、FS
xorw %ax, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss
# 使能 A20 地址线,这样 80386 就可以突破 1MB 访存现在,而可访问 4GB 的 32 位地址空间
seta20.1:
inb $0x, %al # 等待8042键盘控制器不忙
testb $0x2, %al jnz seta20.1
movb $0xd1, %al outb %al, $0x
seta20.2:
inb $0x, %al # 等待8042键盘控制器不忙
testb $0x2, %al jnz seta20.2
movb $0xdf, %al # 打开A20 outb %al, $0x60
# 初始化gdt
lgdt gdtdesc
# 进入保护模式
movl %cr0, %eax
orl $CR0_PE_ON, %eax movl %eax, %cr0
# 长跳转
ljmp $PROT_MODE_CSEG, $protcseg
.code32 protcseg:
# 设置段寄存器,并建立堆栈
movw $PROT_MODE_DSEG, %ax
movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %fs # -> FS movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# 设置堆栈 movl $0x0, %ebp
movl $start, %esp # 栈顶为0x7c00 # 进入bootmain,不再返回 call bootmain spin:
jmp spin
练习4:分析bootloader加载ELF格式的OS的过程
读一个扇区的流程可参看bootmain.c 中的 readsect 函数实现。大致如下:
1. 读 I/O 地址 0x1f7,等待磁盘准备好; 2. 写 I/O 地址 0x1f2~0x1f5,0x1f7,发出读取第 offseet 个扇区处的磁盘数据的命令; 3. 读 I/O 地址 0x1f7,等待磁盘准备好;
4. 连续读 I/O 地址 0x1f0,把磁盘扇区数据读到指定内存。 static void readsect(void *dst, uint32_t secno) { // wait for disk to be ready waitdisk();
outb(0x1F2, 1); // count = 1 outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF);
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); }
该函数封装在readseg函数中,该函数完成读取任意的长度。
Notice: uint32_t secno = (offset / SECTSIZE) + 1; # 0号扇区已被引导占用。
最后在bootmain函数中完成加载ELF格式os的操作: 1: 读取ELF的头部
2: 判断ELF文件是否是合法 3: 将描述表的头地址存在ph
4: 按照描述表将ELF文件中数据载入内存
5: 根据ELF头部储存的入口信息,找到内核的入口(不再返回) Notice:可能会出现内存长度>文件长度的现象 多读入部分包含bss节,需要清0
练习5:实现函数调用堆栈跟踪函数 print_stackframe(void) {
uint32_t ebp = read_ebp(), eip = read_eip(); int i, j;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) { cprintf(\"ebp:0x%08x eip:0x%08x args:\ uint32_t *args = (uint32_t *)ebp + 2; //(uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4] for (j = 0; j < 4; j ++) { cprintf(\"0x%08x \ }
cprintf(\"\\n\");
print_debuginfo(eip - 1);
/*call print_debuginfo(eip-1) to print the C calling function name and line number, etc.*/ eip = ((uint32_t *)ebp)[1]; ebp = ((uint32_t *)ebp)[0]; //popup a calling stackframe } Notice:
ss:ebp指向的堆栈位置储存着caller的ebp,以此为线索可以得到所有使用堆栈的函数ebp。
ss:ebp+4指向caller调用时的eip,ss:ebp+8等是(可能的)参数。 练习6:
(1)中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成
位移,入口地址=段选择子+段内偏移量。
(2) 完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init
可以在/lab1/kern/mm/mmu.h中可以找到SETGATE函数,查找其具体操作。 idt_init(void) {
extern uintptr_t __vectors[]; int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); //设置IDT }
lidt(&idt_pd); //载入IDT表
(3) 完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数 case IRQ_OFFSET + IRQ_TIMER:
ticks ++; //一次中断累加1 if (ticks % TICK_NUM == 0) { print_ticks(); }
break;
扩展:
(1) 内核态切换到用户态:
lab1_switch_to_user(void) {
asm volatile (
\"sub $0x8, %%esp \\n\"
// esp-8 为下一步复制的栈帧留好 tf_ss和tf_esp的位置 \"int %0 \\n\"
\"movl %%ebp, %%esp\" :
: \"i\"(T_SWITCH_TOU) ); } 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;
//在执行int120前系统在核心态,int不会引起栈的切换
switchk2u.tf_eflags |= FL_IOPL_MASK;
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u; }
break;
最后iret时返回5个值。 (2) 用户态切换到内核态:
lab1_switch_to_kernel(void) { asm volatile ( \"int %0 \\n\"
\"movl %%ebp, %%esp \\n\" :
: \"i\"(T_SWITCH_TOK) ); }
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); //在执行int120前系统在核心态,int会引起栈的切换,iret不会引起 //栈的切换
*((uint32_t *)tf - 1) = (uint32_t)switchu2k; /*设置临时栈,指向switchu2k,这样iret返回时,CPU会从switchu2k恢复数据, 而不是从现有栈恢复数据。*/
} break;
最后iret返回三个值
Question:
将用户态转到内核态不用临时栈,直接在原来的栈进行操作?
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- zicool.com 版权所有 湘ICP备2023022495号-2
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务