发布于 

MIT 6.828 Lab1

Notes of MIT 6.828 Lab1.

Part 1: PC Bootstrap

The PC’s Physical Address Space

A PC’s physical address space is hard-wired to have the following general layout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+------------------+  <- 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

The ROM BIOS

The IBM PC starts with CS = 0xf000 and IP = 0xfff0, executing at physical address 0x000ffff0, which is at the very top of the 64KB area reserved for the ROM BIOS.

In real mode (the mode that PC starts off in), address translation works according to the formula: physical address = 16 * segment + offset.

The BIOS in a PC is “hard-wired” to the physical address range 0x000f0000-0x000fffff, this design ensures that the BIOS always gets control of the machine first after power-up or any system restart.

Exercise 2

Generally, the BIOS performs the following tasks

  • Sets up an IDT (Interrupt Descriptor Table,中断向量表)
  • Initializes various devices such as the VGA display
  • After initializing the PCI bus and all the important devices the BIOS knows about, it searches for a bootable device such as a floppy, hard drive, or CD-ROM. When it finds a bootable disk, the BIOS reads the boot loader from the disk and transfers control to it.

Part 2: The Boot Loader

The Boot loader

Floppy and hard disks for PCs are divided into 512 byte regions called sectors. The first sector of a bootable disk is called the boot sector.

When the BIOS finds a bootable floppy or hard disk, it loads the 512-byte boot sector into memory at physical addresses 0x7c00 through 0x7dff, and then uses a jmp instruction to set the CS:IP to 0000:7c00, passing control to the boot loader. Like the BIOS load address, these addresses are fairly arbitrary - but they are fixed and standardized for PCs.

The boot loader performs the following functions:

  • First, switch the processor from real mode to 32-bit protected mode, because it is only in this mode that software can access all the memory above 1MB in the processor’s physical address space. The boot loader sets the $CR0_PE_ON bit in register cr0 and loads the GDT.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
      # 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

    # 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

GDT (Global Descriptor Table) is a significant structure in protected mode.
Typically, we access memory address by selector:offset. In real mode, a selector is an paragraph number of physical memory. In protected mode, a selector value is an intex into a descriptor table, and each segment is assigned an entry in a descriptor table.
The whole system has one Global Descriptor Table. Its entry address and limit of length are hold in register GDTR. During protected mode initialization, we have to use the instruction lgdt to assign GDTR with a new value.
The structure of GDT entry is as the following. It specifies the base and limit address of the segment and access properties (that’s why it’s called protected mode).

1
2
3
4
5
6
7
8
struc gdt_entry_struct
limit_low: resb 2
base_low: resb 2
base_middle: resb 1
access: resb 1
granularity: resb 1
base_high: resb 1
end struc
  • Second, the boot loader reads the kernel from the hard disk by directly accessing the IDE disk device registers via the x86’s special I/O instructions.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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))();

Exercise 3

  • After this instruction ljmp $PROT_MODE_CSEG, $protcseg, the processor starts to work in 32-bit mode. The cause is that ljmp loads the value of $PROT_MODE_CSEG into CS, and the value of $protcseg into EIP. After that, the processor will use the value of CS as an index of GDT to find the corresponding segment descriptor.
  • The last instruction of the boot loader is call *0x10018, the first instruction of the kernel is movw $0x1234,0x472.
  • The first instruction of the kernel is in file kern/entry.s, at address 0x10000c.
  • The boot loader reads the ELFHDR->e_phnum from kernel’s elf header, and uses the ph->p_memsz to decide how many sectors to read per segment.

Loading the kernel

We can examine the ELF section headers like this:

1
2
3
4
5
6
7
8
❯ objdump -h obj/kern/kernel

obj/kern/kernel: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001acd f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
  • LMA(load address): The load address of a section is the memory address at which that section should be loaded into memory.
  • VMA(link address): The link address of a section is the memory address from which the section expects to execute.
    Typically, the link and load addresses are the same.

The boot loader users the ELF program headers to decide how to load the sections. The program headers specify which parts of the ELF object to load into memory and the destination address each should occupy.
We can inspect the program header like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ objdump -x obj/kern/kernel

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

Program Header:
LOAD off 0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
filesz 0x00007e20 memsz 0x00007e20 flags r-x
LOAD off 0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12
filesz 0x0000b6a8 memsz 0x0000b6a8 flags rw-
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
filesz 0x00000000 memsz 0x00000000 flags rwx

