MIT 6.828课程引导部分的解读

引导代码位于boot文件夹下,由一个16位与32位汇编混合的汇编文件(boot.S)和一个C语言文件(main.c)组成。

程序的入口在boot.S中,采用的是AT&T语法,下面先对这个文件进行分析:

#include <inc/mmu.h>

在inc文件夹下有一个mmu.h头文件,这里存放了一些经常会用到的宏定义

.set PROT_MODE_CSEG, 0x8         
.set PROT_MODE_DSEG, 0x10        
.set CR0_PE_ON,      0x1         

上面声明了一些常量,用来设置一些段寄存器的值

.globl start
start:         

这里就要进入正题了,“.globl start”用来告诉编译器start是程序入口,事实上“.globl”的主要作用是说明“start”这个函数可以被其他文件的调用,编译器默认的入口标号为“_start”因此,在linux中编译时,会指定编译入口,用这个参数“-e start”。

.code16                    
  cli                        
  cld      

“.code16”的意思是从这里往后的代码是16位程序,要用16位的编译模式(16位与32位生成的机器码是不一样的,因此不指明的话,会造成不可预计的错误。)

“cli”指令用来关掉可屏蔽中断,为在之后的运行中不受打扰。以后还会打开的。

“cld”重新确定串操作方向,主要是为了main.c文件中复制内核程序时使用。

 xorw    %ax,%ax           #将ax清0 ,分别将ds、es、ss的值变成0,因为在之前的BIOS运行中,不能确定这些寄存器的值。
  movw    %ax,%ds          
  movw    %ax,%es     
  movw    %ax,%ss            
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
 lgdt    gdtdesc

jnz seta20.1 movb $0xd1,%al # 0xd1 -> port 0x64 outb %al,$0x64seta20.2: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.2 movb $0xdf,%al # 0xdf -> port 0x60 outb %al,$0x60

简单来讲,这段代码主要就是一个目的,使能A20地址线,使计算机能够访问更大的内存空间。具体关于A20地址线网上有很多介绍,这里不再废话。要注意的是打开A20地址线并不不能使计算机直接进入保护模式(32位模式),只是使计算机可以访问更大的内存。

 lgdt    gdtdesc

这条指令尤为重要,加载全局描述符(GDT),标号gdtsec处存放了6字节有关GDT位置的信息,稍后我们会看到。

 movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

这里将cr0寄存器的0-bit(最低位)置1,告诉cpu进入保护模式,在这里才算是真正进入了保护模式。

 
 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

“ljmp”这条指令可能会让人感到奇怪,因为这个长跳转指令后的就是他要跳转的位置。事实上,必须这么做,在加载GDT以后,由于内部硬件设计的原因,必须要重设所有段寄存器的值,但我们没有直接改变指令寄存器cs的指令,因此使用一个长跳转指令改变cs的值(跳转指令实际上就是在对cs进行操作,使cs指向预定的位置,cup是根据cs指向的位置来执行机器码的)。

之后的指令就好理解了,因为进入了保护模式,所以“.code32”告诉编译器使用32位方式编译代码。然后是将其他5个段寄存器的值重新设为0x10。

 movl    $start, %esp
  call bootmain

这里很简单,先将栈顶指针指向我们的引导程序首地址,即0x7c00处,由于堆栈是向下生长的,因此实际的堆栈区域变设置为了0x7c00~0x0010这一段。(注:ss被称为栈底指针,指向堆栈的最底部,sp是栈顶指针,时刻指向当前堆栈第一个元素的位置,esp是32位的,64位是rsp)

最后调用bootmain函数,这个函数在main.c文件中。用来将内核复制到内存的指定位置。

spin:            #这里是一个死循环,如果bootmain函数发生错误返回,计算机就会卡在这儿,就操作系统而言,最好在这里设置一些提示信息
  jmp spin

# 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

标号gdt处设计了有3个表项的GDT表,每个表项长度为8字节,第一个表项必须设置为空的

标号gdtdesc是要往GDTR寄存器里加载的6字节信息,GDTR寄存器的低2字节储存GDT表的长度,高4字节储存GDT表在内存中的首地址.由于GDT讲解比较繁杂,这里暂时略过。

下面对main.c文件分析

首先要说明一下bootmain函数的工作方式。

首先它会判断要加载的内核文件是否属于elf格式,这是一种专为unix系统设计的二进制文件,unix/linux下的应用程序均为elf结构,mit 6.828课程也使用了这种结构,结构定义在inc文件夹的elf.h文件中,如下:

