Mit6.828 Day2~3 Lab1 Par1+部分Part2

Mit6.828 Day2~3 Lab1 Par1+部分Part2

摘要: 第一个Lab的部分

0x01 写在前面

这个Lab不需要写代码,分成三个部分,第一个部分用于熟悉x86的汇编、QEMU x86 模拟器和PC的启动进程;第二个部分用于测试6.828kernel的boot lorder;第三个部分用以探究6.828的初始化模板也就是JOS。

之前没有拉取实验源码,今天拉取以下

1
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab

0x02 Part 1: PC Bootstrap

Getting Started with x86 assembly

这个部分用以熟悉AT&T的汇编,因为这个Lab使用的是GNU assembler。可以看这本书的这部分

个人笔记部分

;在AT&T中左边是源地址右边是目标地址

%eax ;%前缀用于表示寄存器,在汇编代码中用于寄存器寻址

$_booga ;$前缀表示常数或者立即数

movl ;在AT&T语法中,指令需要有类似l、b、w这样的后缀来指定longword、byte以及word

in ;从端口输入一个字节或字到寄存器中,源操作数是端口地址。关于对对应端口地址对应的设备,这里可以参考这个网址。其中r代表读取的内容,也就是寄存器的各位的值;w代表写入,可以写入对应的十六进制代码实现相应的功能。

out ;将寄存器上的一个字节或者字输出到端口。

Simulating the x86

1
2
make qemu
make qemu-nox

用以启动qemu,前者会打开一个虚拟终端,后者直接于现有终端运行。

The PC’s Physical Address Space

这部分主要熟悉下PC的物理地址空间,具体如下:

低640KB的“Low Memory”是RAM,早期的RAM的地址空间都很小。

从0x000A0000到0x000FFFFF的是BIOS ROM,显然是为BIOS预留的。BIOS是用于在计算机电源接通时初始化PCI总线和设备并且引导OS加载的程序。

Intel“打破MB墙”后,16MB和4GB地址空间的处理器出现了。为了向后兼容,计算机设计者保留了上面这1MB的低位地址空间,因此现代计算机从0x000A0000到0x00100000有一个空洞。在顶端有一个32位的物理地址空间,如今为BIOS所保留用于32位的PCI设备。

The ROM BIOS

1
2
make qemu-gdb
make gdb

用于调试。si命令用于单步调试。

这边有一个坑,端口号不一样,记得修改lab文件夹下GNUmakefile中的端口号,将25000改为26000。

0x03 Part 2: The Boot Loader

磁盘的第一个扇区称为boot sector,用于引导操作系统启动。其中启动程序位于0x7c00-0xdff,为固定的计算机标准。

常用gdb命令:

首先要求读懂boot.S和bootmain.c(难顶,看了一上午才看懂

一段一段看吧

1
2
3
.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

一些初始化的值,不多讲。

1
2
3
4
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment

cli用于禁止所有中断,防止在BIOS运行过程中发生中断;cld按照查找的资料是指用于指定之后发生的串处理操作的指针移动方向

1
2
3
4
5
# 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

这段让ds、es、ss这几个段寄存器的值清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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:
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

这段根据原文的注释是用来enable A20的。A20是什么呢?根据维基百科

A20总线,是x86体系的扩充电子线路之一。A20总线是专门用来转换地址总线的第二十一位。

在引导系统时,BIOS先打开A20总线来统计和测试所有的系统内存。而当BIOS准备将计算机的控制权交给操作系统时会先将A20总线关闭。一开始,这个逻辑门连接到Intel 8042的键盘控制器。控制它是相对较慢。

激活A20总线是保护模式在引导阶段的步骤之一,通常在引导程序将控制权交给内核之前完成(例如在Linux下)。

所以这段代码是必须的。还是一段段来吧。首先这一段

1
2
3
4
5
6
7
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1

movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64

inb指令将0x64端口的值放入al寄存器中,而后testb将立即数0x2与al寄存器中的值进行AND运算。如果运算结果不是零那么就进行跳转,现在要明白端口0x64对应于什么。参看这个网页:

当第二位为1时,也就是说当有输入需要被读取时,程序等待,等到读取完成,程序继续执行。可以看到d1被传入al寄存器也就是端口而后al寄存器中的值有传入到0x64端口中。查看dl指令为何:

意思是下一条写入0x60端口的数据将被传送到804x输出端口。那么也就来到我们下一段代码了

1
2
3
4
5
6
7
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2

movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60

同样,等待输入被读取,读取完成后,指令df写入到0x60。按照上一部分代码,执行df指令。同样查看df指令为何:

至此,A20被enable了。

继续下一段代码

1
2
3
4
5
6
7
8
9
10
11
12
# 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
movl %cr0, %eax
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位运行模式

这段代码将现有的实模式转换为保护模式。什么是实模式什么是保护模式?参看CPU的实模式和保护模式(一) 。难顶,后面工作原理再深究吧。总之根据文章所言

实模式和保护模式都是CPU的工作模式,而CPU的工作模式指的是CPU的寻址方式、寄存器大小等用来反应CPU在该环境下如何工作的概念。

总之,进入实模式GDT表必不可少,而GDT表需要一个专门的寄存器GDTR来管理,故我们的第一条指令就是将gdtdesc这个标识符的值送入全局映射描述表寄存器GDTR中,也就是将GDT表的一些重要信息存放到CPU的GDTR寄存器中,其中包括GDT表的内存起始地址,以及GDT表的长度。gdt、gdtdesc在文件末尾出现了。

1
2
3
4
5
6
7
8
9
10
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
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
1
2
3
movl    %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

这三条指令修改的是cr0寄存器中的值。cr0~cr3都是80x86的控制寄存器,其中$CRO_PE_ON的值定义于0x00000001,也就是说上面的这个orl指令将cr0的第一位置1了。而cr0寄存器的第一位表示的是protected mode启动位,到此保护模式启动完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Switches processor into 32-bit mode.
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
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.
movl $start, %esp
call bootmain

这里现将当前运行模式切换为32位运行模式,而后重新加载段寄存器的值。这个是硬性规定,只有这样才能使GDTR中的值生效。可以见X86 Assembly/Global Descriptor Table

最后,修改esp寄存器中的值,然后跳转到bootmain函数中。

现在来看看main.c中的bootmain过程。

首先根据注释

This a dirt simple boot loader, whose sole job is to boot an ELF kernel image from the first IDE hard disk.

这个bootmain用于引导硬盘中的ELF内核映像。一步步看吧,先补充下ELF的知识点

elf文件:elf是一种文件格式,主要被用来把程序存放到磁盘上。是在程序被编译和链接后被创建出来的。一个elf文件包括多个段。对于一个可执行程序,通常包含存放代码的文本段(text section),存放全局变量的data段,存放字符串常量的rodata段。elf文件的头部就是用来描述这个elf文件如何在存储器中存储。需要注意的是,你的文件是可链接文件还是可执行文件,会有不同的elf头部格式。

1
2
#define SECTSIZE	512
#define ELFHDR ((struct Elf *) 0x10000) // scratch space

一个分区大小的宏定义加上一个ELF起始地址的宏定义。进入到bootmain中

1
2
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

看注释为读取磁盘的第一页,进入到这个readseg()函数里面看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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++;
}
}

