1.1
ld bin/kernel ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o
ld将.o文件整合成可执行文件kernel,而这些.o文件是Makefile文件通过命令使用gcc把有关kernel的.c文件编译生成
+ 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!
同理ld也将.o文件整合成可执行文件bootblock,大小为488字节,但还是放入512字节扇区中,但是,而这些.o文件也是Makefile文件通过命令使用gcc把有关bootloader的.c文件编译生成
dd if=/dev/zero of=bin/ucore.img count=10000
记录了10000+0 的读入
记录了10000+0 的写出
5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0278414 s, 184 MB/s
创建10000块扇区,每个扇区512字节,制成ucore.img虚拟磁盘
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
记录了1+0 的读入
记录了1+0 的写出
512 bytes copied, 0.000466728 s, 1.1 MB/s
将bootblock存到ucore.img虚拟磁盘的第一块
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
记录了146+1 的读入
记录了146+1 的写出
74828 bytes (75 kB, 73 KiB) copied, 0.00037979 s, 197 MB/s
将kernel存到ucore.img虚拟磁盘的第二块及之后几块,注意seek1,最终ucore.img虚拟磁盘制作完成
注意:ucore很特别,第一扇区存bootloader,第二扇区以及之后存kernel...........
1.2
通过sign.c(生成一个符合标准的主引导扇区)文件可以得到:
char buf[512]; memset(buf, 0, sizeof(buf)); buf[510] = 0x55; buf[511] = 0xAA;
扇区大小为512字节,最后两字节为0x55和0xAA
2.1
直接make debug,然后gdbinit配置文件设置成:
set architecture i8086 target remote :1234 file bin/kernel break kern_init continue
使用next来单步调试
2.2
在Makefile文件中添加命令
lab1-mon: $(UCOREIMG)
注意此处为TAB开头$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null" #将qemu日志记录到q.log中
注意此处为TAB开头$(V)sleep 2
注意此处为TAB开头$(V)$(TERMINAL) -e "gdb -x tools/labinit" #使用gdb的配置文件labinit开启gdb
labinit内容
file bin/kernel #指定gdb调试目标文件
target remote :1234 #把gdb和qemu链接
set architecture i8086 #使用实模式16位
b *0x7c00 #在bootloader程序起始处设定断点,实验设定的bootloader灰被加载到0x7c00
continue #断点之后继续执行
x /2i $pc #显示对应的汇编指令和下一条汇编指令
可以使用next指令来单步调试
通过输入
x/i $cs
x/i $eip
我们可以获取当前 $cs
和 $eip
的值。其中
$cs = 0xf000
$eip = 0xfff0
我们也可以看看这个地址的指令是什么
x/2i 0xffff
得到的结果是
0xffff0: ljmp $0xf000,$0xe05b
2.3
使用x /2i $pc来观察汇编代码,然后与bootasm.S文件里16到56行代码比较即可 2.3
2.4略
3.1
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector,设置内核代码段选择符
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector,设置内核数据段选择符
.set CR0_PE_ON, 0x1 # protected mode enable flag,,设置保护模式使能标志,CR0_PE_ON设置为0x1
cli # Disable interrupts
#关中断
cld # String operations increment # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero,自己跟自己异或结果为0,且放到ax寄存器中 movw %ax, %ds # -> Data Segment,用eax寄存器数据初始化(下同) movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment
看bootloader
- 为何开启A20,以及如何开启A20
- 如何初始化GDT表
- 如何使能和进入保护模式
1.1为何打开A20?
8088/8086只有20位地址线,按理它的寻址空间是2^20,应该是1024KB,但PC机的寻址结构是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,换算成物理地址的计算方法是把segment左移4位,再加上offset,所以segment:offset所能表达的寻址空间最大应为0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh并向左移动4位的结果,后面的0ffffh是可能的最大offset),这个计算出的10ffefh大约是1088KB,就是说,segment:offset的地址表达能力超过了20位地址线的物理寻址能力,所以当你访问大于1M区域是会发生回卷现象,如果你企图寻址100001h这个地址时,你实际得到的内容是地址00001h上的内容,而下一代的基于Intel 80286 CPU的PC AT计算机系统提供了24根地址线,这样CPU的寻址范围变为 2^24=16M,同时也提供了保护模式,可以访问到1MB以上的内存了,此时如果遇到“寻址超过1MB”的情况,系统不会再“回卷”了,这就造成了向下不兼容,为了保持完全的向下兼容性,IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿以上的回绕特征,PC机在设计上在第21条地址线(也就是A20,原先20条地址线是A0-A19)上做了一个开关,当这个开关打开时,这条地址线和其它地址线一样可以使用,当这个开关关闭时,第21条地址线(A20)恒为0,这个开关就叫做A20 Gate。
PS. 0000000H-----100000H已经为1M空间,而1088K比1M多的部分就在高端内存区
在实模式下, 由于我们访问了高端内存区(1088K比1M多的部分),所以我们打开A20gate,在保护模式下,由于使用32位地址线,我们也是打开A20gate,目的是为了访问更大的内存区域
1.2打开A20 Gate的具体步骤大致如下(参考bootasm.S):
- 等待8042 Input buffer为空;
- 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
- 等待8042 Input buffer为空;
- 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer
seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty).
#从0x64端口读入一个字节的数据到al中 testb $0x2, %al
#如果上面的测试中发现al的第2位为0,就不执行该指令 jnz seta20.1 #循环检查 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port #将al中的数据写入到端口0x64中 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
2.初始化GDT表
GDT全称是Global Descriptor Table,也就是全局描述符表。在保护模式下,我们通过设置GDT将内存空间被分割为了一个又一个的segment(这些segment是可以重叠的),这样我们就能实现不同的程序访问不同的内存空间。这和实模式下的寻址方式是不同的, 在实模式下我们只能使用address = segment << 4 | offset的方式进行寻址(虽然也是segment + offset的,但在实模式下我们并不会真正的进行分段)。在这种情况下,任何程序都能访问整个1MB的空间。而在保护模式下,通过分段的方式,程序并不能访问整个内存空间
在实模式下, 逻辑地址由段选择子(保存在段寄存器中)和段选择子偏移量组成. 其中, 段选择子16bit, 段选择子偏移量是32bit. 下面是段选择子的示意图:
- 在段选择子中,其中的INDEX[15:3]是GDT的索引。
- TI[2:2]用于选择表格的类型,1是LDT,0是GDT。
- RPL[1:0]用于选择请求者的特权级,00最高,11最低
段描述符
主要作用是保存[31..24]的段基址
所以地址转换方式为:
- 硬件自动将CPU给的逻辑地址分离出段选择子。
- 利用这个段选择子在GDT中选择一个段描述符。
- 将段描述符里的Base Address和逻辑地址的偏移量相加而得到线性地址(没有页机制下就是物理地址)。
#define SEG_NULLASM .word 0, 0; .byte 0, 0, 0, 0 #define SEG_ASM(type,base,lim) .word (((lim) >> 12) & 0xffff), ((base) & 0xffff); .byte (((base) >> 16) & 0xff), (0x90 | (type)), (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
static inline void lgdt(struct pseudodesc *pd) { asm volatile ("lgdt (%0)" :: "r" (pd)); asm volatile ("movw %%ax, %%gs" :: "a" (USER_DS)); asm volatile ("movw %%ax, %%fs" :: "a" (USER_DS)); asm volatile ("movw %%ax, %%es" :: "a" (KERNEL_DS)); asm volatile ("movw %%ax, %%ds" :: "a" (KERNEL_DS)); asm volatile ("movw %%ax, %%ss" :: "a" (KERNEL_DS)); // reload cs asm volatile ("ljmp %0, $1f 1: " :: "i" (KERNEL_CS)); }
lgdt gdtdesc
# Bootstrap GDT .p2align 2 # force 4 byte alignment,强制4字节对齐 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,数据段描述符
gdtdesc: #GDTR所要存的内容 .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
理论上GDT可以存在内存中任何位置,但在实模式下只有寻址空间只有1M情况下初始化GDT,GDT只能在这1M空间中,CPU通过lgdt指令读入GDT的地址,之后我们就可以使用GDT了。
3.如何使能和进入保护模式
CR0中包含了6个预定义标志,0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
movl %cr0, %eax cr0->eax orl $CR0_PE_ON, %eax 相与结果0x1放入eax movl %eax, %cr0 cr0值为0x1
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode protcseg: # Set up the protected-mode data segment registers movw $PROT_MODE_DSEG, %ax # Our data segment selector
设定数据段选择符到ax寄存器,下面将寄存器初始化 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
设置栈指针,并且调用c函数 movl $start, %esp call bootmain # If bootmain returns (it shouldn't), loop.如果bootmain返回的话,就一直循环
spin:
jmp spin
Bootload的启动过程可以概括如下:
首先,BIOS将第一块扇区(存着bootloader)读到内存中物理地址为0x7c00的位置,同时段寄存器CS值为0x0000,IP值为0x7c00,之后开始执行bootloader程序。CLI屏蔽中断;CLD使DF复位,即DF=0,通过执行cld指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)。设置寄存器 ax,ds,es,ss寄存器值为0;A20门被关闭,高于1MB的地址都默认回卷到0,所以要激活A20,给8042发命令激活A20,8042有两个IO端口:0x60和0x64, 激活流程: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60,打开A20门。从实模式转换到保护模式(实模式将整个物理内存看成一块区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址,地址就是IP值。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的),所以就初始化全局描述符表使得虚拟地址和物理地址匹配可以相互转换;lgdt汇编指令把通过gdt处理后的(asm.h头文件中处理函数)描述符表的起始位置和大小存入gdtr寄存器中;将CR0的第0号位设置为1,进入保护模式;指令跳转由代码段跳到protcseg的起始位置。设置保护模式下数据段寄存器;设置堆栈寄存器并调用bootmain函数;
4
static void readsect(void *dst, uint32_t secno) { // wait for disk to be ready waitdisk(); //读取扇区内容 outb(0x1F2, 1); // count = 1 outb使用内联汇编实现 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(); //将扇区内容加载到内存中虚拟地址dst // read a sector insl(0x1F0, dst, SECTSIZE / 4); //也用内联汇编实现 }
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 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); } }
void bootmain(void) { // read the 1st page off disk readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);//从硬盘读取第一页即ELF文件到内存(ELF文件读到内存的起始位置,大小,ELF文件偏移)
// is this a valid ELF? if (ELFHDR->e_magic != ELF_MAGIC) {//判断所得的ELF文件格式是否合法 goto bad; } struct proghdr *ph, *eph;//设定两个程序头表指针 // load each program segment (ignores ph flags) ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);//程序头表头指针,ELF文件起始地址加上程序头表的偏移量(记录在ELF格式中) eph = ph + ELFHDR->e_phnum; //程序头表尾指针,e_phnum为程序头表中段个数(记录在节区格式中) for (; ph < eph; ph ++) { readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); //循环读取ELF程序头表中每个段到内存中 } // call the entry point from the ELF header // note: does not return ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); //跳到内核程序入口地址,将cpu控制权交给ucore内核代码 bad: outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); /* do nothing */ while (1); }
程序头表中段结构信息
struct proghdr { uint type; // 段类型 uint offset; // 段相对文件头的偏移值 uint va; // 段的第一个字节将被放到内存中的虚拟地址 uint pa; uint filesz; uint memsz; // 段在内存映像中占用的字节数 uint flags; uint align; };
5.
代码如下:
void print_stackframe(void) { uint32_t ebp = read_ebp(),eip = read_eip(); //使用内联函数获取两寄存器值 int i,j; for(i = 0;ebp!=0 && i < STACKFRAME_DEPTH;++i){ cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip); uint32_t *args = (uint32_t*)ebp+2; //观察原先函数的局部变量值 for(j = 0;j < 4;++j){ cprintf("0x%08x ", args[j]); } cprintf(" "); print_debuginfo(eip-1); //输出执行代码的文件名,行号,函数名,当前eip值相对于函数起始地址偏移量 eip = ((uint32_t*)ebp)[1]; //调整eip到原先函数的call的下一条指令 ebp = ((uint32_t*)ebp)[0]; //ebp换成原先函数的ebp } }
调用C函数的时候,先将参数按照执行顺序从后到前压到栈里,然后压入call语句的下一条指令的地址,然后将ebp的值压入栈中,之后将esp的值赋给ebp,然后再调整eip的值为函数入口地址。
6.1
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 };
16+16+5+3+...+16 = 64bit = 8byte
其中 gd_off_15_0 : 16 和gd_off_31_16 : 16表示低偏移量和高偏移量再加上gd_ss : 16;为段选择子到GDT中找到段描述符可得基址,程序入口地址为基址+偏移量
6.2
.globl __alltraps .globl vector0 vector0: pushl $0 pushl $0 jmp __alltraps .globl vector1 vector1: pushl $0 pushl $1 jmp __alltraps .globl vector2 vector2: pushl $0 pushl $2 jmp __alltraps .globl vector3 vector3: pushl $0 pushl $3 jmp __alltraps .globl vector4 vector4: pushl $0 pushl $4 jmp __alltraps .globl vector5 vector5: pushl $0 pushl $5 jmp __alltraps .globl vector6 vector6: pushl $0 pushl $6 jmp __alltraps .globl vector7 vector7: pushl $0 pushl $7 jmp __alltraps
#define SETGATE(gate, istrap, sel, off, dpl) { (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; //0xffff为16位,32位与16位相与只能得到后16位赋值给低16位偏移量 (gate).gd_ss = (sel); (gate).gd_args = 0; (gate).gd_rsv1 = 0; (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; (gate).gd_s = 0; (gate).gd_dpl = (dpl); (gate).gd_p = 1; (gate).gd_off_31_16 = (uint32_t)(off) >> 16; //32位右移16位剩下0000xxxx赋值给高16位偏移量 }
void idt_init(void) { extern uintptr_t __vectors[];//引用vector.S文件中数组 int i; for(i = 0;i < sizeof(idt)/sizeof(struct gatedesc);++i){ SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);GD_KTEXT为kernel text,_vector[i]为偏移量,DPL只能为DPL_KERNEL,因为只能在内核下才能执行中断处理程序 } SETGATE(idt[T_SWITCH_TOU],1,GD_KTEXT,__vectors[T_SWITCH_TOU],DPL_USER);//设定系统调用对应的中断向量,DPL只能为DPL_USER,因为查找系统调用表时还在用户态 lidt(&idt_pd);//将IDT位置赋值给IDTR }
时钟中断:
static void trap_dispatch(struct trapframe *tf) { char c; switch (tf->tf_trapno) { case IRQ_OFFSET + IRQ_TIMER: ticks++; //使用一个全局变量来记录时钟 if (ticks == TICK_NUM) {//TICK_NUM就是固定的100,每到100便调用print_ticks()函数 ticks -= TICK_NUM; print_ticks(); } break;