struct Elf {
	uint32_t e_magic;	// 魔数,elf文件的前四字节分别为(0x7f,E,L,F)
	uint8_t e_elf[12];        //这12字节没有定义
	uint16_t e_type;        //目标文件属性
	uint16_t e_machine;    //硬件平台类型
	uint32_t e_version;    //elf的版本
	uint32_t e_entry;        //程序入口
	uint32_t e_phoff;        //程序头表偏移量,bootmain函数中的第一行的那个结构便是程序表头的结构
	uint32_t e_shoff;        //节表头偏移量,节表多用来储存程序会用到的数据
	uint32_t e_flags;        //处理器特定标志 
	uint16_t e_ehsize;       //elf头部长度 
	uint16_t e_phentsize;    //程序头表中一个条目的长度 
	uint16_t e_phnum;        //程序头表条目数目 
	uint16_t e_shentsize;    //节头表中一个条目的长度 
	uint16_t e_shnum;        //节头表条目个数 
	uint16_t e_shstrndx;    //节头表字符索引 
};

来看bootmain函数的第一行:

struct Proghdr *ph, *eph;

声明了两个指向程序表头的指针,还没有定义。

//这里是将一页(4kb)内核程序读入内存,放在首地址0x10000处
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
if (ELFHDR->e_magic != ELF_MAGIC)//判断是否为elf文件,elf结构中的前4字节为(0x7f,E,L,F)
	goto bad;//bad处是一个错误处理程序,编写者只是简单地做了个循环,毕竟是用来学习的
//加载每个程序段
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
	eph = ph + ELFHDR->e_phnum;//e_phnum储存了程序段的数量,在这里将eph指向了最后一个程序段

在这里,ph指向了内核的程序表头,eph则指向了最后一个程序段。

for (; ph < eph; ph++)
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

反复循环,通过readseg函数将内核全部加载到内存中。

最后的bad,基本没有意义。


main.c中还有两个磁盘操作函数,用来帮助bootmain函数加载内核。

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;//确保偏移量小于512,大于0

	// 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的值是0x10000,即内核的地址,
		pa += SECTSIZE;
		offset++;
	}
}

void
waitdisk(void)
{
	// wait for disk reaady
	//从端口0x1f7获取一字节信息,并判断最高两位是否为01(第7位和第6位),否则不断循环
	//0x1f7 用来存放硬盘操作后的状态,其中第7位为1代表控制器忙碌,第六位为1代表磁盘驱动准备好了
	//这段代码用来判断硬盘是否可以读取,不能读的话计算机就会卡在这里
	while ((inb(0x1F7) & 0xC0) != 0x40)
		/* do nothing */;
}

void
readsect(void *dst, uint32_t offset)
{
	// wait for disk to be ready
	waitdisk();

	//0x1f2存放要读写的扇区数量
	outb(0x1F2, 1);		// count = 1
	//0x1f3用来存放要读写的扇区号码,就是偏移量
	outb(0x1F3, offset);
	//0x1f4 用来存放读写柱面的低8位字节
	outb(0x1F4, offset >> 8);
	//0x1f5 用来存放读写柱面的高2位字节
	outb(0x1F5, offset >> 16);
	//0x1f6 用来存放要读写的磁盘号,磁头号。7-bit:恒为1;6-bit:恒为0;5-bit:恒为1;
	//4-bit:0代表第一块硬盘,1代表第二块硬盘
	//3~0-bit:用来存放要读写的磁头号
	outb(0x1F6, (offset >> 24) | 0xE0);

	//0x1f7 用来存放硬盘操作后的状态,以下为设置为1的情况
	//7-bit 控制器忙碌
	//6-bit 磁盘驱动器准备好了
	//5-bit 写入错误
	//4-bit 搜索完成
	//3-bit 扇区缓冲区没有准备好
	//2-bit 是否正确读取磁盘数据
	//1-bit 磁盘每转一周将此位设置为1
	//0-bit 之前的命令因发生错误而结束
	outb(0x1F7, 0x20);	// cmd 0x20 - read sectors

	// wait for disk to be ready
	waitdisk();//检查磁盘状态

	// read a sector
	insl(0x1F0, dst, SECTSIZE/4);//0x1f0读写功能,其内容为正在传输的一字节数据
}

上面已经说得很细了,在inc文件夹的x86.h文件中可以找的上面用到的一些端口操作函数,使用gcc内联汇编的方式操作端口,不得不说,内联汇编确实不简单。

猜你喜欢

转载自blog.csdn.net/lindorx/article/details/80579861