pa是传入的ELF的起始地址,count传入了512×8=4096kb也就是一个页面的大小,偏移量offset为0。这里相当于是将硬盘中操作系统的ELF头给读入到了内存中。

1
2
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;

文件的magic字段标志了文件的类型,如果文件类型确实为ELF的话,那么这个e_magic==ELF_MAGIC。

ELF头中包含了Program Header Table,这个表中存放着段的信息,也就是上面ELF知识点中的data段、文本段的位置。因此要读取出来。

1
2
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);

e_phoff为这个到这个表末尾所需要的偏移量。

1
2
3
4
5
eph = ph + ELFHDR->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);

后面就是一个循环将所需要的表条目读出来了。

1
2
3
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();

e_entry相当于可执行文件的入口,至此开始执行这个内核文件,所以这个时候控制权就由boot lorder转交给了操作系统的kernel。

总结一下,在打开电源后,BIOS会被引入到内存中运行,它会找到磁盘的第一个扇区,其中包含了bootstrap,其会将CPU的工作模式由实模式转换到保护模式,这个由上面的boot.S文件做了,而后进入到BIOS移交控制权的阶段,在这个阶段中,ELF文件头会被读入到内存,在得到kernel image的所有信息后进入到kernel中,OS开始运行,这上面的bootmain()过程所做的事情。

Exercise 3

第一个部分,比对gdb上的汇编与boot.S和反编译的boot.asm中的代码有何不同。看下面三幅对应的截图

我们可以注意到其实并无多少不同,基本上是一样的,如要说具体的差异那么可以体现在以下几点:

1、GDB中没有出现类似seta20.1这样的标签;

2、GDB和boot.S中没有汇编的机器码;

3、boot.asm中的寄存器名称稍稍有些不同。

那就开始第二个部分吧:

一些汇编知识:

%eip寄存器用于存储CPU要执行的指令的地址的值。在call指令执行完毕后,会将返回地址压入栈中并且将调用函数的地址写入%eip中。

%esp为堆栈振寄存器,用于指向堆栈顶。

这个部分提了一下几个要求:

1、辨别出与readsect()函数中每条语句相关的汇编指令;

2、找到readsect()后返回bootmain()的地方,并且找到那个读取剩下包含kernel扇区的for循环,理解在什么地方这个循环结束了。

通读整个汇编比较难受,所以这个部分就参考了这篇文章,便没有自己单步调试来看一个个寄存器里的值了。

Part2未完,见下篇博客。

0x03 总结

今天(虽然这两天都在搞,但是昨天基本没咋弄)收获还是蛮多的吧。首先,基本理解了电脑接通电源后发生在BIOS和Bootstrap之间的事,较为深入(还不够深入)地了解了其中的一些细节(比方说ELF文件,之前接触过但是不像今天这么近距离;比方说实模式与保护模式;比方说每次必开A20的总线等等);其次对汇编与寄存器这块有了更深的理解。

不过这Lab难度是真的大,看了一天汇编脑壳都给我看疼了,但说实话做起来真的跟吸毒一样爽。


评论