vaddr means virtual addresss and paddr means physical address.

Exercise 5

We can change the link address of boot loader by modifying option -Ttext boot/Makefrag. For example, 0x7d00.

After changing this, boot loader will break at ljmp $PROT_MODE_CSEG, $protcseg because the boot loader is executing from an address (0x7c00) that is different from the address from which it expects to execute (0x7d00).

1
2
(gdb) si
[ 0:7c2d] => 0x7c2d: ljmp $0x8,$0x7d32

Exercise 6

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.

The two results are different because the boot loader loads the kernel into that address.

Part 3: The Kernel

Using virtual memory to work around position dependence

As we inspected above, there was a (rather large) disparity between the kernel’s link address (as printed by objdump) and its load address.

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.

We 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).

For now, we’ll just map the first 4MB of physical memory, using the hand-written, statically-initialized page directory and page table in kern/entrypgdir.c.

After kern/entry.S sets the CR0_PG flag, paging is enabled, memory references are virtual addresses that get translated by the virtual memory hardware to physical addresses. entry_pgdir translates virtual addresses in the range 0xf0000000 through 0xf0400000 to physical addresses 0x00000000 through 0x00400000, as well as virtual addresses 0x00000000 through 0x00400000 to physical addresses 0x00000000 through 0x00400000.

Exercise 7

We can examine memory at 0x00100000 and at 0xf0100000 before and after the movl %eax, %cr0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
=> 0x100025:    mov    %eax,%cr0
0x00100025 in ?? ()
(gdb) x/10gx 0x00100000
0x100000: 0x000000001badb002 0x7205c766e4524ffe
0x100010: 0x2000b81234000004 0xc0200fd8220f0011
0x100020: 0xc0220f800100010d 0xbde0fff010002fb8
0x100030: 0x110000bc00000000 0xfeeb0000006ce8f0
0x100040: 0x56e58955fb1e0ff3 0xc3810000017ee853
(gdb) x/10gx 0xf0100000
0xf0100000 <_start-268435468>: 0x0000000000000000 0x0000000000000000
0xf0100010 <entry+4>: 0x0000000000000000 0x0000000000000000
0xf0100020 <entry+20>: 0x0000000000000000 0x0000000000000000
0xf0100030 <relocated+1>: 0x0000000000000000 0x0000000000000000
0xf0100040 <test_backtrace>: 0x0000000000000000 0x0000000000000000
(gdb) ni
=> 0x100028: mov $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/10gx 0xf0100000
0xf0100000 <_start-268435468>: 0x000000001badb002 0x7205c766e4524ffe
0xf0100010 <entry+4>: 0x2000b81234000004 0xc0200fd8220f0011
0xf0100020 <entry+20>: 0xc0220f800100010d 0xbde0fff010002fb8
0xf0100030 <relocated+1>: 0x110000bc00000000 0xfeeb0000006ce8f0
0xf0100040 <test_backtrace>: 0x56e58955fb1e0ff3 0xc3810000017ee853

The first instruction that would fail if the mapping weren’t in place is mov $relocated, %eax.

Formatted Printing to the Console

The formatted print function int cprintf(const char *fmt, ...) is implemented in kern/printf.c, which relys on lib/printfmt.c and kern/console.c.

Exercise 8

Complete the code of vprintfmt in lib/printfmt.c to print octal numbers using patterns of the form “%o”.

