MIT6.828 Fall2018 笔记 - Lab1: Booting a PC

Part 1: PC Bootstrap

Simulating the x86

下载 JOS 源码,然后编译

# 让 git 忽略 ssl 认证,否则 git clone 可能会失败
export GIT_SSL_NO_VERIFY=1
proxychains git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
cd lab
make

产生的obj/kern/kernel.img为虚拟硬盘,这个硬盘镜像我们的包含boot(obj/boot/boot)和kernel(obj/kernel)

make qemu

输出:

***
*** Use Ctrl-a x to exit qemu
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::25000 -D qemu.log
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

使用Ctrl-a x可以退出qemu

The PC's Physical Address Space

PC的物理地址空间布局:

+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

从 0x00000000 到 0x000FFFFF 的 640KB 区域为 Low memory,是早期PC可以使用的 RAM。硬件保留的从 0x000A0000 到 0x000FFFFF 的 384KB 区域用于特殊用途,例如视频显示缓冲区和非易失性存储器中保存的固件。从 0x000F0000 到 0x000FFFFF 的 64KB 区域的部分是最重要的 BIOS。

The ROM BIOS

在一个终端输入make qemu-gdb,另一个终端输入make gdb,开始调试。
出现:

The target architecture is assumed to be i8086
[f000:fff0]    0xffff0: ljmp   $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb)

这表明 PC 从物理地址 0x000ffff0 开始执行(从物理地址空间布局可知,这是为 ROM BIOS 预留的 64KB 的顶部),然后跳转至 f000:e05b。

QEMU 模拟了 8088 处理器的启动,启动电源时,处理器进入实模式并且将 CS 设置为 0xf000,将 IP 设置为 0xfff0。这样一开机 BIOS 就取得了机器的控制权。BIOS 运行时,它将建立一个中断描述符表并初始化各种设备,例如 VGA 显示。在初始化 PCI 总线和 BIOS 知道的所有重要设备后,它将搜索可引导设备,例如软盘,硬盘驱动器或 CD-ROM。 最终,BIOS 在找到可引导磁盘时,会从磁盘读取 boot loader 并将控制权转移给 boot loader。

我们可以用 GDB 的 si 命令进行跟踪。GDB manual

Part 2: The Boot Loader

PC 的软盘和硬盘分为 512 个字节的区域,称为扇区。扇区是磁盘读写的基本单位:每个读或写操作必须是一个或多个扇区,并且必须在扇区边界上对齐。如果磁盘是可引导的,则第一个扇区称为引导扇区,因为这是引导加载程序代码所在的位置。当 BIOS 找到可引导的软盘或硬盘时,它将 512 字节的引导扇区加载到物理地址 0x7c00 至 0x7dff 的内存中,然后使用 jmp 指令将 CS:IP 设置为 0000:7c00,将控制权传递给 boot loader。

在 6.828 中,使用传统的硬盘启动机制,所以我们的 boot loader 必须是 512 bytes。见 boot/boot.Sboot/main.c。boot loader 执行两个功能:

  1. 将处理器从实模式切换到 32 位保护模式,这样能访问大于 1MB 的物理地址空间。
  2. 从硬盘中读取内核。

obj/boot/boot.asm 是我们编译 boot loader 后的反汇编。同样,obj/kern/kernel.asm 是对 JOS kernel 的反汇编。

b *0x7c00 设置断点,用 c 运行到断点处,用 si N 执行 N 个指令 用 x/i 查看下一条的指令,用 x/Ni ADDR 获取任意一个机器指令的反汇编指令。

Exercise 3

  1. CLI:禁用中断
  2. CLD:清除方向标志位(DF)。在串处理指令中,控制每次操作后si,di的增减。(df=0,每次操作后si、di递增)。

关于 Real Mode 和 Protected Mode

实模式下的地址始终对应于内存中的实际地址。实模式 20 位地址,1MB 的寻址空间。

