MIT 6.828 Lab 1: Booting a PC

课程地址:6.828 Operating System Engineering

MIT 6.828 Operating System Engineering的大作业共由6个Lab组成,这是其中第一个。这6个Lab组合在一起便是MIT用于教学的操作系统jos。在每个Lab中,学生会被要求实现某些功能。

Lab 1 Overview:

准备工作

准备工作包括依赖环境安装与源码克隆。我的开发环境是64-bit Ubuntu 18.04。

依赖环境安装

为了安全以及调试方便等原因,32-bit jos将运行在QEMU模拟器中。为了编译与测试内核,还需要安装Compiler Toolchain(汇编器,连接器,C编译器)以及调试器。

Compiler Toolchain

Linux系统一般提供了本课程所需的Toolchain。使用以下两条命令查看Toolchain是否安装

% objdump -i

输出第2行应有 elf32-i386 字样。

% gcc -m32 -print-libgcc-file-name

输出应类似 /usr/lib/gcc/i486-linux-gnu/version/libgcc.a 或 /usr/lib/gcc/x86_64-linux-gnu/version/32/libgcc.a

在64位机器上,还需安装32-bit支持库。在Ubuntu Linux上,输入以下命令

% sudo apt-get install gcc-multilib

QEMU模拟器

课程设计者对于官方QEMU模拟器打了补丁使得其更好地工作,遵循以下步骤来安装补丁版本。

  1. 克隆课程补丁版本IAP 6.828 QEMU git仓库(可能会较慢):
    git clone https://github.com/mit-pdos/6.828-qemu.git qemu
  2. 在Linux上,还需安装以下库:
    % sudo apt-get install libsdl1.2-dev libtool-bin libglib2.0-dev libz-dev libpixman-1-dev
  3. 配置源代码:
    ./configure --disable-kvm --disable-werror [--prefix=PFX] --target-list="i386-softmmu x86_64-softmmu"

    prefix参数可选,PFX用于指明QEMU安装路径。默认为/usr/local

  4. 使用
    make && make install

    进行安装。

 源码克隆

非常简单。在这里我遵循官方教程。

1 mkdir ~/6.828
2 cd ~/6.828
3 git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
4 cd lab

现在,Lab所需的代码就已经clone至~/6.828/lab文件夹下了。接着测试内核是否能正常启动,在lab文件夹中键入make命令:

% make
+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 380 bytes (max 510)
+ mk obj/kern/kernel.img

然后键入make qemu-nox命令:

% make qemu-nox
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>

在这里也可以键入make qemu命令,效果是相同的,只不过没有-nox后缀的命令会打开一个QEMU Window,这需要图形化界面支持。而我使用ssh连接服务器,只能使用带有-nox后缀的命令。现在的内核非常简陋,只有两条命令可以执行:helpkerninfo

启动与调试

启动过程在前一部分已经介绍过,这里不再赘述。在进行调试之前,首先了解一下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

注意BIOS ROM,它占据了 0x000F0000 到 0x000FFFFF 的64KB空间。

The BIOS is responsible for performing basic system initialization such as activating the video card and checking the amount of memory installed. After performing this initialization, the BIOS loads the operating system from some appropriate location such as floppy disk, hard disk, CD-ROM, or the network, and passes control of the machine to the operating system.

The ROM BIOS

Exercise 2. Use GDB's si (Step Instruction) command to trace into the ROM BIOS for a few more instructions, and try to guess what it might be doing. You might want to look at Phil Storrs I/O Ports Description, as well as other materials on the 6.828 reference materials page. No need to figure out all the details - just the general idea of what the BIOS is doing first.

在这一节我们会对内核进行调试来观察BIOS的工作过程。在lab文件夹下分别打开两个终端。一个终端键入make qemu-nox-gdb,这会启动QEMU,不过QEMU会在执行第一条指令前停止并等待GDB的接入。此时在另一个终端键入make gdb,就会进入debug模式。可以得到如下输出:

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

Line 2为下一条指令(这里也是第一条指令)。此时我们有CS=0xf000IP=0xfff0。容易看出,这条指令位于BIOS ROM空间的顶端。现在需要跳转至BIOS ROM中间一点的位置,即CS=0xf000IP=0xe05b。使用si命令单步调试,使用info reg查看寄存器内容。

(gdb) si
[f000:e05b]    0xfe05b: cmpl   $0x0,%cs:0x6ac8
0x0000e05b in ?? ()
(gdb)

使用x/<number>i addr查看addr开始的内存,并以指令格式显示,显示指令的条数为<number>x/i addr表示将addr开始的内存examine as instructions,且指令条数为1。