1
2
3
4
5
6
// (unsigned) octal
case 'o':
// Replace this with your code.
num = getuint(&ap, lflag);
base = 8;
goto number;
  1. The interface between printf.c and console.c is function void cputchar(int c) that console.c exports.

    printf.c calls it in static void putch(int ch, int *cnt), and passes the pointer to putch as an argument to vprintfmt((void*)putch, &cnt, fmt, ap) that is called in function vcprintf. And vcprintf is called by function cprintf.

  2. The following code is in function static void cga_putc(int c), which is used to output characters to CGA/VGA display.

    The purpose of this piece of code is to deal with the situation where the position of the cursor exceeds the screen.

    crt_pos is the position of next character to be output, on the other hand, the position of the cursor. And CRT_SIZE = CRT_ROWS * CRT_COLS, so it means the maximum number of characters that can be displayed on the screen.

    It moves all the characters on the screen upward by one row (using memmove) , and fills the last row with 0x0x00 | ' ', and ends by recalculating crt_pos.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // What is the purpose of this?
    if (crt_pos >= CRT_SIZE) {
    int i;

    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;
    }
  3. fmt points to the address of format string "x %d, y %x, z %d\n".

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    vcprintf(fmt, ap); // called by cprintf
    cons_putc('x');
    cons_putc(' ');
    va_arg(*ap, int);
    // now ap points to &y
    cons_putc('1');
    cons_putc(',');
    cons_putc(' ');
    cons_putc('y');
    cons_putc(' ');
    va_arg(*ap, int);
    // now ap points to &z
    cons_putc('3');
    cons_putc(',');
    cons_putc(' ');
    cons_putc('z');
    cons_putc(' ');
    va_arg(*ap, int);
    // now ap points to (char *)&z + 4
    cons_putc('4');
    cons_putc('\n');
  4. The output is He110 World. In big-endian, i should be set to 0x726c6400. We don’t need to 57616.

  5. The 4 bytes of data on the stack below 3 will be treated as an integer and output.

  6. We can change its interface so that when the function is called, arguments are passsed in reverse order.

Challenge: Enhance the console to allow text to be printed in different colors. Left to be done.

The Stack

Exercise 9

The kernel initializes its stack in entry.s.

1
2
3
4
5
6
7
# 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

# Set the stack pointer
movl $(bootstacktop),%esp

We can see the definition of bootstacktop in the data section of entry.s, the kernel reserves the space for the stack using .space KSTAKSIZE.

1
2
3
4
5
6
7
8
9
10
.data
###################################################################
# boot stack
###################################################################
.p2align PGSHIFT # force page alignment
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:

According to the result of debug, we know that the stack is located just above entry_pgtable, at 0x0xf0110000 where esp points to. And this is the lowest end of the stack.

1
2
3
4
5
6
(gdb) si
=> 0xf0100034 <relocated+5>: mov $0xf0110000,%esp
relocated () at kern/entry.S:77
77 movl $(bootstacktop),%esp
(gdb) p $esp
$1 = (void *) 0xf0110000 <entry_pgtable>

Various x86 instructions, such as call, are “hard-wired” to use the esp.

The ebp (base pointer) register, in contrast, is associated with the stack primarily by software convention. On entry to a C function, the function’s prologue code normally saves the previous function’s base pointer by pushing it onto the stack, and then copies the current esp value into ebp for the duration of the function.

Exercise 10

The address of the test_backtrace function is 0xf0100040.

First, it is called by function void i386_init(void).

1
2
// Test the stack backtrace function (lab 1 only)
test_backtrace(5);

It also recursively calls itself.

1
2
3
4
5
6
cprintf("entering test_backtrace %d\n", x);
if (x > 0)
test_backtrace(x-1);
else
mon_backtrace(0, 0, 0);
cprintf("leaving test_backtrace %d\n", x);

Five 32-bit words are pushed on the stack by each recursive nesting call.

They are saved ebx, saved esi, saved ebp, saved eip, and an argument x.

Exercise 11

Implement the mon_backtrace() function to print the stack backtrace, and hook this function into the kernel monitor’s commmand list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static struct Command commands[] = {
{ "help", "Display this list of commands", mon_help },
{ "kerninfo", "Display information about the kernel", mon_kerninfo },
{ "backtrace", "Display stack backtrace", mon_backtrace },
};

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
cprintf("Stack backtrace:\n");
uint32_t cur_ebp;
cur_ebp = read_ebp();
while (cur_ebp) {
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",
cur_ebp, ((uint32_t *)cur_ebp)[1], ((uint32_t *)cur_ebp)[2], ((uint32_t *)cur_ebp)[3],
((uint32_t *)cur_ebp)[4], ((uint32_t *)cur_ebp)[5], ((uint32_t *)cur_ebp)[6], ((uint32_t *)cur_ebp)[7]);

cur_ebp = ((uint32_t *)cur_ebp)[0];
}
return 0;
}
1
2
3
4
5
K> backtrace
Stack backtrace:
ebp f0110f58 eip f0100a99 args 00000001 f0110f80 00000000 f0100b01 f0100aa8
ebp f0110fd8 eip f0100109 args 00000000 00001aac 00000640 00000000 00000000
ebp f0110ff8 eip f010003e args 00000003 00001003 00002003 00003003 00004003

