Ucore lab1 实验

《ucore lab1》实验报告

环境配置Ubuntu
Ucore lab1 实验
练习1:
理解通过make生成执行文件的过程
1.1操作系统镜像文件ucore.img是如何一步一步生成的?
生成ucore.img的代码为:
Makefile通过一系列命令生成了bootblock和kernel这两个elf文件,之后通过dd命令将bootblock放到第一个sector,将kernel放到第二个sector开始的区域。可以明显看出bootblock就是引导区,kernel则是操作系统内核。
而在这之前还通过sign对bootblock进行了修饰,在512个字节的最后两个字节写入了0x55AA,作为引导区的标记。生成bootlock的代码为:
Ucore lab1 实验
查看sign.c源码:
Ucore lab1 实验
从上述代码可以看出,要求硬盘主引导扇区的大小是512字节,还需要第510个字节是0x55,第511个字节为0xAA,也就是说扇区的最后两个字节内容是0x55AA
练习2:
使用qemu执行并调试lab1中的软件。
为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
1、在初始化位置0x7c00设置实地址断点,测试断点正常。
2、从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
3、自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

2.1在初始化位置0x7c00设置实地址断点,测试断点正常。
(1)首先修改lab1/tools/gdbinit配置文件:
在其中加入两条指令:
Set architecture i8086
Target remote :1234
与qemu建立联系。
(2)在lab1的目录下,执行命令 make debug;
Ucore lab1 实验
(3)使用gdb命令 si 进行单步调试
(4)在gdb的界面下通过命令 x /2i $pc 查看BIOS的代码
Ucore lab1 实验
2.2在初始化位置0x7c00设置实地址断点,测试断点正常
(1)修改tools/gdbinit文件
在文件末尾加上:
b *0x7c00 //在0x7c00处设置断点,此地址也是bootloader的入口点地址
x /5i $pc //显示当前汇编指令
set architecture i386 //设置当前调试的cpu
(2)在lab1目录下运行 make debug
Gdb打印结果为“the target aerchitecture is assumed to be i386”.
2.3自己找一个bootloader或内核中的代码位置,设置断点进行测试
修改配置文件
Ucore lab1 实验
练习3
分析bootloader进入保护模式的过程。
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示:需要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:
为何开启A20,以及如何开启A20
如何初始化GDT表
如何使能和进入保护模式

打开A20门的代码为:
Ucore lab1 实验
3.2 初始化GDT表
代码为Ucore lab1 实验
利用 Segment Selector 寻找 GDT Entry,然后根据 GDT Entry 的 Base 寻址。
索引,即 GDT 表上的索引,用来获取 GDT 中的条目

练习4
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS
首先,查看lab1/boot/中的bootmain.c文件

Ucore lab1 实验
Ucore lab1 实验
4.1 bootloader如何读取硬盘扇区的?
Ucore lab1 实验
Ucore lab1 实验
4.2 bootloader是如何加载ELF格式的OS?
看上文的bootmain中elfhdr、proghdr相关的信息。
下面是关于elfhdr和proghdr的相关信息

#elfhdr:目标文件头
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12]; // 12 字节,每字节对应意义如下:
// 0 : 1 = 32 位程序;2 = 64 位程序
// 1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式
// 2 : 只是版本,固定为 0x1
// 3 : 目标操作系统架构
// 4 : 目标操作系统版本
// 5 ~ 11 : 固定为 0
uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image
uint16_t e_machine; // 3=x86, 4=68K, etc.
uint32_t e_version; // file version, always 1
uint32_t e_entry; // 程序入口地址 or 0
uint32_t e_phoff; // 程序段表头相对elfhdr偏移位置
uint32_t e_shoff; // 节头表相对elfhdr偏移量
uint32_t e_flags; // 处理器特定标志, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // 程序头部长度
uint16_t e_phnum; // 段个数
uint16_t e_shentsize; // 节头部长度
uint16_t e_shnum; // 节头部个数
uint16_t e_shstrndx; // 节头部字符索引
};
struct proghdr {
uint type; // 段类型
// 1 PT_LOAD : 可载入的段
// 2 PT_DYNAMIC : 动态链接信息
// 3 PT_INTERP :
// 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小
// 4 PT_NOTE : 指定辅助信息的位置和大小
// 5 PT_SHLIB : 保留类型,但具有未指定的语义
// 6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小
// 7 PT_TLS : 指定线程局部存储模板
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa; //段的第一个字节在内存中的物理地址
uint filesz; //段在文件中的长度
uint memsz; // 段在内存映像中占用的字节数
uint flags; //可读可写可执行标志位。
uint align; //段在文件及内存的对齐方式
};

练习5
实现函数调用堆栈跟踪函数
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:

……
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096
kern/debug/kdebug.c:305: print_stackframe+22
ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53
kern/init/init.c:28: kern_init+88
ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
: – 0x00007d72 –
……
请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。
提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成lab1编译后,查看lab1/obj/bootblock.asm,了解bootloader源码与机器码的语句和地址等的对应关系;查看lab1/obj/kernel.asm,了解 ucore OS源码与机器码的语句和地址等的对应关系。
要求完成函数kern/debug/kdebug.c::print_stackframe的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。
补充材料:
由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。

函数堆栈:
栈是一个很重要的编程概念,与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP寄存器的作用。一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl %ebp

movl %esp , %ebp

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以C语言默认的CDECL为例): 关于栈的生长方向,大多数编译器实现的都是向下生长。也就是栈底为高地址。
这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

实现函数:

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];
}
}

执行make debug:

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
: – 0x00007d67 –
++ setup timer interrupts
(THU.CST) os is loading …

练习6
完善中断初始化和处理
6.1 中断描述符表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
一个表项的占64bit,即8字节,其中0 ~ 15位和48 ~ 63位分别为偏移量的低16位和高16位,两者拼接为偏移量,16~31位为段选择器。
*使用段选择符中的偏移值在GDT(全局描述符表) 或 LDT(局部描述符表)中定位相应的段描述符。
*利用段描述符校验段的访问权限和范围,以确保该段是可以访问的并且偏移量位于段界限内。
*利用段描述符中取得的段基地址加上偏移量,形成一个线性地址。

6.2 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。

extern uintptr_t __vectors[];//声明__vertors[] You can use “extern uintptr_t __vectors[];” to define this extern variable which will be used later.
int i;
for(i=0;i<256;i++) {
SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);//对整个idt数组进行初始化
}
SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITCH_TOK],DPL_USER);//在这里先把所有的中断都初始化为内核级的中断
lidt(&idt_pd);//使用lidt指令加载中断描述符表 just google it! and check the libs/x86.h to know more.利用google找到了相关函数
}
//传入的第一个参数gate是中断的描述符表
传入的第二个参数istrap用来判断是中断还是trap
传入的第三个参数sel的作用是进行段的选择
传入的第四个参数off表示偏移
传入的第五个参数dpl表示这个中断的优先级//
6.3请编程完善trap.c中的中断处理函数trap
实验代码填写:

case IRQ_OFFSET + IRQ_TIMER:
/* LAB1 YOUR CODE : STEP 3 /
/
handle the timer interrupt /
/
(1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
* (3) Too Simple? Yes, I think so!
*/

代码:

ticks++;
if(ticks%TICK_NUM == 0)//每次时钟中断之后ticks就会加一 当加到TICK_NUM次数时 打印并重新开始
print_ticks();//前面有定义 打印字符串