(gdb) x/10i 0xfe05b
   0xfe05b:     cmpl   $0x0,%cs:0x6ac8
   0xfe062:     jne    0xfd2e1
   0xfe066:     xor    %dx,%dx
   0xfe068:     mov    %dx,%ss
   0xfe06a:     mov    $0x7000,%esp
   0xfe070:     mov    $0xf34c2,%edx
   0xfe076:     jmp    0xfd15c
   0xfe079:     push   %ebp
   0xfe07b:     push   %edi
   0xfe07d:     push   %esi
(gdb)

第一条指令为cmp指令,立即数0x0与内存单元CS:0x6ac8中的数据比较大小。相等则将flags中的ZF位(Zero Flag)置为1;不等则置为0。第二条指令为jne指令(Jump if Not Equal),若ZF=0则跳转至标号处执行。使用examine(简写为x)来查看内存中的内容。

(gdb) x 0xf6ac8
0xf6ac8:        0
(gdb)

所以不会跳转。可以看出jne后的3条指令为设置栈段与栈顶的指令。继续执行一直到0xfe076之后,跳转到0xfd15c

(gdb) si
[f000:d15c]    0xfd15c: mov    %eax,%ecx
0x0000d15c in ?? ()
(gdb)

查看0xfd15c开始的10条指令。

(gdb) x/10i 0xfd15c
   0xfd15c:     mov    %eax,%ecx
   0xfd15f:     cli    
   0xfd160:     cld    
   0xfd161:     mov    $0x8f,%eax
   0xfd167:     out    %al,$0x70
   0xfd169:     in     $0x71,%al
   0xfd16b:     in     $0x92,%al
   0xfd16d:     or     $0x2,%al
   0xfd16f:     out    %al,$0x92
   0xfd171:     lidtw  %cs:0x6ab8
(gdb)
  • cli:禁止中断发生
  • cld:将标志寄存器Flag的方向标志位DF清零
  • out与in:通过Ports(70、71、92)与I/O设备交互
  • or:按位逻辑或

继续查看

(gdb) x/10i 0xfd171
   0xfd171:     lidtw  %cs:0x6ab8
   0xfd177:     lgdtw  %cs:0x6a74
   0xfd17d:     mov    %cr0,%eax
   0xfd180:     or     $0x1,%eax
   0xfd184:     mov    %eax,%cr0
   0xfd187:     ljmpl  $0x8,$0xfd18f
   0xfd18f:     mov    $0x10,%ax
   0xfd192:     add    %al,(%bx,%si)
   0xfd194:     mov    %ax,%ds
   0xfd196:     mov    %ax,%es
(gdb)
  • cr0:control register,which changes or controls the general behavior of a CPU or other digital device
BitNameFull NameDescription
0 PE Protected Mode Enable If 1, system is in protected mode, else system is in real mode
  • ljmp:ljmp specifies a code segment to switch to in addition to the address to jump to

查看0xfd18f开始的10条指令(此时工作在保护模式中)

(gdb) x/10i 0xfd18f
   0xfd18f:     mov    $0x10,%eax
   0xfd194:     mov    %eax,%ds
   0xfd196:     mov    %eax,%es
   0xfd198:     mov    %eax,%ss
   0xfd19a:     mov    %eax,%fs
   0xfd19c:     mov    %eax,%gs
   0xfd19e:     mov    %ecx,%eax
   0xfd1a0:     jmp    *%edx
   0xfd1a2:     mov    %eax,%ecx
   0xfd1a4:     mov    $0x20,%eax
(gdb)

可以看出主要是对寄存器赋值。

The Boot Loader

Exercise 3. Take a look at the lab tools guide, especially the section on GDB commands. Even if you're familiar with GDB, this includes some esoteric GDB commands that are useful for OS work.

Set a breakpoint at address 0x7c00, which is where the boot sector will be loaded. Continue execution until that breakpoint. Trace through the code in boot/boot.S, using the source code and the disassembly file obj/boot/boot.asm to keep track of where you are. Also use the x/i command in GDB to disassemble sequences of instructions in the boot loader, and compare the original boot loader source code with both the disassembly in obj/boot/boot.asm and GDB.

Trace into bootmain() in boot/main.c, and then into readsect(). Identify the exact assembly instructions that correspond to each of the statements in readsect(). Trace through the rest of readsect() and back out into bootmain(), and identify the begin and end of the for loop that reads the remaining sectors of the kernel from the disk. Find out what code will run when the loop is finished, set a breakpoint there, and continue to that breakpoint. Then step through the remainder of the boot loader.