Exercise 12

Modify the stack backtrace function to display, for each eip, the function name, source file name, and line number corresponding to that eip.

In kern/kernel.ld, we can find the code that include debugging information in kernel memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}

.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}

In the result of objdump -h obj/kern/kernel, we can find information of .stab and .stabstr in the sections of the kernel.

1
2
3
4
2 .stab         00004375  f010229c  0010229c  0000329c  2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 00001999 f0106611 00106611 00007611 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA

In the result of objdump -G obj/kern/kernel, we can examine the contents of the .stab section, that are all the symbols.

1
2
3
4
5
6
7
8
9
10
11
12
❯  objdump -G obj/kern/kernel

obj/kern/kernel: file format elf32-i386

Contents of .stab section:

Symnum n_type n_othr n_desc n_value n_strx String

-1 HdrSym 0 1438 00001998 1
0 SO 0 0 f0100000 1 {standard input}
1 SOL 0 0 f010000c 18 kern/entry.S
2 SLINE 0 44 f010000c 0

Then we run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at the init.s.

We can see the contents of the .stabs.

1
2
3
4
.Ltext0:
.stabs "gcc2_compiled.",60,0,0,0
.stabs "int:t(0,1)=r(0,1);-2147483648;2147483647;",128,0,0,0
.stabs "char:t(0,2)=r(0,2);0;127;",128,0,0,0

So the bootloader loads the symbol table in memory as part of loading the kernel binary, and debuginfo_eip reads it.

We add this piece of code in debuginfo_eip to find the line number.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Search within [lline, rline] for the line number stab.
// If found, set info->eip_line to the right line number.
// If not found, return -1.
//
// Hint:
// There's a particular stabs type used for line numbers.
// Look at the STABS documentation and <inc/stab.h> to find
// which one.
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline) {
info->eip_line = stabs[lline].n_desc;
}
else {
info->eip_line = -1;
}

And we modify the function mon_backtrace like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
cprintf("Stack backtrace:\n");
uint32_t cur_ebp, cur_eip;
struct Eipdebuginfo info;

cur_ebp = read_ebp();
while (cur_ebp) {
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",
cur_ebp, ((uint32_t *)cur_ebp)[1], ((uint32_t *)cur_ebp)[2], ((uint32_t *)cur_ebp)[3],
((uint32_t *)cur_ebp)[4], ((uint32_t *)cur_ebp)[5], ((uint32_t *)cur_ebp)[6], ((uint32_t *)cur_ebp)[7]);

cur_eip = ((uint32_t *)cur_ebp)[1];
debuginfo_eip(cur_eip, &info);
cprintf(" %s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, cur_eip - info.eip_fn_addr);

cur_ebp = ((uint32_t *)cur_ebp)[0];
}
return 0;
}
1
2
3
4
5
6
7
8
K> backtrace
Stack backtrace:
ebp f0110f58 eip f0100abe args 00000001 f0110f80 00000000 f0100b26 f0100acd
kern/monitor.c:138: monitor+343
ebp f0110fd8 eip f0100109 args 00000000 00001aac 00000640 00000000 00000000
kern/init.c:43: i386_init+95
ebp f0110ff8 eip f010003e args 00000003 00001003 00002003 00003003 00004003
kern/entry.S:83: <unknown>+0

Now, finally, lab1 is finished. Run make grade to see the grades.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
❯ make grade
make clean
make[1]: Entering directory '/home/shrimp/myfiles/6.828/lab'
rm -rf obj .gdbinit jos.in qemu.log
make[1]: Leaving directory '/home/shrimp/myfiles/6.828/lab'
./grade-lab1
make[1]: Entering directory '/home/shrimp/myfiles/6.828/lab'
+ 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
ld: warning: section `.bss' type changed to PROGBITS
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 412 bytes (max 510)
+ mk obj/kern/kernel.img
make[1]: Leaving directory '/home/shrimp/myfiles/6.828/lab'
running JOS: (1.4s)
printf: OK
backtrace count: OK
backtrace arguments: OK
backtrace symbols: OK
backtrace lines: OK
Score: 50/50