获取完资源就开始干了。
我这里就做了个大概的内容分析,具体的还是跟着那几个大佬吧
前面一大堆都是没用的,就是告诉一些作业的提交。
Part 1: PC Bootstrap
这个并没有要你做啥,就是让你 熟悉下汇编。
然后让你知道怎么运行那个内核的,这些都是些不用讲的东西,看看就行了。
说一下,这个内核退出说是ctrl+a x
,意思是先按ctrl+a
,再按x
然后就开始来重点了,第一个是让你知道现在计算机内存的分布。
The PC’s Physical Address Space(PC的物理地址空间)
The ROM BIOS
后面就比较直接了,直接让你去运行这个内核,一步步来看他是怎么运行的。
就是让你这样运行两个终端,跑GDB 调试,第一个运行make qemu-gdb
,第二个运行make gdb
。
不出意外,在make gdb
里面出现的第一条指令是跳转指令。
跳到了哪里去,这个地址官方是给了你解释的,就是段地址*16加偏移地纸,很显然,他的地址是在BIOS中。
然后回让你一步步执行看发生了什么。
不难发现,计算机的运行最先开始的是 BIOS。
后面一步步,执行的内容我就看不懂,只知道他就做了一些初始化工作,想了解的去看看大佬的博客吧。
Part 2: The Boot Loader
初始化完成BIOS 之后,就运行Boot Loader,就是引导操作系统,这个东西一直都是放在计算机磁盘的第一个扇区(也用可能是其他的引导,这个百度搜一搜能搜出来,另外一个最常见的就是U盘启动,学计算机的重装系统至少也有十次八次了吧,所以这个很容易理解,我们重装系统的步骤不就是现在BIOS里面选了U盘引导,干的就是这个了)。然后BIOS会把这个运行的引导程序装入到内存0x7c00-0x7dff
。我不知道现代操作系统是不是这,但是这个系统是的。
这个引导系统主要就干了两件事:
- 实模式转换成保护模式,区别百度一下有讲解的,另外发现了另一个操作系统学习资源,好像文档还挺齐全的。
- 引导加载程序通过x86的特殊I / O指令直接访问IDE磁盘设备寄存器,从而从硬盘读取内核。
后面会让你去看源码,知道怎么运行的Boot Loader,这个各位就自己翻翻博客吧,我就不细讲了,贴个自己的大致介绍。
#include <inc/mmu.h>
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
.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
.globl start
start: # 开始程序首先关了中断,和串处理操作的移动指针(这个暂时不用管)
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
#初始化 数据段 扩展段 和 栈 的段寄存器 用于保护模式
# Set up the important data segment registers (DS, ES, SS).
xorw %ax,%ax # Segment number zero
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment
# 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
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to their physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc # 存放 GDT表信息
movl %cr0, %eax # 用或操作 把最后一位置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模式下了
.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
# 简单来讲 上面 就是初始化了一些寄存器,然后就去 boot main 里面了
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin: # 这个翻译上面英文就好
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt: # GDT 表信息,具体的不清楚
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
由上面这注释,应该很容易得出他问的问题的答案。
后面有一个让你运行一个C语言指针程序,那个有点基础应该就看得懂。就不多说了。
main.c里面,主要是加载ELF文件,也就是你的操作系统,什么是ELF,这个可以在百度百科里面了解到。
boot main.c
#include <inc/x86.h>
#include <inc/elf.h>
/**********************************************************************
* 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(boot.S and main.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 boot.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 Elf *) 0x10000) // scratch space
void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// read 1st page off disk 把内核的起始地址第一个页加载到内存,ELFHDR处 一页为 512*8=4M
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);//一般是操作系统映象文件的elf 头部
// is this a valid ELF? 是一个ELF文件 就继续否侧失败
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
//加载 程序表头到 ph
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;//这个是 表未 e_phnum 存的是表项个数
for (; ph < eph; ph++) //这个就是把表里面的都加载到内存
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();//e_entry 是程序运行的入口 也就是在这个时候 操作系统开始加载了
bad: //具体干啥的 我不知道 如果没猜错 就是执行 没有加载到系统的操作,
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
// 顾名思义 ,就是加载 偏移量为 offset 的 连续 count 个字节 到地址 pa
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
uint32_t end_pa;
end_pa = pa + count;
// round down to sector boundary
pa &= ~(SECTSIZE - 1);
// translate from bytes to sectors, and kernel starts at sector 1
offset = (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.
while (pa < end_pa) {
// Since we haven't enabled paging yet and we're using
// an identity segment mapping (see boot.S), we can
// use physical addresses directly. This won't be the
// case once JOS enables the MMU.
readsect((uint8_t*) pa, offset);
pa += SECTSIZE;
offset++;
}
}
void
waitdisk(void)
{ // 判磁盘是不是准备 好了
// wait for disk reaady
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
void
readsect(void *dst, uint32_t offset)
{
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, offset);
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector 读一个扇区
insl(0x1F0, dst, SECTSIZE/4);
}
后面几步还是要自己好好坐一坐,我没有做什么详细的介绍。
一如既往大佬博客,像我这样的菜鸡是无法理解的。
最后让你做个测试,运行Boot Loader 前看下0x00100000
处的8个内存字,运行后再看一下,发现原本是 全是 0
,后面就有了一大堆乱七八糟的值。这个不是很明显吗,看我上面main.c
的注释,明显有个函数把硬盘里面的值读到了内存。
Part 3: The Kernel
后面的实验,这个大佬的GitHub就全都有了。
我做个简单的笔记。
movl %eax, %cr0
这条语句实现了,映射,将高地址映射到了物理地址的低地址,这是因为计算机希望操作系统是运行在高地址,用户运行在低地址,详细内容,下一次实验会讲清楚。
练习8 让你完善那个printf
里面的%o
也就是8进制输出。
这个很容易了,把前面16进制输出复制一遍就行了,不做详细介绍了。
然后你需要回答4个问题
printf
之间的关系依旧找大佬的博客
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';
6 crt_pos -= CRT_COLS;
7 }
解释这个,看起来就是如果超过了一页,就把最前面的那一行删掉??,然后继续输出下一行???不知道理解对没。
- 这个是个重点 ,这个实验很容易看出来C语言压栈的顺序是从后往前压栈。
- 这个不说了,就是按格式输出
- 这个就是,我们经常用的
printf
,如果少了一个参数会咋样,通过看内存应该就知道,会输出一个随机值。多一个多的其实就没啥用。 - 这个就是压栈是从后往前压,所以可以实现,多个参数。如果是从前往后压该怎么办?我还不知道怎么处理。
练习9 问啥时候初始化堆栈,这个看看大佬的博客就行.
练习10 这个就是让你明白是怎么调用函数的,又是怎么返回的。
一般来说,栈基地址指向的是栈底,栈指针指向的是栈顶。
大概调用一个函数就是这么干的,怎么返回的就不用我说了吧。
调用参数也就是通过栈来的,如果超过了五个参数我也不知道会发生啥,反正这上面没说。
练习 11 也就是让你写个输出栈里面的内容。
练习 12 让你实现一个调试信息的指令。
改三个地方。
前两个都在kern/monitor.c
。
第一个
static struct Command commands[] = {
{ "help", "Display this list of commands", mon_help },
{ "kerninfo", "Display information about the kernel", mon_kerninfo },
{"backtrace","Display stack backtrace", mon_backtrace},
};
后面那个是多加的,前面那两个很眼熟吧。
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
// return 0;
uint32_t ebp,eip,*p;
struct Eipdebuginfo info;
ebp=read_ebp();
while(ebp!=0){
p=(uint32_t*)ebp;
eip=p[1];
cprintf("ebp %x eip %x args %08x %08x %08x %08x %08x\n",ebp,p[1],p[2],p[3],p[4],p[5],p[6]);
if(debuginfo_eip(eip,&info)==0){
int fn_offset=eip-info.eip_fn_addr;
cprintf("%s:%d:%.*s+%d\n",info.eip_file,info.eip_line,info.eip_fn_namelen,info.eip_fn_name,fn_offset);
}
ebp=p[0];
}
return 0;
}
这个也很眼熟吧,前面改过。
到这个地方实际上已经能够运行了,只是没有行号。
最后一个在kern/kdebug.c
stab_binsearch(stabs, &lfun, &rfun, N_SLINE, addr - info->eip_fn_add r);
if (lfun <= rfun)
{
info->eip_line = stabs[lfun].n_desc;
}
这个具体原理我也不知道,抄别人的。百度能搜到。
到此,实验一就全部完成了,目前我能了解的就这么多。