本文主要是介绍学堂在线-清华大学-操作系统实验Lab1【练习3-4】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
练习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 modecli # Disable interruptscld # String operations increment# Set up the important data segment registers (DS, ES, SS).xorw %ax, %ax # Segment number zeromovw %ax, %ds # -> Data Segmentmovw %ax, %es # -> Extra Segmentmovw %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, %aljnz seta20.1movb $0xd1, %al # 0xd1 -> port 0x64outb %al, $0x64 # 0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.2movb $0xdf, %al # 0xdf -> port 0x60outb %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 gdtdescmovl %cr0, %eaxorl $CR0_PE_ON, %eaxmovl %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 registersmovw $PROT_MODE_DSEG, %ax # Our data segment selectormovw %ax, %ds # -> DS: Data Segmentmovw %ax, %es # -> ES: Extra Segmentmovw %ax, %fs # -> FSmovw %ax, %gs # -> GSmovw %ax, %ss # -> SS: Stack Segment# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)movl $0x0, %ebpmovl $start, %espcall bootmain# If bootmain returns (it shouldn't), loop.
spin:jmp spin# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:SEG_NULLASM # null segSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernelSEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernelgdtdesc:.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, %aljnz seta20.2movb $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 gdtdescmovl %cr0, %eax # 加载cro到eaxorl $CR0_PE_ON, %eax # 将eax的第0位置为1movl %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赋0x8movw %ax, %ds # ds赋0x8movw %ax, %es # es赋0x8movw %ax, %fs # fs赋0x8movw %ax, %gs # gs赋0x8movw %ax, %ss # ss赋0x8movl $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 spacestatic void
waitdisk(void) {while ((inb(0x1F7) & 0xC0) != 0x40)
}/* readsect - 读一个扇区@secno到@dst */
static void
readsect(void *dst, uint32_t secno) {waitdisk(); // 等待磁盘准备好outb(0x1F2, 1); // 要读写扇区的个数为1outb(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) {// 先从磁盘读出第一个pagereadseg((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端口读数据 |
这篇关于学堂在线-清华大学-操作系统实验Lab1【练习3-4】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!