ucore_lab学习笔记lab1
练习1
1、操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
生成ucore.imge的代码如下:
$(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)
用zero块设备作为输入生成一个大小为10000B的块
然后先将bootblock即BootLoader写入块中接着是kernel
输入“make V=”的时候生成了以下信息:
+ cc kern/init/init.c
+ cc kern/libs/readline.c
+ cc kern/libs/stdio.c
+ cc kern/debug/kdebug.c
+ cc kern/debug/kmonitor.c
+ cc kern/debug/panic.c
+ cc kern/driver/clock.c
+ cc kern/driver/console.c
+ cc kern/driver/intr.c
+ cc kern/driver/picirq.c
+ cc kern/trap/trap.c
+ cc kern/trap/trapentry.S
+ cc kern/trap/vectors.S
+ cc kern/mm/pmm.c
+ cc libs/printfmt.c
+ cc libs/string.c
+ ld bin/kernel
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
+ ld bin/bootblock
生成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
生成bootblock的细节:
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o
obj/boot/bootmain.o
-o obj/bootblock.o
-e 指定入口 -Ttext指定代码起始地址
2、一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
- 磁盘主引导扇区只有512字节
- 磁盘最后两个字节为0x55AA
- 由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符组成
练习2
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
其实就是用qemu和gdb进行调试,需要了解一些简单的gdb命令
执行以下命令
make debug
将加载tools/gdbinit文件的配置设置gdb
#######tools/gdbinit#######
file bin/kernel
target remote:1234
break kern_init
continue
file 加载被调试的可执行文件
target 连接qemu进行调试
break或b 设置断点
continue 继续运行
修改tools/gdbinit文件为
#######tools/gdbinit#######
file obj/bootblock.o
target remote:1234
break *0x7c00
continue
运行
make debug
有以下信息
输入s/step n/next si/stepi(step in 的意思) ni/nextin进行单步调试。
附:
以Intel 80386为例,计算机加电后,CPU从物理地址0xFFFFFFF0(由初始化的CS:EIP确定,此时CS和IP的值分别是0xF000和0xFFF0))开始执行。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。
2.在初始化位置0x7c00设置实地址断点,测试断点正常。
就是b *0x7c00
3.从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
define hook-stop
x/i $pc
end
可以对指令反汇编,然后进行比较
4.自己找一个bootloader或内核中的代码位置,设置断点并进行测试
略
练习3
1、BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
实模式下(16位模式),只能访问1MB物理内存空间。必须通过修改A20地址线(就是指地址总线中的第20根)才能从实模式转换到保护模式。
查看bootasm.s文件。
# 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: #开启A20
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 #加载GDT表
movl %cr0, %eax #修改CR0的第0位为1,表示开启保护模式
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 #长跳转到32位代码段,重装CS和EIP
.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 #设置栈,从而可以跳转到c语言代码
call bootmain
完成了从实模式到保护模式,然后设置段寄存器,最后跳转到bootmain
练习4
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。
1、一个扇区512字节。要了解如何读取扇区,就要知道elf的头
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC elf的魔数
uint8_t e_elf[12];
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; // entry point if executable 入口地址
uint32_t e_phoff; // file position of program header or 0第一个programheader的位置,
//这是个结构体,通过这个指针可以找到结构体数组的位置结合e_phnum可以取得所有ph结构体
uint32_t e_shoff; // file position of section header or 0
uint32_t e_flags; // architecture-specific flags, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // size of an entry in program header
uint16_t e_phnum; // number of entries in program header or 0
uint16_t e_shentsize; // size of an entry in section header
uint16_t e_shnum; // number of entries in section header or 0
uint16_t e_shstrndx; // section number that contains section name strings
};
/* program section header */
struct proghdr {
uint32_t p_type; // loadable code or data, dynamic linking info,etc.
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
uint32_t p_pa; // physical address, not used
uint32_t p_filesz; // size of segment in file
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
uint32_t p_flags; // read/write/execute bits
uint32_t p_align; // required alignment, invariably hardware page size
};
加载OS的对应代码
bootmain(void) {
..........
//首先判断是不是ELF
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; //最后一个程序头结构体的地址
//按照程序头表的描述,将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
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
理解实验指导书中函数堆栈的内容则不难完成这一练习。
1、函数堆栈
一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
pushl %ebp
movl %esp , %ebp
这里有个小细节要小心,其实查看bootasm.S中的源码也可以发现的,在gun下采用的是AT&T汇编指令 和intel格式的汇编指令有一些区别,主要是源操作数与目的操作数,位置相反
例如:AT&T: movl %eax, %ebx Intel: mov ebx, eax
表示将eax寄存器的值移到ebx寄存器中。
这样就可以知道函数调用栈结构如下:
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 |
| 上一层[ebp] | <-------- [ebp]
| 局部变量 | 低位地址
函数调用可以描述为以下几个步骤:
- 1、参数入栈:将参数从右向左依次压入栈中。
- 2、返回地址入栈:call指令内部隐含的动作,将call的下一条指令入栈
- 3、代码区跳转:跳转到被调用函数入口处
- 4、函数入口处前两条指令,为本地编译器自动插入的指令,执行这两条指令
- 4.1、将ebp的值入栈,即调用之前的那个栈帧的底部
- 4.2、将当前的esp值赋给ebp,当前的esp即为新的函数的栈帧的底部,保存到ebp中
- 4.3、给新栈帧分配空间(把ESP减去所需空间的大小)。
函数返回的大概步骤:
- 1、保存返回值,通常将函数返回值保存到寄存器EAX中。
- 2、将当前的ebp赋给esp
- 3、从栈中弹出一个值给ebp
- 4、弹出返回地址,从返回地址处继续执行
就是一个相反的过程。
对于函数调用时为什么会有4、3这个步骤,有如下解释:
 深入理解计算机系统一书P154这样描述:“GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括保存%ebp值的4个字节和返回值的4个字节,采用这个规则是为了保证访问数据的严格对齐。”
以下为使用objdump -S反汇编得到的指令:
/* cputchar - writes a single character to stdout */
void
cputchar(int c) {
100338: 55 push %ebp
100339: 89 e5 mov %esp,%ebp
10033b: 83 ec 18 sub $0x18,%esp
cons_putc(c);
10033e: 8b 45 08 mov 0x8(%ebp),%eax
100341: 89 04 24 mov %eax,(%esp)
100344: e8 9f 11 00 00 call 1014e8 <cons_putc>
}
100349: c9 leave
10034a: c3 ret
其中leave指令相当于
mov %ebp,%esp
pop %ebp
2、print_stackframe函数的实现
uint32_t ebp,eip,i,j;
ebp = read_ebp();
eip = read_eip();
for(i=0;(i<STACKFRAME_DEPTH)&&(ebp>0);i++){
cprintf("ebp:0x%08x eip:0x%08x ",(uint32_t*)ebp,(uint32_t*)eip);
cprintf("args:");
for(j=0;j<4;j++){
cprintf("0x%08x ",*((uint32_t*)ebp+2+j));
}
cprintf("\n");
print_debuginfo(eip-4);
eip = *((uint32_t*)ebp+1);
ebp = *((uint32_t*)ebp);
}
了解函数调用栈结构即可很容易实现。
print_debuginfo(eip-4);
这一行代码可以找出某个地址处于哪个函数当中。
参考答案是eip-1,我觉得eip-4也行,eip = *((uint32_t*)ebp+1);得到的地址是函数返回地址,减1即处于函数调用的指令处了。
ebp:0x00007b08 eip:0x001009a6 args:0x00010094 0x00000000 0x00007b38 0x00100092
kern/debug/kdebug.c:307: print_stackframe+18
ebp:0x00007b18 eip:0x00100c9b args:0x00000000 0x00000000 0x00000000 0x00007b88
kern/debug/kmonitor.c:125: mon_backtrace+7
ebp:0x00007b38 eip:0x00100092 args:0x00000000 0x00007b60 0xffff0000 0x00007b64
kern/init/init.c:48: grade_backtrace2+30
ebp:0x00007b58 eip:0x001000bb args:0x00000000 0xffff0000 0x00007b84 0x00000029
kern/init/init.c:53: grade_backtrace1+35
ebp:0x00007b78 eip:0x001000d9 args:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+20
ebp:0x00007b98 eip:0x001000fe args:0x001032fc 0x001032e0 0x0000130a 0x00000000
kern/init/init.c:63: grade_backtrace+31
ebp:0x00007bc8 eip:0x00100055 args:0x00000000 0x00000000 0x00000000 0x00010094
kern/init/init.c:28: kern_init+81
ebp:0x00007bf8 eip:0x00007d68 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d64 --
++ setup timer interrupts
在我的实验中,结果如上。
练习6
1、中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
参照实验指导书中中断与异常部分。
IDT(Interrupt Description Table)一个表项为8个字节。
/* Gate descriptors for interrupts and traps */
struct gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_ss : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
由gd_ss与gd_off构成中断处理代码入口。
2、请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
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);
}
lidt(&idt_pd);
#define SETGATE(gate, istrap, sel, off, dpl)
使用SETGATE设置每一个中段描述符表项。
3、请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
在kern/driver/clock.h中声明了ticks为extern。
在这里直接用就是了。
ticks++;
if((ticks%TICK_NUM)==0){
print_ticks();
}
扩展练习
………….