为了向后兼容,所有x86 CPU在复位时都以实模式启动,尽管在其他模式下启动时也可以在其他系统上仿真实模式。

关于 A20 line 和 PS/2 Controller

在实模式中,最大访问地址为 1MB,然而,FFFFH:FFFFH = 10FFEFH,也就是说从 100000H 到 10FFEFH 无法访问,当访问这段地址时,会产生 wrap-around,也就是实际访问地址会对 1MB 求模。
到了 80286 中有 24 根地址总线,最大访问地址为 16MB。这个时候,不会产生 wrap-around,为了向下兼容 8086,需要使用第 21 根地址总线。所以 IBM 的工程师使用 PS/2 Controller 输出端口中多余的端口来管理 A20 gate,也就是第 21 根地址总线(从 0 开始)。

加上我的注释的 boot.S

#include <inc/mmu.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 相当于 #define,用于设置常量
.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

.globl start
start:
  .code16                     # Assemble for 16-bit mode
  # 禁用中断
  cli                         # Disable interrupts
  # 清除方向标志。df=0时,串处理指令中每次操作后si、di递增
  cld                         # String operations increment

  # ax,ds,es,ss 全部置零
  xorw    %ax,%ax             # Segment number zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # 8086中有20根地址线,最大访问地址为1MB
  # 因此高于1MB的地址在默认情况下会自动变为0。此代码将取消此操作。
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  # 如果input buffer满了,即busy,则跳转回去,继续检测,直到不busy为止
  jnz     seta20.1

  # 将端口0x64的值设置为0xd1
  # 告诉PS/2 Controller将下一个0x60的字节写出它的Output Port
  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  # 将0xdf写出到Output Port,这样就打开了A20 Gate
  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

  # 从实模式切换到保护模式
  # 加载全局描述符表
  lgdt    gdtdesc
  # 将 cr0 最后一位(PE位)置1,以让cpu运行在保护模式下
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

  # 跳转,进入32位保护模式
  ljmp    $PROT_MODE_CSEG, $protcseg

  .code32                     # Assemble for 32-bit mode
protcseg:
  # 设置保护模式的段寄存器
  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

  # 设置esp,并且调用main.c中的bootmain函数
  movl    $start, %esp
  call bootmain

  # 如果bootmain返回了(本不应该返回),就死循环
spin:
  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

总结一下boot.S干了什么:

  1. 启用A20 line
  2. 加载GDT,从实模式切换到保护模式
  3. 进入mian.c的bootmain函数

别人对 Lab 1 Exercise 3 的分析

TODO: GDT的部分暂时先不管

回答问题:

  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
(gdb) si
[   0:7c2d] => 0x7c2d:  ljmp   $0xb866,$0x87c32
0x00007c2d in ?? ()

不知为何这里是$0xb866,$0x87c32,但实际上还是跳转到了0x7c32。实际上导致切换到保护模式的是

(gdb) si
[   0:7c23] => 0x7c23:  mov    %cr0,%eax
0x00007c23 in ?? ()
(gdb) si
[   0:7c26] => 0x7c26:  or     $0x1,%ax
0x00007c26 in ?? ()
(gdb) si
[   0:7c2a] => 0x7c2a:  mov    %eax,%cr0
0x00007c2a in ?? ()

cr0 寄存器置1

  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

在mian.c中得知,boot loader的最后一行代码为

((void (*)(void)) (ELFHDR->e_entry))();

通过查看obj/boot/boot.asm的内容得知,boot loader的最后一条指令为:

7d71:       ff 15 18 00 01 00       call   *0x10018

意思是跳转到0x10018指针所指向的地址,根据inc/elf.h中Elf结构体的定义,e_entry的地址为0x10000 + 4 + 12*1 + 2 + 2 + 4 = 0x10018。确实。查看一下0x10018指针指向的地址:

(gdb) x/1xw 0x10018
0x10018:    0x0010000c