At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?

从boot.S中以下代码开始:

  # 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.
  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

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

Boot loader调用的最后一条指令是位于main.c中的

        // call the entry point from the ELF header
        // note: does not return!
        ((void (*)(void)) (ELFHDR->e_entry))();

在boot.asm中得知该指令地址为0x7d6b,打断点执行至该指令处。下一条指令为

=> 0x7d6b:    call   *0x10018

表示跳转至[10018]处,查看内存

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

则下一条指令(即kernel的第一条指令位于0x1000c),查看内存

(gdb) x/i 0x10000c
=> 0x10000c:    movw   $0x1234,0x472

此即为kernel的第一条指令。

Where is the first instruction of the kernel?

由前可知,kernel的第一条指令位于0x1000c

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?

在main.c中

    // 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);

显然,是根据Boot loader读入的第一个sector(ELFHDR)的e_phnum决定的。

Loading the kernel

Exercise 4. Read about programming with pointers in C. The best reference for the C language is The C Programming Language by Brian Kernighan and Dennis Ritchie (known as 'K&R'). We recommend that students purchase this book (here is an Amazon Link) or find one of MIT's 7 copies.

Read 5.1 (Pointers and Addresses) through 5.5 (Character Pointers and Functions) in K&R. Then download the code for pointers.c, run it, and make sure you understand where all of the printed values come from. In particular, make sure you understand where the pointer addresses in printed lines 1 and 6 come from, how all the values in printed lines 2 through 4 get there, and why the values printed in line 5 are seemingly corrupted.

There are other references on pointers in C (e.g., A tutorial by Ted Jensen that cites K&R heavily), though not as strongly recommended.

Warning: Unless you are already thoroughly versed in C, do not skip or even skim this reading exercise. If you do not really understand pointers in C, you will suffer untold pain and misery in subsequent labs, and then eventually come to understand them the hard way. Trust us; you don't want to find out what "the hard way" is.

 比较基本的指针知识。

To make sense out of boot/main.c you'll need to know what an ELF binary is. When you compile and link a C program such as the JOS kernel, the compiler transforms each C source ('.c') file into an object ('.o') file containing assembly language instructions encoded in the binary format expected by the hardware. The linker then combines all of the compiled object files into a single binary image such as obj/kern/kernel, which in this case is a binary in the ELF format, which stands for "Executable and Linkable Format".

An ELF binary starts with a fixed-length ELF header, followed by a variable-length program header listing each of the program sections to be loaded. The C definitions for these ELF headers are in inc/elf.h. The program sections we're interested in are:

  • .text: The program's executable instructions.
  • .rodata: Read-only data, such as ASCII string constants produced by the C compiler. (We will not bother setting up the hardware to prohibit writing, however.)
  • .data: The data section holds the program's initialized data, such as global variables declared with initializers like int x = 5;.

When the linker computes the memory layout of a program, it reserves space for uninitialized global variables, such as int x;, in a section called .bss that immediately follows .data in memory. C requires that "uninitialized" global variables start with a value of zero. Thus there is no need to store contents for .bss in the ELF binary; instead, the linker records just the address and size of the .bss section. The loader or the program itself must arrange to zero the .bss section.

Examine the full list of the names, sizes, and link addresses of all the sections in the kernel executable by typing:

objdump -h obj/kern/kernel
shawn@ubuntu:~/6.828/lab$ objdump -h obj/kern/kernel

obj/kern/kernel:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         000019e9  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       000006c0  f0101a00  00101a00  00002a00  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00003b95  f01020c0  001020c0  000030c0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      00001948  f0105c55  00105c55  00006c55  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         00009300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .got          00000008  f0111300  00111300  00012300  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .got.plt      0000000c  f0111308  00111308  00012308  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data.rel.ro.local 00000044  f0113000  00113000  00014000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  9 .bss          00000648  f0113060  00113060  00014060  2**5
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      00000029  00000000  00000000  000146a8  2**0
                  CONTENTS, READONLY

Take particular note of the "VMA" (or link address) and the "LMA" (or load address) of the .text section. The load address of a section is the memory address at which that section should be loaded into memory.

The link address of a section is the memory address from which the section expects to execute. The linker encodes the link address in the binary in various ways, such as when the code needs the address of a global variable, with the result that a binary usually won't work if it is executing from an address that it is not linked for. (It is possible to generate position-independent code that does not contain any such absolute addresses. This is used extensively by modern shared libraries, but it has performance and complexity costs, so we won't be using it in 6.828.)

