练习3:分析bootloader进入保护模式的过程。
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
lab1/boot/bootasm.S源码如下:
#include <asm.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
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.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:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
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
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to 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
.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. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
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:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
分析代码如下:
.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
.set相当于宏定义。
.globl start
start:
.code16 # 启动CPU为16位模式
cli # 关中断
cld # 清方向标志
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # 寄存器置零
movw %ax, %ds # -> 数据段寄存器
movw %ax, %es # -> 附加段寄存器
movw %ax, %ss # -> 堆栈段寄存器
CLI 全称 Clear Interupt,CLD 全称 Clear Director。
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 # 将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写入al中
outb %al, $0x60 # 将al写入到0x60端口中,即将A20置1
首先等待8042 input buffer为空,向其发送写数据的指令,再次等待8042 input buffer为空,将0xdf发送至0x60,打开A20。
通过修改A20地址线可以完成从实模式到保护模式的转换。只有在保护模式下,80386的全部32根地址线有效,可寻址高达4G字节的线性地址空间和物理地址空间,可访问64TB(有2^14个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。
接下来加载GDT表,并且将cr0置为1开启保护模式。
lgdt gdtdesc
movl %cr0, %eax # 加载cro到eax
orl $CR0_PE_ON, %eax # 将eax的第0位置为1
movl %eax, %cr0 # 将cr0的第0位置为1
cr0的第0位为1表示处于保护模式
cr0的第0位为0表示处于实模式
长跳转指令更新cs的基地址
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # 使用32位模式编译
protcseg:
设置寄存器并建立堆栈
movw $PROT_MODE_DSEG, %ax # ax赋0x8
movw %ax, %ds # ds赋0x8
movw %ax, %es # es赋0x8
movw %ax, %fs # fs赋0x8
movw %ax, %gs # gs赋0x8
movw %ax, %ss # ss赋0x8
movl $0x0, %ebp # 设置帧指针
movl $start, %esp # 设置栈指针
转到保护模式完成,进入boot主方法
call bootmain
练习4:分析bootloader加载ELF格式的OS的过程。
1.bootloader如何读取硬盘扇区的?
答:①等待磁盘准备好;②发出读取扇区的命令;③等待磁盘准备好;④把磁盘扇区数据读到指定内存。这些是readsect()函数所做的事。但读取多个扇区用readseg() 函数,其中循环调用了readsect()。
2.bootloader是如何加载ELF格式的OS?
答:这是bootmain函数的任务。首先从硬盘读取elf的文件头,它包含整个执行文件的控制结构,然后利用它得到 program header 表,再根据表将所有段读入内存,最后启动程序。
查看bootmani.c文件内容:
#include <defs.h>
#include <x86.h>
#include <elf.h>
#define SECTSIZE 512
#define ELFHDR ((struct elfhdr *)0x10000) // scratch space
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
}
/* readsect - 读一个扇区@secno到@dst */
static void
readsect(void *dst, uint32_t secno) {
waitdisk(); // 等待磁盘准备好
outb(0x1F2, 1); // 要读写扇区的个数为1
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 - 读取扇区
// 等待磁盘就绪
waitdisk();
// 把磁盘扇区数据读到指定内存
insl(0x1F0, dst, SECTSIZE / 4);
}
/* *
* readseg - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
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
// 从字节转换到扇区;kernel从扇区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);
}
}
/* bootmain - the entry of bootloader */
// 根据elfhdr和proghdr的结构描述,bootloader就可以完成对ELF格式的ucore操作系统的加载过程
void
bootmain(void) {
// 先从磁盘读出第一个page
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 有效的ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// phoff 是 program header 表的位置偏移
// phnum 是 program header表中的入口数目
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,这里读取出来将之存入ph
// 按照程序头表的描述,将ELF文件中的数据载入内存
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
// 段的第一个字节的虚拟地址,段在内存映像中占用的字节数,段相对文件头的偏移值
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// 根据ELF程序入口的虚拟地址,找到入口开始运行
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
磁盘IO地址和对应功能
IO地址 | 功能 |
---|---|
0x1f0 | 读数据,当0x1f7不为忙状态时,可以读。 |
0x1f2 | 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区 |
0x1f3 | 如果是LBA模式,就是LBA参数的0-7位 |
0x1f4 | 如果是LBA模式,就是LBA参数的8-15位 |
0x1f5 | 如果是LBA模式,就是LBA参数的16-23位 |
0x1f6 | 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘 |
0x1f7 | 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据 |