然后在0x7d71打断点,运行,然后跳转,可知kernel的第一条指令为:

=> 0x10000c:    movw   $0x1234,0x472
  • Where is the first instruction of the kernel?

objdump -f obj/kern/kernel可以查看到其起始地址

[hyuuko@hyuuko-manjaro lab]$ objdump -f obj/kern/kernel

obj/kern/kernel:     文件格式 elf32-i386
体系结构:i386,标志 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
起始地址 0x0010000c
  • How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

Loading the Kernel

ELF headers 的定义在 inc/elf.h

我们所要关心的program sections是:

  1. .text:可执行指令
  2. .rodata:只读数据段。比如字符串常量
  3. .data:存放已初始化静态数据(具有静态存储期)的数据段。
  4. .bss:存放的是未初始化(全0)的静态数据,只需记录.bss段的地址和长度

使用如下命令查看ELF format二进制文件的段信息:

objdump -h obj/kern/kernel
objdump -h obj/boot/boot.out

有些段存放debugging information

VMA 指 link address,指该段应该在哪个内存地址执行,LMA 指 load address,指该段应该被加载到哪个内存地址。一般而言两者相同。

使用如下命令查看program headers和符号表等:

objdump -x obj/kern/kernel

vaddr 指 virtual address,padder 指 physical address,filesz即file size,memsz即memory size

boot/main.c 的作用就是从硬盘中先读kernel的elf头,再根据elf头将kernel的每个section读入内存,然后进入kernel。

boot/main.c部分注释:


#define SECTSIZE    512 // 扇区大小
// elf头起始位置
#define ELFHDR      ((struct Elf *) 0x10000) // scratch space

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
    struct Proghdr *ph, *eph;

    // 从磁盘中读取第一页
    // 将0地址开始的 512*8 个byte,即 4k 的数据读入 0x10000地址
    readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC)
        goto bad;

    // ELF头部有描述kernel应加载到内存什么位置的描述表,
    // 先将描述表的头地址存在ph
    ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    // 按照描述表将kernel各段中数据载入内存
    for (; ph < eph; ph++)
        // p_pa是这个段的物理地址
        // 注意,ph++,由于ph是指针,所以实际上加的值不是1,而是4
        readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

    // 将ELFHDR->e_entry转为函数指针,然后调用
    // 注意:该函数不会返回
    ((void (*)(void)) (ELFHDR->e_entry))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);
    while (1)
        /* do nothing */;
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
    uint32_t end_pa; // 将offset 的count个byte,读到 pa~end_pa 中

    end_pa = pa + count;

    // 即 pa=(pa/SECTSIZE)*SECTSIZE,比如pa是513,会变为512
    // 将 pa 按扇区对齐
    pa &= ~(SECTSIZE - 1);

    // 将以byte为单位的offset转为以sector为单位
    offset = (offset / SECTSIZE) + 1;

    while (pa < end_pa) {
        readsect((uint8_t*) pa, offset);
        pa += SECTSIZE;
        offset++;
    }
}

Exercise 5

BIOS把引导扇区加载到内存地址0x7c00,这也就是引导扇区的加载地址和链接地址。在 boot/Makefrag 中,是通过传 -Ttext 0x7C00 这个参数给链接程序设置了链接地址,因此链接程序在生成的代码中产生了正确的内存地址。如果将这个值设置为其他值,虽然bios还是会把引导扇区加载到内存地址0x7c00,但是在执行ljmp $PROT_MODE_CSEG, $protcseg时,$protcseg不是正确的值,无法从实模式进入保护模式,gg。

boot/Makefrag里的-Ttext 0x7C00改为-Ttext 0x8C00,然后make clean,再调试。出错的指令:

(gdb) x/i 0x7c2d
   0x7c2d:  ljmp   $0xb866,$0x88c32

BIOS 会把 boot loader 固定加载在 0x7c00,但这条指令会跳转到0x8c32,然而我们想要跳转到的指令实际上在0x7c32

