练习1:
在此练习中,大家需要通过静态分析代码来了解:
- 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
执行make:
+ cc kern/init/init.c
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
+ cc kern/libs/stdio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
+ cc kern/driver/intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
+ cc kern/trap/trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c
gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o
+ 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
+ cc boot/bootasm.S
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c
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
+ ld bin/bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 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.0410345 s, 125 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.00270139 s, 190 kB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74931 bytes (75 kB) copied, 0.00432275 s, 17.3 MB/s
1.
通过在makefile文件中查找有关ucore.img的代码:
创建ucore.omg:
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)
call函数是唯一一个可以创建定制化参数函数的引用函数
- 函数语法:$(call variable,param,param,…)
- 函数功能:在执行时,将它的参数“ param”依次赋值给临时变量“ $(1)”、“ $(2)” call 函数对参数的数目没有限制,也可以没有参数值,没有参数值的“ call”没有任何实际存在的意义。执行时变量“ variable”被展开为在函数上下文有效的临时变量,变量定义中的“ $(1)”作为第一个参数,并将函数参数值中的第一个参数赋值给它;变量中的“ $(2)”一样被赋值为函数的第二个参数值;依此类推(变量 $ (0)代表变量“ variable”本身)。之后对变量“ variable” 表达式的计算值。
- 返回值:参数值“ param”依次替换“ $(1)”、“ $(2)”…… 之后变量“ variable”定义的表达式的计算值。
例子:
reverse = $(2)$(1)
foo = $(call reverse,a,b)
all:
@echo "foo=$(foo)"
结果:
foo=ba
生成ucore.omg 需要kernel和bootblock文件
//dd : 为Linux下的指令,作用为用指定大小的块拷贝一个文件,并在拷贝的同时进行转换
//if=文件名: 输入文件名 of=文件名 : 输出文件名
//count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数
//conv=conversion:用指定的参数转换文件, notrunc: 不截短输出文件
//seek=blocks:从输出文件开头跳过blocks个块后再开始复制
//$@: 目标文件
// /dev/zero: /dev/zero文件代表一个永远输出 0的设备文件,使用它作输入可以得到全为空的文件
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
指令的意图:先将当前目标文件清空10000个块,然后讲bootblock 和 kernel 作为输入,以不截短的方式输出到ucore.img中,其中kernel文件跳过一个块以后再开始复制;
2.
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
在/lab1/tools/sign.c 文件中:
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
符合规范的硬盘主引导扇区的特征是
扇区大小为512字节
练习2:
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
在初始化位置0x7c00设置实地址断点,测试断点正常。
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
执行make lab1-mon
file bin/kernel
target remote :1234
set architecture i8086
b *0x7c00 //在0x7c00处打断点
continue //继续运行
x /2i $pc //查看当前指令的两位地址
练习3
为何开启A20,以及如何开启A20
如何初始化GDT表
如何使能和进入保护模式
- (1)因为一开始A20的地址是被屏蔽的,需要通过系统软件的I/O去开启它(bootasm.S),在实模式下要访问高端内存区,需要开启A20,而到了保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0–1M、2-3M、4-5M…,如果不打开A20,就无法访问到更多的内存区,所以必须打开。(实模式下,所有程序都可以被内核和用户修改,这样做可能会造成不可挽回的后果,所以需要切换到保护模式下);
(2)
seta20.1: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.1 //al的第2位为0,则跳出循环
movb $0xd1, %al //将0xd1写入al中
outb %al, $0x64 //将0xd1写入到0x64端口中
seta20.2: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.2 //al的第2位为0,则跳出循环
movb $0xdf, %al //将0xdf入al中
outb %al, $0x60 //将0xdf入到0x64端口中,打开A20
通过 0x60和0x64来控制A20的开启;
全局描述符表:
大小为 = 0x17 + 1 = 0x18,为24字节
全局描述符表每项为8字节,一共有三项,且第一项为空白项,则剩下的两项为代码项和数据项;
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address 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
lgdt gdtdesc //加载
movl %cr0, %eax //CR0** — 包含系统控制标志,控制处理器的操作模式和状态 把cr0的值传给eax
orl $CR0_PE_ON, %eax //两个寄存器或操作---》相当于打开
movl %eax, %cr0 //再把操作传给cr0
练习4:
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,理解:
-
1.bootloader如何读取硬盘扇区的?
2.bootloader是如何加载ELF格式的OS?表一 磁盘IO地址和对应功能
第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1
-
当前 硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程(可参看boot/bootmain.c中的readsect函数实现)大致如下:
等待磁盘准备好
发出读取扇区的命令
等待磁盘准备好
把磁盘扇区数据读到指定内存
/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40) //看高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回
/* 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 读取一个扇区
// 读取的扇区起始编号共28位,分成4部分依次放在0x1F3~0x1F6寄存器中
outb(0x1F3, secno & 0xFF); //读取0-8位
outb(0x1F4, (secno >> 8) & 0xFF); //读取8-16位
outb(0x1F5, (secno >> 16) & 0xFF); //读取16-24位
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); //24-28位
outb(0x1F7, 0x20); // cmd 0x20 - read sectors 不忙则读取数据
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); //硬盘再次空闲后,开始从0x1F0寄存器中读数据。注意insl的作用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字节为单位的,因此这里SECTIZE需要除以4.
}
ELF header在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构,其定义在elf.h中:
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
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
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
};
程序头文件的数据结构:
struct proghdr {
uint type; // 段类型
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;
uint filesz;
uint memsz; // 段在内存映像中占用的字节数
uint flags;
uint align;
};
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过储存在头部的幻数判断是否是合法的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文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// 根据ELF头部储存的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
问题5
由于代码答案已经在文件里给出了,所以就去查明白 ebp,eip,esp的原理
EIP存储着下一条指令的地址,每执行一条指令,该寄存器变化一次。
EBP存储着当前函数栈底的地址,栈低通常作为基址,我们可以通过栈底地址和偏移相加减来获取变量地址。
ESP始终指向栈顶,只要ESP指向变了,那么当前栈顶就变了。
问题6:
请完成编码工作和回答如下问题:
- 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
- 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。