VMA为Virtual Memory Address,为虚拟内存地址,为Linker链接之后的地址;LMA为Load Memory Address,为加载内存地址,为Loader装入内存之后的地址。

shawn@ubuntu:~/6.828/lab$ objdump -h obj/boot/boot.out

obj/boot/boot.out:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000186  00007c00  00007c00  00000074  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .eh_frame     000000a8  00007d88  00007d88  000001fc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         0000087c  00000000  00000000  000002a4  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      00000925  00000000  00000000  00000b20  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .comment      00000029  00000000  00000000  00001445  2**0
                  CONTENTS, READONLY

可以看出boot.out中VMA与LMA相等。

Back in boot/main.c, the ph->p_pa field of each program header contains the segment's destination physical address.

Exercise 5. Trace through the first few instructions of the boot loader again and identify the first instruction that would "break" or otherwise do the wrong thing if you were to get the boot loader's link address wrong. Then change the link address in boot/Makefrag to something wrong, run make clean, recompile the lab with make, and trace into the boot loader again to see what happens. Don't forget to change the link address back and make clean again afterward!

Look back at the load and link addresses for the kernel. Unlike the boot loader, these two addresses aren't the same: the kernel is telling the boot loader to load it into memory at a low address (1 megabyte), but it expects to execute from a high address. We'll dig in to how we make this work in the next section.

Besides the section information, there is one more field in the ELF header that is important to us, named e_entry. This field holds the link address of the entry point in the program: the memory address in the program's text section at which the program should begin executing. You can see the entry point:

shawn@ubuntu:~/6.828/lab$ objdump -f obj/kern/kernel

obj/kern/kernel:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c

可以看出与之前的分析相符合。

You should now be able to understand the minimal ELF loader in boot/main.c. It reads each section of the kernel from disk into memory at the section's load address and then jumps to the kernel's entry point.

Exercise 6. We can examine memory using GDB's x command. The GDB manual has full details, but for now, it is enough to know that the command x/NADDR prints N words of memory at ADDR. (Note that both 'x's in the command are lowercase.) Warning: The size of a word is not a universal standard. In GNU assembly, a word is two bytes (the 'w' in xorw, which stands for word, means 2 bytes).

Reset the machine (exit QEMU/GDB and start them again). Examine the 8 words of memory at 0x00100000 at the point the BIOS enters the boot loader, and then again at the point the boot loader enters the kernel. Why are they different? What is there at the second breakpoint? (You do not really need to use QEMU to answer this question. Just think.)

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

Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x/8x 0x100000
0x100000:    0x00000000    0x00000000    0x00000000    0x00000000
0x100010:    0x00000000    0x00000000    0x00000000    0x00000000
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x10000c:    movw   $0x1234,0x472

Breakpoint 2, 0x0010000c in ?? ()
(gdb) x/8x 0x100000
0x100000:    0x1badb002    0x00000000    0xe4524ffe    0x7205c766
0x100010:    0x34000004    0x2000b812    0x220f0011    0xc0200fd8

之前查看了kernel中各个section的装入地址,可以看出.text被装入至0x100000处。

The Kernel

Operating system kernels often like to be linked and run at very high virtual address, such as 0xf0100000, in order to leave the lower part of the processor's virtual address space for user programs to use.

Many machines don't have any physical memory at address 0xf0100000, so we can't count on being able to store the kernel there. Instead, we will use the processor's memory management hardware to map virtual address 0xf0100000 (the link address at which the kernel code expects to run) to physical address 0x00100000 (where the boot loader loaded the kernel into physical memory).

Exercise 7. Use QEMU and GDB to trace into the JOS kernel and stop at the movl %eax, %cr0. Examine memory at 0x00100000 and at 0xf0100000. Now, single step over that instruction using the stepi GDB command. Again, examine memory at 0x00100000 and at 0xf0100000. Make sure you understand what just happened.

What is the first instruction after the new mapping is established that would fail to work properly if the mapping weren't in place? Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right.

观察obj/kern/kernel.asm可知,movl %eax, %cr0的link address为0xf0100025,load address为0x100025。打断点并执行至该处

(gdb) b *0x100025
Breakpoint 1 at 0x100025
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x100025:    mov    %eax,%cr0

