MIT 6.828 2018 Lab 1 Booting a PC Part 1-2

Lab 1 Booting a PC

Part 1: PC Bootstrap

首先,你需要熟悉x86的汇编语言

本次实验使用GNU 编译器, 生成的汇编代码是 AT&T格式的

Exercise 1. Familiarize yourself with the assembly language materials available on the 6.828 reference page. You don’t have to read them now, but you’ll almost certainly want to refer to some of this material when reading and writing x86 assembly.

We do recommend reading the section “The Syntax” in Brennan’s Guide to Inline Assembly. It gives a good (and quite brief) description of the AT&T assembly syntax we’ll be using with the GNU assembler in JOS.

我们使用QEMU来模拟x86环境.

配置好环境后, 键入make qemu:

QEMU将加载obj/kern/kernel.img.该文件相当于是模拟出来的电脑的虚拟硬盘, 它同时包含了boot loader和 kernel.

Booting from Hard Disk...
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>

从’‘6828…’’ 开始的文字,都是由我们的kernel打印的.

The PC’s Physical Address Space

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5OYD7Cj-1569761272684)(C:\Users\hyf98\AppData\Roaming\Typora\typora-user-images\1569410015552.png)]

第一代PC处理器是16位字长的Intel 8088处理器,这类处理器只能访问1MB的地址空间,即0x00000000 ~ 0x000FFFFF。但是这1MB也不是用户都能利用到的,只有低640KB(0x00000000 ~ 0x000A0000)的地址空间是用户程序可以使用的。如图所示。

而剩下的384KB的高地址空间则被保留用作其他的目的,比如(0x000A0000 ~ 0x000C0000)被用作屏幕显示内容缓冲区,其他的则被非易失性存储器(ROM)所使用,里面会存放一些固件,其中最重要的一部分就是BIOS,占据了0x000F0000~0x00100000的地址空间。BIOS负责进行一些基本的系统初始化任务,比如开启显卡,检测该系统的内存大小等等工作。在初始化完成后,BIOS就会从某个合适的地方加载操作系统。

虽然Intel处理器突破了1MB内存空间,在80286和80386上已经实现了16MB,4GB的地址空间,但是PC的架构必须仍旧把原来的1MB的地址空间的结构保留下来,这样才能实现向后兼容性。所以现代计算机的地址 0x000A0000~0x00100000区间是一个空洞,不会被使用。因此这个空洞就把地址空间划分成了两个部分,第一部分就是从0x00000000 ~ 0x000A0000,叫做传统内存(base mem)。剩下的不包括空洞的其他部分叫做扩展内存(extend mem)。而对于这种32位字长处理器通常把BIOS存放到整个存储空间的顶端处。

由于xv6操作系统设计的一些限制,它只利用256MB的物理地址空间,即它假设用户的主机只有256MB的内存。

The ROM BIOS

这一节中,我们将研究 一个IA-32计算机如何boot.(使用gdb)

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

上述指令是GDB反汇编出来第一条需要执行的指令,我们可以得知:

  • The IBM PC starts executing at physical address 0x000ffff0, which is at the very top of the 64KB area reserved for the ROM BIOS.
  • The PC starts executing with CS = 0xf000 and IP = 0xfff0.
  • The first instruction to be executed is a jmp instruction, which jumps to the segmented address CS = 0xf000 and IP = 0xe05b.

一开始跳转到此处,是8088的规定,以使得BIOS能够接管

BIOS运行过程中,它设定了中断描述符表,对VGA显示器等设备进行了初始化。在初始化完PCI总线和所有BIOS负责的重要设备后,它就开始搜索软盘、硬盘、或是CD-ROM等可启动的设备。最终,当它找到可引导磁盘时,BIOS从磁盘读取引导加载程序并将控制权转移给它。

Part 2 boot loader

BIOS将引导磁盘的第一个扇区,即前512字节load到内存0x7c00-0x7dff处, 然后使用jmp指令跳转到此处,转交控制权给boot loader.

boot loader由两个文件组成: boot/boot.Sboot/main.c, 请仔细阅读研究这两个文件,确定你知道这些代码的意义

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 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
    cld                         # String operations increment
  
    # Set up the important data segment registers (DS, ES, SS).
	  #将各个段的基址设置为0
    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
    testb   $0x2,%al
    jnz     seta20.1
  
    movb    $0xd1,%al               # 0xd1 -> port 0x64
    outb    %al,$0x64
  
  seta20.2:
    inb     $0x64,%al               # Wait for not busy
    testb   $0x2,%al
    jnz     seta20.2
  
    movb    $0xdf,%al               # 0xdf -> port 0x60
    outb    %al,$0x60
  
    # Switch from real to protected mode, using a bootstrap GDT
    # and segment translation that makes virtual addresses 
    # identical to their physical addresses, so that the 
    # effective memory map does not change during the switch.
    # 切换至保护模式, 但注意,这些代码执行完后仍然还是执行16位代码
    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.
   # 直到此时,才转到32-bit(执行后)
    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.
    movl    $start, %esp
    call bootmain  #转到bootmain.c
  
    # If bootmain returns (it shouldn't), loop.
  spin:
    jmp spin
  
  # Bootstrap GDT
  .p2align 2                                # force 4 byte alignment
  gdt:
    SEG_NULL              # null seg   #SEG_NULL和SEG都定义在mmu.h文件中
    SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg   #SEG可以让我们方便的构造段描述符
    SEG(STA_W, 0x0, 0xffffffff)           # data seg
  
  gdtdesc:
    .word   0x17                            # sizeof(gdt) - 1
    .long   gdt                             # address gdt
  

