【练习1】

理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
在此练习中,大家需要通过阅读代码来了解: 1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果) 2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

[1.1]
1、生成ucore.img需要kernel和bootblock
生成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)

首先先创建一个大小为10000字节的块儿,然后再将bootblock拷贝过去。
生成ucore.img需要先生成kernel和bootblock

2、生成kernel
生成kernel的代码如下:

$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)@echo "bbbbbbbbbbbbbbbbbbbbbb$(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)

通过make V=指令得到执行的具体命令如下:

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

然后根据其中可以看到,要生成kernel,需要用GCC编译器将kern目录下所有的.c文件全部编译生成的.o文件的支持。具体如下:

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

3、生成bootblock
而生成bootblock的代码如下:

$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign) @echo "========================$(call toobj,$(bootfiles))"@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)

同样根据make V=指令打印的结果,得到要生成bootblock,首先需要生成bootasm.o、bootmain.o、sign, 下列代码为生成bootasm.o、bootmain.o的代码,由宏定义批量实现了。

bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))

而实际的命令在make V=指令结果里可以看到。
下述是由bootasm.S生成bootasm.o的具体命令:

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

下述是由bootmain.c生成bootmain.o的具体命令

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

下列代码为生成sign的代码

$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)

下面是生成sign具体的命令:

gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

有了上述的bootasm.o、bootmain.o、sign。
接下来就可以生成bootblock了,实际命令如下:

ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o

[1.2]
一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:

  • 磁盘主引导扇区只有512字节
  • 磁盘最后两个字节为0x55AA
  • 由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符组成

【练习2】

1、从 CPU加电后执行的第一条指令开始,单步跟踪 BIOS的执行。
2、在初始化位置 0x7c00 设置实地址断点,测试断点正常。
3、 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S和 bootblock.asm进行比较。

[2.1]
tools/gdbinit的内容如下。可见,这里是对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数

file bin/kernel
target remote :1234
break kern_init
continue

为了从CPU加电后执行的第一条指令开始调试,需要修改tools/gdbinit的内容为:

set architecture i8086
file bin/bootblock
target remote :1234
break start
continue

执行make debug,这时会弹出一个QEMU窗口和一个Terminal窗口,这是正常的,因为我们在makefile中定义了debug的操作正是启动QEMU、启动Terminal并在其中运行gdb。

debug: $(UCOREIMG)$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &$(V)sleep 2$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

Terminal窗口此时停在0x0000fff0的位置,这是eip寄存器的值,而cs寄存器的值为0xf000.

The target architecture is assumed to be i8086
0x0000fff0 in ?? ()
Breakpoint 1 at 0x7c00: file boot/bootasm.S, line 16.

输入si,执行1步,程序会跳转到0xe05b的地方。查看寄存器也可以发现eip的值变为0xe05b,而cs的值不变,仍然是0xf000.
反复输入si,以单步执行。

[2.2]
直接在tools/gdbinit中设置了断点break start,由于boot loader的入口为start,其地址为0x7c00,因此这和break *0x7c00效果是相同的。
设置断点后,输入continue或c,可以看到程序在0x7c00处停了下来,说明断点设置成功。
[2.3]
反汇编的代码与bootblock.asm基本相同,而与bootasm.S的差别在于:反汇编的代码中的指令不带指示长度的后缀,而bootasm.S的指令则有。比如,反汇编 的代码是xor %eax, %eax,而bootasm.S的代码为xorw %ax, %ax
反汇编的代码中的通用寄存器是32位(带有e前缀),而bootasm.S的代码中的通用寄存器是16位(不带e前缀)。

【练习3】

分析从bootloader进入保护模式的过程。BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行
bootloader。请分析bootloader是如何完成从实模式进入保护模式的

首先我们先分析一下bootloader:

1、关闭中断,将各个段寄存器重置
它先将各个寄存器置0

cli               # Disable interrupts
cld               # String operations increment
xorw %ax, %ax     # Segment number zero
movw %ax, %ds     # -> Data Segment
movw %ax, %es     # -> Extra Segment
movw %ax, %ss     # -> Stack Segment

2、开启A20

seta20.1:inb $0x64, %al   testb $0x2, %al  jnz seta20.1movb $0xd1, %al      outb %al, $0x64 seta20.2:inb $0x64, %al    testb $0x2, %al  jnz seta20.2movb $0xdf, %al   # 通过0x60写入数据11011111 即将A20置1outb %al, $0x60

3、加载GDT表

lgdt gdtdesc

4、将CR0的第0位置1

movl %cr0, %eaxorl $CR0_PE_ON, %eaxmovl %eax, %cr0

