学堂在线-清华大学-操作系统实验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 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端口读数据
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JILIN.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值