Breakpoint 1, 0x00100025 in ?? ()
(gdb) x/8x 0x100000
0x100000:    0x1badb002    0x00000000    0xe4524ffe    0x7205c766
0x100010:    0x34000004    0x2000b812    0x220f0011    0xc0200fd8
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>:    0x00000000    0x00000000    0x00000000    0x00000000
0xf0100010 <entry+4>:    0x00000000    0x00000000    0x00000000    0x00000000
(gdb) si => 0
x100028: mov $0xf010002f,%eax 0x00100028 in ?? () (gdb) x/8x 0x100000 0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8 (gdb) x/8x 0xf0100000 0xf0100000 <_start+4026531828>: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0xf0100010 <entry+4>: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8 (gdb)

可以看出,执行movl %eax, %cr0命令之后,高地址空间已被映射至低地址空间。将该命令注释后重新运行。

        # Turn on paging.
        movl    %cr0, %eax
f010001d:       0f 20 c0                mov    %cr0,%eax
        orl     $(CR0_PE|CR0_PG|CR0_WP), %eax
f0100020:       0d 01 00 01 80          or     $0x80010001,%eax
#       movl    %eax, %cr0

        # Now paging is enabled, but we're still running at a low EIP
        # (why is this okay?).  Jump up above KERNBASE before entering
        # C code.
        mov     $relocated, %eax
f0100025:       b8 2c 00 10 f0          mov    $0xf010002c,%eax
        jmp     *%eax
f010002a:       ff e0                   jmp    *%eax

f010002c <relocated>:
relocated:

        # Clear the frame pointer register (EBP)
        # so that once we get into debugging C code,
        # stack backtraces will be terminated properly.
        movl    $0x0,%ebp                       # nuke frame pointer
f010002c:       bd 00 00 00 00          mov    $0x0,%ebp

why is this okay?是因为该段程序之前已被Boot Loader装入内存。重新调试

(gdb) si
=> 0xf010002c <relocated>:    add    %al,(%eax)
relocated () at kern/entry.S:74
74        movl    $0x0,%ebp            # nuke frame pointer
(gdb) si
Remote connection closed

kernel报错信息

qemu: fatal: Trying to execute code outside RAM or ROM at 0xf010002c

可以看出是在0xf010002c处出错,因为0xf010002a处(已被Boot Loader装入内存)的指令要求跳转至0xf010002c。由于未开启paging,无法将0xf010002c映射至低地址,故出错。

Formatted Printing to the Console

Read through kern/printf.clib/printfmt.c, and kern/console.c, and make sure you understand their relationship. It will become clear in later labs why printfmt.c is located in the separate lib directory.

Exercise 8. We have omitted a small fragment of code - the code necessary to print octal numbers using patterns of the form "%o". Find and fill in this code fragment.

这段代码在lib/printfmt.c中Line206 - 212

// (unsigned) octal 
case 'o': 
  // Replace this with your code. 
  putch('X', putdat); 
  putch('X', putdat); 
  putch('X', putdat);
  break;

 仿照%u,可以较容易地写出

// (unsigned) octal
case 'o':
    // Replace this with your code.
    num = getuint(&ap, lflag);
    base = 8;
    goto number;

Be able to answer the following questions:

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中定义的putch()方法。而printf.c中的putch()方法则是包装了console.c中的cputchar()方法。

2. Explain the following from console.c:

1      if (crt_pos >= CRT_SIZE) {
2              int i;
3              memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4              for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5                      crt_buf[i] = 0x0700 | ' ';
6              crt_pos -= CRT_COLS;
7      }

当显示缓存区(crt_buf)满时,刷新一行,并将最下面一行置为白底空格。

3. For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC's calling convention on the x86.

Trace the execution of the following code step-by-step:

int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);

 

    • In the call to cprintf(), to what does fmt point? To what does ap point?
    • List (in order of execution) each call to cons_putcva_arg, and vcprintf. For cons_putc, list its argument as well. For va_arg, list what ap points to before and after the call. For vcprintf list the values of its two arguments.

 

4. Run the following code.

    unsigned int i = 0x00646c72;
    cprintf("H%x Wo%s", 57616, &i);
What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise. Here's an ASCII table that maps bytes to characters.

The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here's a description of little- and big-endian and a more whimsical description.

 

 

5. In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

    cprintf("x=%d y=%d", 3);

 

 

6. Let's say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

 

Challenge Enhance the console to allow text to be printed in different colors. The traditional way to do this is to make it interpret ANSI escape sequences embedded in the text strings printed to the console, but you may use any mechanism you like. There is plenty of information on the 6.828 reference page and elsewhere on the web on programming the VGA display hardware. If you're feeling really adventurous, you could try switching the VGA hardware into a graphics mode and making the console draw text onto the graphical frame buffer.

The Stack

 

posted @ 2020-11-11 16:52  xywei0905  阅读(624)  评论(0)    收藏  举报