5、长跳转到32位代码段,重装CS和EIP*

ljmp $PROT_MODE_CSEG, $protcseg

6、重装DS、ES等段寄存器等

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

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

movl $0x0, %ebp
movl $start, %esp
call bootmain

[练习4]

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

  1. bootloader如何读取硬盘扇区的?
  2. bootloader是如何加载 ELF格式的 OS? 这里主要分析是bootmain函数,
bootmain(void) {readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);if (ELFHDR->e_magic != ELF_MAGIC) {goto bad;    }struct proghdr *ph, *eph;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);}((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);while (1);
}

bootloader读取硬盘扇区
根据上述bootmain函数分析,首先是由readseg函数读取硬盘扇区,而readseg函数则循环调用了真正读取硬盘扇区的函数readsect来每次读出一个扇区 ,如下:

readsect(void *dst, uint32_t secno) {waitdisk(); // 等待硬盘就绪// 写地址0x1f2~0x1f5,0x1f7,发出读取磁盘的命令outb(0x1F2, 1);outb(0x1F3, secno & 0xFF);outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);outb(0x1F7, 0x20);waitdisk();insl(0x1F0, dst, SECTSIZE / 4);//读取一个扇区
}

bootloader加载 ELF格式的 OS
读取完磁盘之后,开始加载ELF格式的文件。

bootmain(void) {..........//首先判断是不是ELFif (ELFHDR->e_magic != ELF_MAGIC) {goto bad;                 }struct proghdr *ph, *eph;//ELF头部有描述ELF文件应加载到内存什么位置的描述表,这里读取出来将之存入phph = (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头表中的入口信息,找到内核的入口并开始运行 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:..........
}

[练习5]

完成kdebug.c中函数print_stackframe的实现,可以通过函数>print_stackframe来跟踪函数调用堆栈中记录的返回地址。

编程前,首先了解下当前情况:在Terminal下输入make qemu,发现打印以下信息后就退出了:

along:~/src/ucore/labcodes/lab1$ sudo make qemu
WARNING: Image format was not specified for 'bin/ucore.img' and probing guessed raw.Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.Specify the 'raw' format explicitly to remove the restrictions.
(THU.CST) os is loading ...Special kernel symbols:entry  0x00100000 (phys)etext  0x001036f3 (phys)edata  0x0010e950 (phys)end    0x0010fdc0 (phys)
Kernel executable memory footprint: 64KB

分析print_stackframe的函数调用关系

kern_init ->
grade_backtrace ->grade_backtrace0(0, (int)kern_init, 0xffff0000) ->grade_backtrace1(0, 0xffff0000) ->grade_backtrace2(0, (int)&0, 0xffff0000, (int)&(0xffff0000)) ->mon_backtrace(0, NULL, NULL) ->print_stackframe ->

找到print_stackframe函数,发现函数里面的注释已经提供了十分详细的步骤,基本上按照提示来做就行了。
1、首先定义两个局部变量ebp、esp分别存放ebp、esp寄存器的值。这里将ebp定义为指针,是为了方便后面取ebp寄存器的值。
2、调用read_ebp函数来获取执行print_stackframe函数时ebp寄存器的值,这里read_ebp必须定义为inline函数,否则获取的是执行read_ebp函数时的ebp寄存器的值。
3、调用read_eip函数来获取当前指令的位置,也就是此时eip寄存器的值。这里read_eip必须定义为常规函数而不是inline函数,因为这样的话在调用read_eip时会把当前指令的下一条指令的地址(也就是eip寄存器的值)压栈,那么在进入read_eip函数内部后便可以从栈中获取到调用前eip寄存器的值。
4、由于变量eip存放的是下一条指令的地址,因此将变量eip的值减去1,得到的指令地址就属于当前指令的范围了。由于只要输入的地址属于当前指令的起始和结束位置之间,print_debuginfo都能搜索到当前指令,因此这里减去1即可。
5、以后变量eip的值就不能再调用read_eip来获取了(每次调用获取的值都是相同的),而应该从ebp寄存器指向栈中的位置再往上一个单位中获取。
6、由于ebp寄存器指向栈中的位置存放的是调用者的ebp寄存器的值,据此可以继续顺藤摸瓜,不断回溯,直到ebp寄存器的值变为0

