练习1: 理解通过make生成执行文件的过程
[练习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)
要生成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的代码,由宏定义批量实现了。
下列代码为生成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了。
[练习2]
- 从 CPU加电后执行的第一条指令开始,单步跟踪 BIOS的执行。
- 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
- 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S和 bootblock.asm进行比较。
- 自己找一个 bootloader或内核中的代码位置,设置断点并进行测试
- 首先通过make qemu指令运行出等待调试的qemu虚拟机,然后再打开一个终端,通过下述命令连接到qemu虚拟机:
gdb
target remote 127.0.0.1:1234
-
在lab1目录下执行gdb -x tools/gdbinit使gdb处于等待连接状态
-
在新窗口lab1目录下执行make debug
[----------------------------------registers-----------------------------------]
EAX: 0x0 (0x00000000)
EBX: 0x0 (0x00000000)
ECX: 0x0 (0x00000000)
EDX: 0x663 --> 0x0 (0x00000000)
ESI: 0x0 (0x00000000)
EDI: 0x0 (0x00000000)
EBP: 0x0 (0x00000000)
ESP: 0x0 (0x00000000)
EIP: 0xfff0 --> 0x0 (0x00000000)
EFLAGS: 0x2 (carry parity adjust zero sign trap interrupt direction overflow)
[-------------------------------------code-------------------------------------]
0xffea: add BYTE PTR [eax],al
0xffec: add BYTE PTR [eax],al
0xffee: add BYTE PTR [eax],al
=> 0xfff0: add BYTE PTR [eax],al
0xfff2: add BYTE PTR [eax],al
0xfff4: add BYTE PTR [eax],al
0xfff6: add BYTE PTR [eax],al
0xfff8: add BYTE PTR [eax],al
[------------------------------------stack-------------------------------------]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGTRAP
0x0000fff0 in ?? ()
Breakpoint 1 at 0xffff0
gdb-peda$ x /i 0xffff0
0xffff0: jmp 0x3630:0xf000e05b
gdb-peda$
- 此时CPU从物理地址0xFFFFFFF0(由初始化的CS:EIP确定,此时CS和IP的值分别是0xF000和0xFFF0))开始执行。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。
- BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。
在初始化位置0x7c00 设置实地址断点,测试断点正常
b *0x7c00
c
自己找一个bootloader或内核中的代码位置,设置断点并进行测试
b kern_init
c
[练习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
然后就是将A20置1,这里简单解释一下A20,当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。
seta20.1:
inb $0x64, %al # 读取状态寄存器,等待8042键盘控制器闲置
testb $0x2, %al # 判断输入缓存是否为空
jnz seta20.1
movb $0xd1, %al # 0xd1表示写输出端口命令,参数随后通过0x60端口写入
outb %al, $0x64
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 通过0x60写入数据11011111 即将A20置1
outb %al, $0x60
3、加载GDT表
lgdt gdtdesc
4、将CR0的第0位置1
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
5、重装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
[练习4]
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) {
..........
//首先判断是不是ELF
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
//ELF头部有描述ELF文件应加载到内存什么位置的描述表,这里读取出来将之存入ph
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]
1、函数堆栈的原理
理解函数堆栈最重要的两点是:栈的结构,以及EBP寄存器的作用。
一个函数调用动作可分解为零到多个 PUSH指令和一个 CALL 指令。CALL 指令内部其实还暗含了一个将返回地址压栈的动作,这是由硬件完成的。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
pushl %ebp
movl %esp,%ebp
这两条汇编指令的含义是:首先将ebp 寄存器入栈,然后将栈顶指针 esp 赋值给 ebp。
movl %esp %ebp这条指令表面上看是用esp覆盖 ebp原来的值,其实不然。因为给 ebp赋值之前,
原ebp 值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个
非常重要的地位,该寄存器中存储着栈中的一个地址(原 ebp入栈后的栈顶),从该地址为基准,
向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址
处又存储着上一层函数调用时的ebp值。
而由此我们可以直接根据ebp就能读取到各个栈帧的地址和值,一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用 4 字节内存,对应32位系统),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层 ebp 值。
2、print_stackframe函数的实现
首先我们直接看到print_stackframe函数的注释:
void print_stackframe(void) {
/* 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 (unit32_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]
*/
}
这样我们直接根据注释以及之前的相关知识就能比较简单的编写成程序,如下所示:
void print_stackframe(void) {
uint32_t ebp=read_ebp();//(1) call read_ebp() to get the value of ebp. the type is (uint32_t)
uint32_t eip=read_eip();//(2) call read_eip() to get the value of eip. the type is (uint32_t)
int i;
for(i=0;i<STACKFRAME_DEPTH&&ebp!=0;i++){//(3) from 0 .. STACKFRAME_DEPTH
cprintf("ebp:0x%08x eip:0x%08x ",ebp,eip);//(3.1)printf value of ebp, eip
uint32_t *tmp=(uint32_t *)ebp+2;
cprintf("arg :0x%08x 0x%08x 0x%08x 0x%08x",*(tmp+0),*(tmp+1),*(tmp+2),*(tmp+3));//(3.2)(uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
cprintf("\n");//(3.3) cprintf("\n");
print_debuginfo(eip-1);//(3.4) 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];//(3.5) popup a calling stackframe
}
}
[练习6.1]
中断描述符表一个表项占8字节。其中015位和4863位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。
bootmain.c源代码
#include <defs.h>
//定义类型、指针的位数,以及对地址的处理
#include <x86.h>
//定义I/O输入输出,处理描述符表的汇编指令,以及用汇编编写字符串处理函数
#include <elf.h>
//定义elf文件头以及进程头信息
/* *********************************************************************
* This a dirt simple boot loader, whose sole job is to boot
* an ELF kernel image from the first IDE hard disk.
*
* DISK LAYOUT
* * This program(bootasm.S and bootmain.c) is the bootloader.
* It should be stored in the first sector of the disk.
*
* * The 2nd sector onward holds the kernel image.
*
* * The kernel image must be in ELF format.
*
* BOOT UP STEPS
* * when the CPU boots it loads the BIOS into memory and executes it
*
* * the BIOS intializes devices, sets of the interrupt routines, and
* reads the first sector of the boot device(e.g., hard-drive)
* into memory and jumps to it.
*
* * Assuming this boot loader is stored in the first sector of the
* hard-drive, this code takes over...
*
* * control starts in bootasm.S -- which sets up protected mode,
* and a stack so C code then run, then calls bootmain()
*
* * bootmain() in this file takes over, reads in the kernel and jumps to it.
* */
#define SECTSIZE 512
#define ELFHDR ((struct elfhdr *)0x10000) // scratch space
/* waitdisk - wait for disk ready */
//等待磁盘就绪
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
//读取段
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 - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
//readseg - 将@offset上的@count字节从内核读入虚拟地址@va
//读段寄存器值
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// round down to sector boundary
//向下到扇区边界
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
//从字节转换为扇区;内核从扇区1开始
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
//从磁盘上读取段寄存器值
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
//ELF是否有效
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
//加载每个程序段(忽略ph标志)
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
//调用ELF文件头的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}