gdb的x/Nx ADDR命令可以打印出在ADDR处的n个word。

obj/kern/kernel的VMA与LMA并不相同,kernel告诉boot loader在1M处将kernel载入内存,但是在一个高地址执行

Exercise 6

(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) b *0x7d71
Breakpoint 2 at 0x7d71
(gdb) c
Continuing.
[   0:7c00] => 0x7c00:  cli

Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x/4x 0x10000
0x10000:    0x00000000  0x00000000  0x00000000  0x00000000
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x7d71:  call   *0x10018

Breakpoint 2, 0x00007d71 in ?? ()
(gdb) x/4x 0x10000
0x10000:    0x464c457f  0x00010101  0x00000000  0x00000000
(gdb)

Part 3: The Kernel

操作系统内核通常被链接到非常高的虚拟地址(例如0xf0100000)下运行,以便留下处理器虚拟地址空间的低地址部分供用户程序使用。 在下一个lab中,这种安排的原因将变得更加清晰。

许多机器在地址范围无法达到0xf0100000,因此我们无法指望能够在那里存储内核。相反,我们将使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码期望运行的链接地址)映射到物理地址0x00100000(引导加载程序将内核加载到物理内存中)。

现在,我们只需映射前4MB的物理内存,这足以让我们启动并运行。 我们使用kern/entrypgdir.c中手写的,静态初始化的页面目录和页表来完成此操作。 现在,你不必了解其工作原理的细节,只需注意其实现的效果。

实现虚拟地址,有一个很重要的寄存器CR0-PG;

PG:CR0的位31是分页(Paging)标志。当设置该位时即开启了分页机制;当复位时则禁止分页机制,此时所有线性地址等同于物理地址。在开启这个标志之前必须已经或者同时开启PE标志。即若要启用分页机制,那么PE和PG标志都要置位。

Exercise 7

(gdb) si
=> 0x100025:    mov    %eax,%cr0
0x00100025 in ?? ()
(gdb) x/4x 0x100000
0x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c766
(gdb) x/4x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000  0x00000000  0x00000000  0x00000000
(gdb) si
=> 0x100028:    mov    $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/4x 0x100000
0x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c766
(gdb) x/4x 0xf0100000
0xf0100000 <_start+4026531828>: 0x1badb002  0x00000000  0xe4524ffe  0x7205c766
(gdb)

执行mov %eax,%cr0后,0x100000的数据无变化,0xf0100000的数据从0变成了与0x100000的数据一致,这说明,虚拟地址0xf0100000已经被映射到了0x100000

文件kern/entry.S

# Turn on paging.
movl    %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl    %eax, %cr0

这部分指令启动了分页机制,内存引用变成了通过 virtual memory hardware 转换过的物理地址产生的虚拟地址。例如,虚拟地址 0x00000000 到 0x00400000 以及 0xf0000000 到 0xf0400000 都被转为物理地址 0x00000000 到 0x00400000。

如果注释掉 kern/entry.S中的movl %eax, %cr0,当访问高位地址时,会出现RAM or ROM 越界错误。

Formatted Printing to the Console and Exercise 8

lib/printfmt.c的第206行改为:

// (unsigned) octal
case 'o':
    num = getuint(&ap, lflag);
    base = 8;
    goto number;

回答问题:

  1. Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?

    console.c 暴露接口给 printf.c,比如函数 cputchar

  2. Explain the following from console.c:

    // 如果缓冲区满了
    if (crt_pos >= CRT_SIZE) {
        int i;
        // 将第 2~80 行往上移,这样就空出了一行
        memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
        // 将最后一行全部变成空的
        for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
            crt_buf[i] = 0x0700 | ' ';
        // 光标位置弄到行首
        crt_pos -= CRT_COLS;
    }

猜你喜欢

转载自www.cnblogs.com/zsmumu/p/12417220.html