void print_stackframe(void) {uint32_t *ebp = 0;uint32_t esp = 0;ebp = (uint32_t *)read_ebp();esp = read_eip();while (ebp){cprintf("ebp:0x%08x eip:0x%08x args:", (uint32_t)ebp, esp);cprintf("0x%08x 0x%08x 0x%08x 0x%08x\n", ebp[2], ebp[3], ebp[4], ebp[5]);print_debuginfo(esp - 1);esp = ebp[1];ebp = (uint32_t *)*ebp;}/* LAB1 YOUR CODE : STEP 1 *//* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);* (2) call read_eip() to get the value of eip. the type is (uint32_t);* (3) from 0 .. STACKFRAME_DEPTH*    (3.1) printf value of ebp, eip*    (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]*    (3.3) cprintf("\n");*    (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.*    (3.5) popup a calling stackframe*           NOTICE: the calling funciton's return addr eip  = ss:[ebp+4]*                   the calling funciton's ebp = ss:[ebp]*/
}

编码完成后,执行make qemu,打印结果如下所示,与实验指导书的结果类似。

ebp:0x00007b38 eip:0x00100bf2 args:0x00010094 0x0010e950 0x00007b68 0x001000a2kern/debug/kdebug.c:297: print_stackframe+48
ebp:0x00007b48 eip:0x00100f40 args:0x00000000 0x00000000 0x00000000 0x0010008dkern/debug/kmonitor.c:125: mon_backtrace+23
ebp:0x00007b68 eip:0x001000a2 args:0x00000000 0x00007b90 0xffff0000 0x00007b94kern/init/init.c:48: grade_backtrace2+32
ebp:0x00007b88 eip:0x001000d1 args:0x00000000 0xffff0000 0x00007bb4 0x001000e5kern/init/init.c:53: grade_backtrace1+37
ebp:0x00007ba8 eip:0x001000f8 args:0x00000000 0x00100000 0xffff0000 0x00100109kern/init/init.c:58: grade_backtrace0+29
ebp:0x00007bc8 eip:0x00100124 args:0x00000000 0x00000000 0x00000000 0x0010379ckern/init/init.c:63: grade_backtrace+37
ebp:0x00007be8 eip:0x00100066 args:0x00000000 0x00000000 0x00000000 0x00007c4fkern/init/init.c:28: kern_init+101
ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8<unknow>: -- 0x00007d6d --

[练习6]

1.中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。注意除了系统调用中断(T_SYSCALL)以外,其它中断均使用中断门描述符,权限为内核态权限;而系统调用中断使用异常,权限为陷阱门描述符。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

3.请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用
print_ticks子程序,向屏幕上打印一行文字100 ticks。

[6.1]
中断描述符表一个表项占8字节。其中015位和4863位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。

[6.2]
idt_init函数的功能是初始化IDT表。IDT表中每个元素均为门描述符,记录一个中断向量的属性,包括中断向量对应的中断处理函数的段选择子/偏移量、门类型(是中断门还是陷阱门)、DPL等。因此,初始化IDT表实际上是初始化每个中断向量的这些属性。

题目已经提供中断向量的门类型和DPL的设置方法:除了系统调用的门类型为陷阱门、DPL=3外,其他中断的门类型均为中断门、DPL均为0.
中断处理函数的段选择子及偏移量的设置要参考kern/trap/vectors.S文件:由该文件可知,所有中断向量的中断处理函数地址均保存在__vectors数组中,该数组中第i个元素对应第i个中断向量的中断处理函数地址。而且由文件开头可知,中断处理函数属于.text的内容。因此,中断处理函数的段选择子即.text的段选择子GD_KTEXT。从kern/mm/pmm.c可知.text的段基址为0,因此中断处理函数地址的偏移量等于其地址本身。
完成IDT表的初始化后,还要使用lidt命令将IDT表的起始地址加载到IDTR寄存器中。

extern uintptr_t __vectors[];/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void idt_init(void) {uint32_t pos;uint32_t sel = GD_KTEXT;/* along: how to set istrap and dpl? */for (pos = 0; pos < 256; pos++) {SETGATE(idt[pos], 0, sel, __vectors[pos], 0);}SETGATE(idt[128], 1, sel, __vectors[128], 3);lidt(&idt_pd);
}

[6.3]
trap函数只是直接调用了trap_dispatch函数,而trap_dispatch函数实现对各种中断的处理,题目要求我们完成对时钟中断的处理,实现非常简单:定义一个全局变量ticks,每次时钟中断将ticks加1,加到100后打印"100 ticks",然后将ticks清零重新计数。代码实现如下:

case IRQ_OFFSET + IRQ_TIMER:if (((++ticks) % TICK_NUM) == 0) {print_ticks();ticks = 0;}

