清华实验lab1
本markdown遵循markdown plus与与Typora编辑器规则
若需要使用目录,请使用markdown plus或Typora
markdwon plus 在线编辑器
[TOC]
练习1
理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
在此练习中,大家需要通过阅读代码来了解:
- 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中
每一条相关命令和命令参数的含义,以及说明命令导致的结果)- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
练习1.1
1. 生成ucore.img
查看makefile源码,在178行处有注释:create ucore.img
以下是此部分代码
UCOREIMG := $(call totarget,ucore.img)
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)
此处参考dd命令参数 " Linux 下的dd命令使用详解 "可知道,创建了一块10000字节的块,并且将bootblock复制过去。并且把kernel接在之后的位置。(有seek = 1可知 复制的时候从文件开始跳过一个块,再存放。但此处bootblock为什么只占了一个块,会不会发生文件覆写 存疑)。
2. 生成kernel
可以看到上文的依赖中有kernel,搜索全文得到
# create kernel target
kernel = $(call totarget,kernel)
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
$(call create_target,kernel)
此处涉及到tools/kernel.ld文件,可运行查看make指令的实际样子
+ ld bin/kernel
ld -m elf_i386 -nostdlib -T tools/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
可以得出结论,此处链接的是如上的文件,也就是kern目录下的全部文件编译而成的文件。
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector
此处的全部文件使用类如上格式编译 相关的编译参数有
- 不适用c语言的内建函数,解决函数名冲突的情况
- 打开警告开关
- 生成gdb调试信息
- 生成32位环境代码
- 生成stabs格式的调试信息
- 不使用标准库,内核代码不需要标准io
- 禁用堆栈保护,这一条的作用如下 from stackoverflow ,并没有读的很懂
(In the standard/stock GCC, stack protector is off by default. However, some Linux distributions have patched GCC to turn it on by default. In my opinion, this is rather harmful, as it breaks the ability to compile anything that's not linked against the standard userspace libraries unless the Makefile specifically disables stack protector. It would even break the Linux kernel build except that the distributions with this hack added additional hacks to GCC to detect that the kernel is being built and disable it.)
3.生成bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
bootblock = $(call totarget,bootblock)
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
$(call create_target,bootblock)
代码如上,一样通过运行来查看实际的运行情况
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
此处有一个用sign规范bootblock.o到bin/block.o的操作
'obj/bootblock.out' size: 472 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0237835 s, 215 MB/s
猜想是规范为 "符合规范的硬盘主引导扇区文件"
以上就是makefile所做的事情,下面加上一张图以示
练习1.2
根据刚刚的猜想,阅读sign.c文件
char buf[512];
memset(buf, 0, sizeof(buf));
···
buf[510] = 0x55;
buf[511] = 0xAA;
这三行代码表明了 主引导扇区
- 大小为512字节
- 最后的两位为AA55(小端机,低位在前)
- 初始化为全零
练习2,练习3
- 输入make debug进入调试界面
- 输入
b *0x7c00
设置断点 -
continue
运行 - 查看接下的指令,与boot/bootasm.S和bootblock.asm里的内容十分类似
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
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
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
代码后面基本上都带有注释,在此处总结一下流程
- 禁止中断
- 复位标志寄存器方向标志位
- 初始化ds,es, ss三个段(设置为0)
- 使能A20(扩大寻址空间从1M)
- 跳转到gdtdes中,加载GDT(全局描述符表)
- 使能cr0,切换到保护模式
- 切换到32位模式
- 修改保护模式下各个寄存器的值(0x10)
- 设置堆栈以调用c语言
- 调用bootmain.c
按照练习3中的要求具体分析切换保护模式
1. 为何开启A20
A20的历史可以通过阅读wiki/A20_line得知:
大概是8086的时候使用段加偏移访问的时候,1M以上的空间必须要第21根线来寻址,所以才有了A20。
发展到后来的80286也沿用8086的方式,80386出来了保护模式。此时地址线已经是32根,如果不使能A20的话,A20将保持低电平,访问的空间减少了一半(第21位恒为零)。
2. 如何初始化GDT表
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
3.如何使能和进入保护模式
使能cr0的wp位为1,进入保护模式。
练习4
使用source insight可以很方便的查看各种数据类型和结构体的定义
void
bootmain(void) {
// read the 1st page off disk
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
先通过readseg函数读取扇区,readseg函数则是循环调用readsect函数来读取每一个扇区的内容。
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);
}
此处代码具体的读取方式存疑,猜想是通过outb:I/O 上写入 8 位数据 ( 1 字节 )的方法往不同的io端口写入指令来实现不同的功能。
扇区内容读完以后,进入判断函数判断是否为ELF格式的文件“ Loading ELF Binaries ” 配合相关ELF加载方法文档,将ELF Header的信息读如ph中。
按照ph里的信息,将ELF文件载入内存,然后再通过过函数入口信息,加载内核的入口。
练习5
栈相关的寄存器两个,ebp(基址寄存器)和esp(栈指针寄存器),栈的增长方向是由高到低
eip是程序指令指针,当前程序运行的指令
此时ebp是sum函数栈的基址,然后eip里面是sum函数中的第一条指令
sum函数执行完之后,sum函数栈的内容全部出栈,
eip=((uint_t)ebp+1),就是sum函数之后的指令的地址,然后函数参数出栈
然后ebp重新变成main函数的函数栈基址,ebp=((uint_t)ebp)
我们通过ebp获得当前函数栈的基址,eip获得程序当前运行的位置。
然后打印两个值,由指导方案的示意图与注释输出参数表
然后在调用print_debuginfo输出其他的信息。
再通过结构示意图,将ebp+1的内容(返回地址)给eip
eip = *((uint32_t*)ebp + 1);
再吧ebp的内容给ebp回到上层调用函数继续输出
ebp = *((uint32_t*)ebp);
原代码没有判断ebp是否为0,输出了很多为零的ebp信息,在循环条件处判断ebp是否为零就可以了
最后一行的信息由 print_debuginfo函数输出
void
print_debuginfo(uintptr_t eip) {
struct eipdebuginfo info;
if (debuginfo_eip(eip, &info) != 0) {
cprintf(" : -- 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);
}
}
输出的信息分别是:源码所在文件,源码所在行数,函数名,当前位置与函数指针的差(即函数源码的长度)
至于这里的被ebp为什么是7bf8。
其实很好理解,因为每个函数体之前在编译的时候都会被插入
pushl %ebp
movl %esp,%ebp
所以7c00-0008 = 7bf8
练习6
1.中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
由指导方案的上图可知,一个表项占32*2位,8个字节。0到15位和48到63位为偏移量的低位和高位。16到31位是段选择子。 通过这几个数据来找到中断处理代码的入口。
2.补全idt_init函数
void idt_init(void)
{
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
int i = 0;
extern uintptr_t __vectors[];
for(i = 0; i < 255; ++i)
{
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], 0);
}
SETGATE(idt[T_SWITCH_TOK], 1, GD_KTEXT, __vectors[T_SWITCH_TOK], 3);
lidt(&idt_pd);
}
- 根据上面的注释,先申明外部变量__vectors[],也就是偏移量。
- 然后使用SETGATE宏来设置idt表。第二个参数按照要求设为零(for 中断)
第三个参数按照实验手册上说的通常设置为内核代码段,猜想应该折折以一个有关内存管理的宏定义,查看mm文件下的.h文件,发现在memlayout.h里定义了相关的宏
/* This file contains the definitions for memory management in our OS. */
/* global segment number */
#define SEG_KTEXT 1
#define SEG_KDATA 2
#define SEG_UTEXT 3
#define SEG_UDATA 4
#define SEG_TSS 5
/* global descriptor numbers */
#define GD_KTEXT ((SEG_KTEXT) << 3) // kernel text
#define GD_KDATA ((SEG_KDATA) << 3) // kernel data
#define GD_UTEXT ((SEG_UTEXT) << 3) // user text
#define GD_UDATA ((SEG_UDATA) << 3) // user data
#define GD_TSS ((SEG_TSS) << 3) // task segment selector
- 第四个参数设置为偏移量__vector[i]
- 第五个参数按照指导手册上所写,除了系统调用使用特权级3以外,均使用特权级0
- 然后是专门设置系统调用中断,即用户态切换到内核态,使用陷阱门描述符,并且特权级为3
- 最后使用lidt加载中断描述符表。此处虽然注释上写参数是idt_pd,这个变量类型是struct gatedesc 类型。但在写的时候发现lidt的申明是lidt(struct pseudodesc *pd) 所以将idt_pd取地址传入。
练习ex 1
扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务
此处参考《操作系统设计与实现》中的代码
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = USER_DS;
switchk2u.tf_es = USER_DS;
switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
//设置EFLAG的i/o特权位,在用户态也能使用i/o指令
switchk2u.tf_eflags |= (3<<12);
//设置临时栈,以便cpu用switchk2u中恢复数据
*((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;
//关闭用户态使用io的特权位
tf->tf_eflags &= ~(3<<12);
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;
图片来源网络,权侵删
根据操作系统设计与实现里的指导,先申明临时变量
struct trapframe switchk2u, *switchu2k;
然后在trap_disptach函数中,设置寄存器为用户/内核。
最后根据上面的两张图保存栈里的值。
在trap函数运行结束后,在trapentry.s里的iret的值返回以后,以用户态/内核态继续执行。
在init.c中启动测试函数,
static void
lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"sub $0x8, %%esp \n" //从中断返回的时候会多pop两位用来更新ss和sp,所以先把栈压两位
"int %0 \n"
"movl %%ebp, %%esp" //修复esp
:
: "i"(T_SWITCH_TOU)
);
}
static void
lab1_switch_to_kernel(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
}
两段内联汇编的作用与含义依然存在疑问。
练习ex 2
(未成功实现
尝试在trap.c里重写switch_to_u/k函数,然后通过键盘中断调用系统调用中断,使用中断嵌套的方式实现
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
if('0' == c && tf->tf_cs != KERNEL_CS){
print_trapframe(tf);
lab1_switch_to_kernel();
cprintf("switch to kernel : \n");
print_trapframe(tf);
}
else if('3' == c && tf->tf_cs != USER_CS){
print_trapframe(tf);
lab1_switch_to_userl();
cprintf("switch to user : \n");
print_trapframe(tf);
}else
break;
实际运行的时候,首先是无法观察到寄存器的变化。理论在调用switch函数的时候,会有新的中断产生,会再次调用trap函数,在trap函数返回的时候,.s文件里的iret会执行,寄存器的值被写入应该会有不同的输出。
然后就是内核态在调用键盘中断时转入用户态是成功了的,但是之后的用户态转入内核态一直失败,原因也仍在探索。