main.c 文件节选:

  bootmain(void)
  {   
      struct Proghdr *ph, *eph;
      
      // 读取disk上的第一页,一个扇区512byte, 8个正好4k,也就是一页
      readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
      
      // 判断ELF文件是否有效 
      if (ELFHDR->e_magic != ELF_MAGIC)
          goto bad;
      
      // load each program segment (ignores ph flags)
      // 一个个段的加载
      ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
      eph = ph + ELFHDR->e_phnum;
      for (; ph < eph; ph++)
          // p_pa is the load address of this segment (as well
          // as the physical address)
          readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
      
      // call the entry point from the ELF header
      // note: does not return!
      ((void (*)(void)) (ELFHDR->e_entry))();
  
  bad:
      outw(0x8A00, 0x8A00);
      outw(0x8A00, 0x8E00);
      while (1)
          /* do nothing */;
 }

在了解kernel之前,回答以下问题:

  1. 在何处/何时,处理器开始指向32位代码,时什么导致了16-32位的切换.
  2. boot loader执行的最后一条指令是什么?kernel刚被load的时候执行的第一条指令是什么?
  3. kernel的第一条指令在哪?
  4. boot loader如何觉得直到读取多少个sector来获取整个kernel?它在哪获取这个信息?

解答:

  1. 见代码注释

  2. boot loader执行的最后一条代码自然是跳转向ELF header提供的entry:

    查看obj/boot/boot.asm代码,得知最后一条代码:

    305     ((void (*)(void)) (ELFHDR->e_entry))();
    306     7d6b:   ff 15 18 00 01 00       call   *0x10018
    

    当然, gdb调试也能得到相同的结果, 只是屏幕上显示的不是 call *ox10018, 而会显示 0x10018位置处的值

    kernel的第一条指令,查看obj/kern/kernel.asm得知:

      11 .globl entry
      12 entry:
      13     movw    $0x1234,0x472           # warm boot
      14 f0100000:   02 b0 ad 1b 00 00       add    0x1bad(%eax),%dh
      15 f0100006:   00 00                   add    %al,(%eax)
      16 f0100008:   fe 4f 52                decb   0x52(%edi)
      17 f010000b:   e4                      .byte 0xe4
      18 
      19 f010000c <entry>:
      20 f010000c:   66 c7 05 72 04 00 00    movw   $0x1234,0x472
    

    不确定是上面的还是下面的,gdb调试后得知:

    => 0x10000c:	movw   $0x1234,0x472
    0x0010000c in ?? ()
    
  3. kernel的第一条指令:

    位于0xf0100000 ,这条指令不知道有没有执行? 也没看懂意思

  4. boot/main.c的代码:

    首先读取一页,这一页应该包含了ELF文件的header. 然后boot loader从header里获取信息.

    这里的ph是ProgHdr的缩写? 也就是程序段header的意思?

    感觉需要了解ELF文件.

    网友fatsheep9146的回答如下:

    答:首先关于操作系统一共有多少个段,每个段又有多少个扇区的信息位于操作系统文件中的Program Header Table中。这个表中的每个表项分别对应操作系统的一个段。并且每个表项的内容包括这个段的大小,段起始地址偏移等等信息。所以如果我们能够找到这个表,那么就能够通过表项所提供的信息来确定内核占用多少个扇区。

    那么关于这个表存放在哪里的信息,则是存放在操作系统内核映像文件的ELF头部信息中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验目标: 本实验的目标是完成一个可以在QEMU仿真器上运行的x86操作系统。具体地说,我们将编写引导扇区代码和内核代码,并将它们组合成一个可引导的磁盘映像。最后,我们将使用QEMU仿真器启动我们的操作系统。 实验步骤: 1. 准备工作 准备工作包括安装必要的软件和工具、下载实验代码和文档等。 2. 编写引导扇区代码 引导扇区是操作系统的第一个扇区,它需要被放置在磁盘的第一个扇区。引导扇区必须包含一个512字节的主引导记录(MBR),其中包括一个引导程序和分区表。我们需要编写一个能够在引导扇区中运行的汇编代码,它将加载内核并将控制权转交给内核。 3. 编写内核代码 内核是操作系统的核心部分,它负责管理计算机的硬件资源、提供系统调用接口等。我们需要编写一个简单的内核,该内核将输出“Hello, world!”并进入无限循环。我们可以使用C语言编写内核代码,并使用GCC编译器将其编译成汇编代码。 4. 构建磁盘映像 我们需要将引导扇区和内核代码组合成一个可引导的磁盘映像。为此,我们可以使用dd命令将引导扇区和内核代码写入一个空白磁盘映像中。 5. 启动操作系统 最后,我们需要使用QEMU仿真器启动我们的操作系统。我们可以使用以下命令启动QEMU并加载磁盘映像: ``` qemu-system-i386 -hda os.img ``` 实验结果: 经过以上步骤,我们成功地编写了一个简单的操作系统,并使用QEMU仿真器进行了测试。当我们启动操作系统时,它将输出“Hello, world!”并进入无限循环。 实验总结: 本实验让我了解了操作系统的基本概念和架构,并学会了如何编写一个简单的操作系统。通过实验,我更深入地理解了计算机系统的底层原理,对操作系统的工作原理有了更深入的了解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值