操作系统LAB1实验报告相关推荐

  1. 操作系统ucore lab1实验报告

    操作系统lab1实验报告 [练习1] 理解通过 make 生成执行文件的过程.(要求在报告中写出对下述问题的回答) 在此练习中,大家需要通过阅读代码来了解: 1. 操作系统镜像文件 ucore.img ...

  2. ucore lab1 实验报告

    UCORE LAB1 实验报告 练习一 理解通过make生成执行文件的过程 1.操作系统镜像文件ucore.img是如何一步一步生成的? 先打开lab1文件夹下的Makefile,查看里面的代码,在各 ...

  3. 《操作系统》实验报告——熟悉Linux基础命令及进程管理

    理论知识 Linux--进程管理 Linux--Linux C语言编程基础知识 手把手教你安装Linux虚拟机 一.实验目的 (1)加深对进程概念的理解,明确进程和程序的区别. (2)进一步认识并发执 ...

  4. 进程同步算法实现实验报告Linux,操作系统进程同步实验报告.doc

    操作系统进程同步实验报告 实验三:进程同步实验 一.实验任务: (1)掌握操作系统的进程同步原理: (2)熟悉linux的进程同步原语: (3)设计程序,实现经典进程同步问题. 二.实验原理: (1) ...

  5. 东北大学软件学院操作系统v实验报告

    课程编号:B080000070     <操作系统>实验报告             姓名   学号   班级   指导教师   实验名称 <操作系统>实验 开设学期 2016 ...

  6. 课程linux实验报告,Linux操作系统课程实验报告.doc

    Linux操作系统课程实验报告.doc Linux操作系统课程实验报告班级姓名学号指导老师田丽华完成时间2014年7月目录一.实验目的1二.实验要求1三.实验内容1[第一题]1[第二题]2[第三题]4 ...

  7. linux课程实验报告,Linux操作系统课程实验报告

    Linux操作系统课程实验报告 Linux操作系统 课程实验报告 班级: 姓名: 学号: 指导老师:田丽华 完成时间:2014年7月 目录 一.实验目的1 二.实验要求1 三.实验内容1 [第一题]1 ...

  8. [HITML] 哈工大2020秋机器学习Lab1实验报告

    Gtihub仓库 不想白嫖的就来这投个币吧 2020年春季学期 计算学部<机器学习>课程 Lab1 实验报告 姓名 学号 班号 电子邮件 手机号码 1 实验目的 掌握最小二乘法求解(无惩罚 ...

  9. 操作系统 作业调度实验报告

    题目要求 一. 实验目的 用高级语言编写和调试一个或多个作业调度的模拟程序,以加深对作业调度算法的理解. 二. 例题 为单道批处理系统设计一个作业调度程序. 由于在单道批处理系统中,作业一投入运行,它 ...

最新文章

  1. 计算机文档里的东西可以删吗,电脑c盘哪些文件可以删除
  2. Aviator(表达式执行引擎)发布1.0.1
  3. SpringSecurity常用过滤器介绍
  4. 使用语句修改数据表结构
  5. Java开发中的常见危险信号
  6. linux日志添加到文件,关于linux:将变量中的内容追加到日志文件中
  7. 一叶知秋:基于“单目标域样本”的领域自适应方法
  8. linux如何查看jupyter日志_在Linux服务器上运行Jupyter notebook server教程
  9. JAVA遇见HTML——JSP篇 阶段项目总结 model1模型实现商品浏览记录
  10. java.lang.IllegalStateException: No output folder
  11. 二十四、Java集合框架(一)
  12. 智能跟随小车-红外遥控(程序+原理图+PCB+论文报告)
  13. 如何深刻理解IEEE浮点数的表示(IEEE floating-point representation)
  14. git commit
  15. A320M HDV 4.0主板用CH341A手动刷BIOS支持5600g
  16. 读书笔记-哈佛大学极简经济学2
  17. JACK——TeamsMaual6 Team Formation
  18. 近期业务大量突增微服务性能优化总结-2.开发日志输出异常堆栈的过滤插件
  19. python海龟作图好看图案_海龟作图---用Python绘图
  20. java controller注解原理_SpringMVC运行流程与原理【Controller接口实现注解实现】

热门文章

  1. 【小程序开发之准备工作】如何开通云开发和CMS内容管理平台
  2. 【论文检索】推荐一个可以免费下载论文的网站
  3. 学java应该学什么
  4. idea如何查看已安装的插件
  5. Product-based Neural Networks (PNN) - 改进特征交叉的方式
  6. nginx配置同一个端口转发多个项目
  7. Nginx 设置域名转发到指定端口
  8. Win32 编程基础
  9. 基于UBAT工具的试验性应用
  10. python statsmodels安装(亲测可用)