(二)6.828 Operating System lab1: Assembly, Tools, and Bootstrapping

part 1:PC Bootstrap


Exercise 1

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.

  熟悉汇编语言

知识点:The PC's Physical Address Space

(二)6.828 Operating System lab1: Assembly, Tools, and Bootstrapping_第1张图片

The first PCs, which were based on the 16-bit Intel 8088 processor, were only capable of addressing 1MB of physical memory. The physical address space of an early PC would therefore start at 0x00000000 but end at 0x000FFFFF instead of 0xFFFFFFFF. The 640KB area marked "Low Memory" was the only random-access memory (RAM) that an early PC could use; in fact the very earliest PCs only could be configured with 16KB, 32KB, or 64KB of RAM!

  最早的PC,实际的内存空间只有1M(0x00000000 ~ 0x000FFFFF),其中被称为Low Memory的640KB区域是唯一能提供读写的区域。

The 384KB area from 0x000A0000 through 0x000FFFFF was reserved by the hardware for special uses such as video display buffers and firmware held in non-volatile memory. The most important part of this reserved area is the Basic Input/Output System (BIOS), which occupies the 64KB region from 0x000F0000 through 0x000FFFFF. In early PCs the BIOS was held in true read-only memory (ROM), but current PCs store the BIOS in updateable flash memory. 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.

  0x000A0000~ 0x000FFFFF 是留给硬件特殊用途的内存空间。其中最重要的一块是BIOS(0x000F0000~ 0x000FFFFF),它负责初始化计算机系统,之后加载操作系统。

When Intel finally "broke the one megabyte barrier" with the 80286 and 80386 processors, which supported 16MB and 4GB physical address spaces respectively, the PC architects nevertheless preserved the original layout for the low 1MB of physical address space in order to ensure backward compatibility with existing software. Modern PCs therefore have a "hole" in physical memory from 0x000A0000 to 0x00100000, dividing RAM into "low" or "conventional memory" (the first 640KB) and "extended memory" (everything else). In addition, some space at the very top of the PC's 32-bit physical address space, above all physical RAM, is now commonly reserved by the BIOS for use by 32-bit PCI devices.

  后来计算机发展了,内存空间增大,为了维持兼容性最低地址的1M物理空间仍然保持原样,在这1M空间上方增加"extended memory"用作RAM,这样计算机的RAM区域就被分成两部分:low memory 和 extended memory。

Recent x86 processors can support more than 4GB of physical RAM, so RAM can extend further above 0xFFFFFFFF. In this case the BIOS must arrange to leave a second hole in the system's RAM at the top of the 32-bit addressable region, to leave room for these 32-bit devices to be mapped. Because of design limitations JOS will use only the first 256MB of a PC's physical memory anyway, so for now we will pretend that all PCs have "only" a 32-bit physical address space. But dealing with complicated physical address spaces and other aspects of hardware organization that evolved over many years is one of the important practical challenges of OS development.

  现在的X86内存空间又变大了(超过4G),所以RAM空间可能会超过0xFFFFFFFF,为了保留 32-bit memory mapped devices 来维持兼容性,需要再次切分RAM,超出的RAM在0xFFFFFFFF之上继续增加。方便起见,JOS只会使用前256M的内存空间。

知识点:The ROM BIOS

  使用qemu和gdb联调jos,第一条看到的指令是:

(二)6.828 Operating System lab1: Assembly, Tools, and Bootstrapping_第2张图片

The following line:

[f000:fff0] 0xffff0:	ljmp   $0xf000,$0xe05b

is GDB's disassembly of the first instruction to be executed. From this output you can conclude a few things:

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

Why does QEMU start like this? This is how Intel designed the 8088 processor, which IBM used in their original PC. Because 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 - which is crucial because on power-up there is no other software anywhere in the machine's RAM that the processor could execute. The QEMU emulator comes with its own BIOS, which it places at this location in the processor's simulated physical address space. On processor reset, the (simulated) processor enters real mode and sets CS to 0xf000 and the IP to 0xfff0, so that execution begins at that (CS:IP) segment address. How does the segmented address 0xf000:fff0 turn into a physical address?

To answer that we need to know a bit about real mode addressing. In real mode (the mode that PC starts off in), address translation works according to the formula: physical address = 16 * segment + offset. So, when the PC sets CS to 0xf000 and IP to 0xfff0, the physical address referenced is:

   16 * 0xf000 + 0xfff0   # in hex multiplication by 16 is
   = 0xf0000 + 0xfff0     # easy--just append a 0.
   = 0xffff0 

0xffff0 is 16 bytes before the end of the BIOS (0x100000). Therefore we shouldn't be surprised that the first thing that the BIOS does is jmp backwards to an earlier location in the BIOS; after all how much could it accomplish in just 16 bytes?

  这是GDB执行的第一条指令,可以看出:

  •   IBM计算机是从物理地址0x000ffff0开始执行的,也就是ROM BIOS空间的顶端。
  •   PC开始的时候,CS = 0xf000 and IP = 0xfff0。
  •   第一个被执行的指令是jmp,跳到CS = 0xf000 and IP = 0xe05b的位置。

一般来说,CPU在访问内存的时候要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。段寄存器提供段地址。典型的提供段地址和偏移地址的寄存器为CS和IP寄存器。也就是说,CPU会将CS:IP合成的物理地址指向的内容当作指令执行。

  计算机的设置就是刚开始跳到BIOS里开始执行,为的是在计算机一充电启动的时候BIOS就能获得控制权,因为刚充电后RAM里没有任何程序能够执行。计算机靠刚开始把寄存器CS设值 0xf000以及寄存器IP设值0xfff0找到BIOS。

  刚开始的时候,CS = 0xf000 and IP = 0xfff0(应该是写入硬件的,此时其他寄存器值都是0),通过计算公式0xf000:0xfff0 转化为 16 * 0xf000 + 0xfff0 = 0xffff0,0xffff0离BIOS顶端(0x100000)距离16bytes,只有16bytes没有发挥空间,所以BIOS第一件需要做得事是回跳到前面一点的位置(CS = 0xf000 and IP = 0xe05b)执行指令。

When the BIOS runs, it sets up an interrupt descriptor table and initializes various devices such as the VGA display. This is where the "Starting SeaBIOS" message you see in the QEMU window comes from.

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. Eventually, when it finds a bootable disk, the BIOS reads the boot loader from the disk and transfers control to it.

  当BIOS执行起来,它会初始化中断向量表以及一些硬件设备(比如VGA),这就是在QEMU里看到"Starting SeaBIOS"的地方。这些初始化完成后,BIOS会从软盘、硬盘的开始扇区读取引导记录,找到boot loader并将控制权转交给boot loader。

Exercise 2

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.

  熟悉GDB的si(Step Instruction)指令,并玩下执行几步,粗略看看BIOS会做什么。

  答案:并不是很能看得懂,也猜不到。

Part 2: The Boot Loader


知识点:Boot Loader

Floppy and hard disks for PCs are divided into 512 byte regions called sectors. A sector is the disk's minimum transfer granularity: each read or write operation must be one or more sectors in size and aligned on a sector boundary. If the disk is bootable, the first sector is called the boot sector, since this is where the boot loader code resides. 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 ability to boot from a CD-ROM came much later during the evolution of the PC, and as a result the PC architects took the opportunity to rethink the boot process slightly. As a result, the way a modern BIOS boots from a CD-ROM is a bit more complicated (and more powerful). CD-ROMs use a sector size of 2048 bytes instead of 512, and the BIOS can load a much larger boot image from the disk into memory (not just one sector) before transferring control to it. For more information, see the "El Torito" Bootable CD-ROM Format Specification.

  软盘和硬盘会被分成很多个512byte的分区,每个分区被称为“扇区”,扇区是磁盘的最小粒度单位。如果一个磁盘是可以启动的,那么该磁盘的第一个扇区成为启动扇区,这就是boot loader代码放置的地方。当BIOS找到启动扇区以后,把启动扇区载入内存放在0x7c00 ~ 0x7dff的位置,然后跳到CS:IP to 0000:7c00的位置,把控制权交给boot loader。  

For 6.828, however, we will use the conventional hard drive boot mechanism, which means that our boot loader must fit into a measly 512 bytes. The boot loader consists of one assembly language source file, boot/boot.S, and one C source file, boot/main.c Look through these source files carefully and make sure you understand what's going on. The boot loader must perform two main functions:

  1. First, the boot loader switches 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. Protected mode is described briefly in sections 1.2.7 and 1.2.8 of PC Assembly Language, and in great detail in the Intel architecture manuals. At this point you only have to understand that translation of segmented addresses (segment:offset pairs) into physical addresses happens differently in protected mode, and that after the transition offsets are 32 bits instead of 16.
  2. 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. If you would like to understand better what the particular I/O instructions here mean, check out the "IDE hard drive controller" section on the 6.828 reference page. You will not need to learn much about programming specific devices in this class: writing device drivers is in practice a very important part of OS development, but from a conceptual or architectural viewpoint it is also one of the least interesting.

 

  boot loader相关的源代码包括汇编语言 boot/boot.S和C语言 boot/main.c ,仔细查看代码了解流程,boot loader主要实现了两个功能:

  1.   boot loader从real mode转变成32-bit protected mode,以便有权限访问1MB以上的物理空间。
  2.   boot loader 从硬盘中读取kernel。

  boot/main.c 里面的注释很清晰的说明了boot loader的相关工作,程序boot.S和main.c一起组成了boot loader,它被存储在硬盘第一个扇区,之后的第二个扇区保存了一个ELF格式的kernle映像。

  BOOT的流程是:

  1.   CPU加载BIOS进入内存并执行。
  2.   BIOS初始化设备以及中断事件,之后从启动盘中读取第一个扇区,加载进内存并且跳到加载位置。
  3.   跳到加载boot loader的位置开始执行,boot loader接管控制权。
  4.   控制权先从boot.S开始执行,切换为保护模式(protected mode)并建立栈,准备好执行C代码,之后调用main.c()。
  5.   main.c接管控制权,加载kernel进内存并跳到加载位置。  

 

Exercise 3

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.

  边通过GDB观察代码边回答问题:

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

  答案1:

  通过 b *0x7c00 在该地址设置断点,这也是boot loader被载入的位置,GDB开始调试后直接通过c跳到断点位置执行,每一次si(运行下一条机器指令)开始与boot.S里的汇编代码一一对应,持续运行到以下ljmp指令。

  # 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

  此时GDB界面如下:

(gdb) si
[   0:7c2a] => 0x7c2a:	mov    %eax,%cr0
0x00007c2a in ?? ()
(gdb) si
[   0:7c2d] => 0x7c2d:	ljmp   $0x8,$0x7c32
0x00007c2d in ?? ()
(gdb) si
The target architecture is assumed to be i386
=> 0x7c32:	mov    $0x10,%ax
0x00007c32 in ?? ()
(gdb) 

  这一段使用ljmp指令执行段间跳转,格式是ljmp $SECTION, $OFFSET,可以注意到地址从0x7c2d变成了0x7c32,开始执行32位代码。之后就是设置好protected model底下的数据代码信息,设置堆栈,并且跳转到main.c。

  设置最早内核栈的代码如下:

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call bootmain

  结论:在执行汇编代码ljmp的时候从16bit转为32bit的模式。

 

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

  转向main.c文件后,开始执行c文件里的main函数,因为没有返回而是直接载入内核,所以bootloader最后执行的指令是main函数最后一句代码,如下所示中的((void (*)(void)) (ELFHDR->e_entry))():

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

	// read 1st page off disk
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

	// is this a valid 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 */;
}

  问题2的第二个问题,要找到kernel第一条执行的指令。通过别人博客的提醒(https://www.cnblogs.com/fatsheep9146/p/5115086.html),执行完最后一条指令会跳转到obj/kern/kernel继续执行,可以反编译kernel看看第一条指令的地址,反编译结果如下:

HemingbeardeMacBook-Pro:~ hemingbear$ cd Desktop/jos/obj/kern/
HemingbeardeMacBook-Pro:kern hemingbear$ objdump -f kernel

kernel:	file format ELF32-i386

architecture: i386
start address: 0x10000c

  可以看出kernel的代码是从0x10000c开始执行的,我们在DGB里设置断点,看看在这里会执行什么命令。

(gdb) b *0x10000c
Breakpoint 2 at 0x10000c
(gdb) c
Continuing.
=> 0x10000c:	movw   $0x1234,0x472

Breakpoint 2, 0x0010000c in ?? ()
(gdb) 

  kernel第一条执行的指令是 movw   $0x1234,0x472。在文件entry.S里面也可以找到这第一条代码。

 

  问题3:Where is the first instruction of the kernel?

  回答3:kernel的第一条地址就是反编译找到的地址,是0x10000c。

 

  问题4: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?

  通过Program Header Table中的信息来决定读取的扇区,利用objdump命令可以查看内核程序的段信息,结果如下:

HemingbeardeMacBook-Pro:kern hemingbear$ objdump -p kernel

kernel:	file format ELF32-i386

Program Header:
    LOAD off    0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
         filesz 0x0000efdf memsz 0x0000efdf flags r-x
    LOAD off    0x00010000 vaddr 0xf010f000 paddr 0x0010f000 align 2**12
         filesz 0x0000a300 memsz 0x0000a944 flags rw-

Dynamic Section:

  回答4:根据ELF格式文件头的信息确定并读取的,代码就是((void (*)(void)) (ELFHDR->e_entry))()。

Exercise 4

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 lines 1 and 6 come from, how all the values in 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.

  下载pointers.c文件,观察输出,了解C语言里面的指针。

 

知识点:ELF格式文件

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

Full information about this format is available in the ELF specification on our reference page, but you will not need to delve very deeply into the details of this format in this class. Although as a whole the format is quite powerful and complex, most of the complex parts are for supporting dynamic loading of shared libraries, which we will not do in this class. The Wikipedia page has a short description.

For purposes of 6.828, you can consider an ELF executable to be a header with loading information, followed by several program sections, each of which is a contiguous chunk of code or data intended to be loaded into memory at a specified address. The boot loader does not modify the code or data; it loads it into memory and starts executing it.

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:

athena% i386-jos-elf-objdump -h obj/kern/kernel

You can substitute objdump for i386-jos-elf-objdump if your computer uses an ELF toolchain by default like most modern Linuxen and BSDs.

You will see many more sections than the ones we listed above, but the others are not important for our purposes. Most of the others are to hold debugging information, which is typically included in the program's executable file but not loaded into memory by the program loader.

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

Typically, the link and load addresses are the same. For example, look at the .text section of the boot loader:

athena% i386-jos-elf-objdump -h obj/boot/boot.out

The boot loader uses 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. You can inspect the program headers by typing:

athena% i386-jos-elf-objdump -x obj/kern/kernel

The program headers are then listed under "Program Headers" in the output of objdump. The areas of the ELF object that need to be loaded into memory are those that are marked as "LOAD". Other information for each program header is given, such as the virtual address ("vaddr"), the physical address ("paddr"), and the size of the loaded area ("memsz" and "filesz").

Back in boot/main.c, the ph->p_pa field of each program header contains the segment's destination physical address (in this case, it really is a physical address, though the ELF specification is vague on the actual meaning of this field).

The BIOS loads the boot sector into memory starting at address 0x7c00, so this is the boot sector's load address. This is also where the boot sector executes from, so this is also its link address. We set the link address by passing -Ttext 0x7C00 to the linker in boot/Makefrag, so the linker will produce the correct memory addresses in the generated code.

    刚开始编译器把.c文件编译成汇编代码的.o文件,之后链接器把所有.o文件链接成一个binary image(如obj/kern/kernel),这是ELF格式的二进制文件,是Executable and Linkable Format的简称,也就是可执行文件。每个目标文件(.o 文件)都有自己独立的.text段(Section)和.data段(Section)等空间。链接器将多个目标文件链接之后,生成一个新的可执行文件,需要对.text段以及.data段进行空间划分。

    每个ELF文件都有一个固定长度的ELF文件头,后面跟着不同的段(section)信息。通过反编译可以看到kernel的内容:

HemingbeardeMacBook-Pro:jos hemingbear$ i386-jos-elf-objdump -h obj/kern/kernel

obj/kern/kernel:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000178e  f0100000  00100000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       00000704  f01017a0  001017a0  000027a0  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         000044e9  f0101ea4  00101ea4  00002ea4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      00008c52  f010638d  0010638d  0000738d  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         0000a300  f010f000  0010f000  00010000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .bss          00000644  f0119300  00119300  0001a300  2**5
                  ALLOC
  6 .comment      00000011  00000000  00000000  0001a300  2**0
                  CONTENTS, READONLY

每个目标文件都包含一连串的section,最常见,最基础的至少有:

  • .text 代码段,就是CPU要运行的指令代码;
  • .data 数据段,程序中包含的一些数据,放在这个段里;
  • .bss 未初始化段,记录了程序里有哪些未初始化的变量,就相当于只记录对应的名字,留着程序运行前去初始化为0,所以,此处并不占用具体空间。打个比方就是,只记录人名,没有人站在这里占地方,而对应的.text和.data段,都是既有人名(函数或者变量名),又占对应的地方(包含具体空间记录到底是什么指令代码和数据的数值是多少) 

  图中的 "LMA" (装载内存地址 load address)是该段从外存被加载到内存中的地址,所谓的装载就是把程序从存储的外存搬到内存里的过程,搬到内存的位置就是LMA。

  图中的"VMA" (虚拟内存地址 link address)是该段在外存中的地址,也就是实际地址。

  大多数情况下是一样的,不一样的情况出现在嵌入式系统中,详细了解可看https://blog.csdn.net/suz_cheney/article/details/24586745。

  boot loader利用ELF头文件信息决定如何加载段信息,头文件信息主要包括哪部分ELF文件要拷贝以及要拷贝到内存的哪里。

  重看main.c文件,主要工作就是判断是否是一个有效的ELF文件,如果是,循环读取头文件信息,加载程序的段信息。

 

Exercise 5

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!

  修改程序让链接地址不正确。原来BIOS设置加载boot loader从0x7C00的位置开始,先运行boot.S转变为32-bit保护模式,然后执行ljmp    $PROT_MODE_CSEG, $protcseg指令跳到main.C读取第一块boot扇区,如果设置为从0x7C10的位置开始,跳转指令无法跳转到正确的位置,出现异常。

$(OBJDIR)/boot/boot: $(BOOT_OBJS)
	@echo + ld boot/boot
	$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C10 -o [email protected] $^
	$(V)$(OBJDUMP) -S [email protected] >[email protected]
	$(V)$(OBJCOPY) -S -O binary -j .text [email protected] $@
	$(V)perl boot/sign.pl $(OBJDIR)/boot/boot

  然后重新编译脚本,仍然把断点设在0x7C00,执行到以下语句出现错误:

[   0:7c2d] => 0x7c2d: ljmp   $0x8,$0x7c42

0x00007c2d in ?? ()

(gdb) si

[   0:7c2d] => 0x7c2d: ljmp   $0x8,$0x7c42

0x00007c2d in ?? ()

(gdb) si

[   0:7c2d] => 0x7c2d: ljmp   $0x8,$0x7c42

0x00007c2d in ?? ()

  这里涉及到了跳转,由于加载地址在0x7c10,链接计算出来的地址也进行了偏移,而内存中的地址由于是从0x7c00开始的,所以实际没有偏移,结果跳转到了错误的地址。

Exercise 6

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

  进行两次检查检查BIOS进入boot loader(0x7c00)和boot loader进入kernel(0x10000c)时0x00100000位置上八个words有什么不同,也即kernel没加载进内存与kernel加载进内存以后0x00100000地址开始内容的对比,直接上结果图:

(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/8w 0x00100000
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/8w 0x00100000
0x100000:	0x1badb002	0x00000000	0xe4524ffe	0x7205c766
0x100010:	0x34000004	0x7000b812	0x220f0011	0xc0200fd8
(gdb) 

  原来是空的,后来有了内容,产生差异的原因是kernel内核的加载。

Part 3: The Kernel


  part3 讲进入内核执行,主要做了三件事

  1. 开启分页模式,将虚拟地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)

  2. 提供输出格式化字符串到控制台的能力

  3. 函数的调用过程

知识点:Using virtual memory to work around position dependence

When you inspected the boot loader's link and load addresses above, they matched perfectly, but there was a (rather large) disparity between the kernel's link address (as printed by objdump) and its load address. Go back and check both and make sure you can see what we're talking about. (Linking the kernel is more complicated than the boot loader, so the link and load addresses are at the top of kern/kernel.ld.)

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. The reason for this arrangement will become clearer in the next lab.

  我们发现kernel的链接地址和载入地址不同(0x10000c和0xf0100000),操作系统内核喜欢被链接以及运行在高位虚拟地址上,例如0xf0100000,为的是留出低位虚拟地址给用户程序使用,这样做的原因下个lab说明。

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). This way, although the kernel's virtual address is high enough to leave plenty of address space for user processes, it will be loaded in physical memory at the 1MB point in the PC's RAM, just above the BIOS ROM. This approach requires that the PC have at least a few megabytes of physical memory (so that physical address 0x00100000 works), but this is likely to be true of any PC built after about 1990.

In fact, in the next lab, we will map the entire bottom 256MB of the PC's physical address space, from physical addresses 0x00000000 through 0x0fffffff, to virtual addresses 0xf0000000 through 0xffffffff respectively. You should now see why JOS can only use the first 256MB of physical memory.

 

For now, we'll just map the first 4MB of physical memory, which will be enough to get us up and running. We do this using the hand-written, statically-initialized page directory and page table in kern/entrypgdir.c. For now, you don't have to understand the details of how this works, just the effect that it accomplishes. Up until kern/entry.S sets the CR0_PG flag, memory references are treated as physical addresses (strictly speaking, they're linear addresses, but boot/boot.S set up an identity mapping from linear addresses to physical addresses and we're never going to change that). Once CR0_PG is set, 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. Any virtual address that is not in one of these two ranges will cause a hardware exception which, since we haven't set up interrupt handling yet, will cause QEMU to dump the machine state and exit (or endlessly reboot if you aren't using the 6.828-patched version of QEMU).

  很多机器没有0xf0100000那么大的实际物理内存空间,我们需要使用内存管理硬件把0xf0100000的虚拟地址映射到 0x00100000这样的实际内存地址。这样一来,尽管内核的虚拟地址已经留出了足够的地址空间给用户使用,它被载入内存时会被放在BIOS ROM的上方(RAM 1MB的地方)。

  实际上在下个实验,我们要把整个256MB物理地址空间,从0x00000000 ~ 0x0fffffff 一一映射到虚拟地址0xf0000000 ~0xffffffff,这就是为什么JOS只使用前256MB的物理地址。现在,我们需要映射前4MB的物理地址。

Exercise 7

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.

  在执行movl %eax, %cr0指令后观察0x00100000 and at 0xf0100000位置处的内容。之前,0xf0100000内存地址处的数据为空,而在执行指令之后,0xf0100000地址被映射到了0x100000地址,可以看出,它们的数据是相同的。

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

  之后注释掉指令movl %eax, %cr0,执行到这个位置报错,报错信息如下:

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

  我们可以看看为什么报错,现在注释指令是干嘛的:

	# We haven't set up virtual memory yet, so we're running from
	# the physical address the boot loader loaded the kernel at: 1MB
	# (plus a few bytes).  However, the C code is linked to run at
	# KERNBASE+1MB.  Hence, we set up a trivial page directory that
	# translates virtual addresses [KERNBASE, KERNBASE+4MB) to
	# physical addresses [0, 4MB).  This 4MB region will be
	# sufficient until we set up our real page table in mem_init
	# in lab 2.

	# Load the physical address of entry_pgdir into cr3.  entry_pgdir
	# is defined in entrypgdir.c.
	movl	$(RELOC(entry_pgdir)), %eax
	movl	%eax, %cr3
	# Turn on paging.
	movl	%cr0, %eax
	orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
	movl	%eax, %cr0

  kernel开启分页模式,将虚拟地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB,代码是要要开启分页机制将虚拟地址映射到物理地址,如果没开启的话就越界访问报错。

知识点:Formatted Printing to the Console

Most people take functions like printf() for granted, sometimes even thinking of them as "primitives" of the C language. But in an OS kernel, we have to implement all I/O ourselves.

Read through kern/printf.c, lib/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.

  平常使用printf的时候我们都觉得很简单,但是在OS内核中,我们需要自己实现I/0。阅读kern/printf.c, lib/printfmt.c, and kern/console.c,搞清楚他们之间的联系。

Exercise 8

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.

  写点代码,写出来的代码要能够打印用"%o"打印八进制数,并回答一些问题。

  照猫画虎在printfmt.c中按照%d写%o:

		// (unsigned) octal
		case 'o':
			// Replace this with your code.
			// putch('X', putdat);
			// putch('X', putdat);
			// putch('X', putdat);
			// break;
			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?

  答案1:console.c中暴露了cputchar函数;被printf.c中的putch函数调用。

 

  问题2:Explain the following from console.c

1      if (crt_pos >= CRT_SIZE) {
2              int i;
3              memcpy(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      }

  答案2

	// What is the purpose of this?
	// 判断当前光标是否到结尾
	if (crt_pos >= CRT_SIZE) {
		int i;
		// 把从第1~n行的内容复制到0~(n-1)行,第n行未变化
		memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
		// 第n行覆盖成空格
		for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
			crt_buf[i] = 0x0700 | ' ';
		// 将光标移动到行首
		crt_pos -= CRT_COLS;
	}

 

  问题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);

  问题3.1:In the call to cprintf(), to what does fmt point? To what does ap point?

  答案3.1

  在kern/monitor.c()文件中添加这段代码:

57    int
58    mon_backtrace(int argc, char **argv, struct Trapframe *tf)
59    {
60    	// Your code here.
61	    int x = 1, y = 3, z = 4;
62	    cprintf("x %d, y %x, z %d\n", x, y, z);
63	    return 0;
64    }

  设置断点运行(b kern/monitor.c:62),查看结果:  

=> 0xf010077d :	call   0xf0100906 
0xf010077d	62		cprintf("x %d, y %x, z %d\n", x, y, z);
(gdb) si
=> 0xf0100906 :	push   %ebp
cprintf (fmt=0xf0101abe "x %d, y %x, z %d\n") at kern/printf.c:27
27	{
(gdb) si
=> 0xf0100907 :	mov    %esp,%ebp
0xf0100907	27	{
(gdb) si
=> 0xf0100909 :	sub    $0x18,%esp
0xf0100909	27	{
(gdb) si
=> 0xf010090c :	lea    0xc(%ebp),%eax
31		va_start(ap, fmt);
(gdb) si
=> 0xf010090f :	mov    %eax,0x4(%esp)
32		cnt = vcprintf(fmt, ap);
(gdb) si
=> 0xf0100913 :	mov    0x8(%ebp),%eax
0xf0100913	32		cnt = vcprintf(fmt, ap);
(gdb) si
=> 0xf0100916 :	mov    %eax,(%esp)
0xf0100916	32		cnt = vcprintf(fmt, ap);
(gdb) si
=> 0xf0100919 :	call   0xf01008d3 
0xf0100919	32		cnt = vcprintf(fmt, ap);
(gdb) si
=> 0xf01008d3 :	push   %ebp
vcprintf (fmt=0xf0101abe "x %d, y %x, z %d\n", ap=0xf0116f04 "\001") at kern/printf.c:18
18	{
(gdb) si
=> 0xf01008d4 :	mov    %esp,%ebp
0xf01008d4	18	{
(gdb) si
=> 0xf01008d6 :	sub    $0x28,%esp
0xf01008d6	18	{
(gdb) si
=> 0xf01008d9 :	movl   $0x0,-0xc(%ebp)
19		int cnt = 0;
(gdb) si
=> 0xf01008e0 :	mov    0xc(%ebp),%eax
21		vprintfmt((void*)putch, &cnt, fmt, ap);
(gdb) si
=> 0xf01008e3 :	mov    %eax,0xc(%esp)
0xf01008e3	21		vprintfmt((void*)putch, &cnt, fmt, ap);
(gdb) si
=> 0xf01008e7 :	mov    0x8(%ebp),%eax
0xf01008e7	21		vprintfmt((void*)putch, &cnt, fmt, ap);
(gdb) si

  上图中可以看到:

(gdb) si
=> 0xf01008d3 :    push   %ebp
vcprintf (fmt=0xf0101abe "x %d, y %x, z %d\n", ap=0xf0116f04 "\001") at kern/printf.c:18

  在cprintf()的调用中,fmt指针指向字符串 "x %d, y %x, z %d\n", ap指向va_list类型变量的地址。

  问题3.2: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.

  答案3.2:各函数调用的逻辑是 cprintf(printf.c) ===> vcprintf(printf.c) ===>  vprintfmt(printfmt.c) ===>  putch(printf.c) ===>  cputchar(console.c) ===> cons_putc(console.c)===> serial_putc;lpt_putc;cga_putc

cons_putc的参数依次是每个要输出的字符,比如打出第一个字符'x'(120)的过程:

(gdb) si

=> 0xf010062a : leave  

cputchar (c=120) at kern/console.c:462

462 }

 打出第二个字符'  '(32)的过程:

(gdb) si
=> 0xf010061c :	push   %ebp
cputchar (c=32) at kern/console.c:460
460	{

  函数type va_arg ( va_list ap, type );的作用是解析参数,第一个参数是变量列表ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置。

 

  Run the following code.

    unsigned int i = 0x00646c72;
    cprintf("H%x Wo%s", 57616, &i);

  问题4: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?

6828 decimal is 15254 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
He110 Worldleaving 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!

  答案4:  打印结果是He110 World,先把57616转换成16进制(0xe110),输出e110;然后把i按照字符串输出,每次读取一个字符,在ASCII码中,0x00,0x64,0x6c,0x72分别代表字符' \0','d','l','r' ,小端模式存储的数据按照字符串的读取方法是倒序的,读取直到'\0'结束。(大端模式存储的数据读取方法是正序的)

可以看出,0x00646c72 存储在内存中从低位到高位应该是:0x72,0x6c,0x64,0x00。于是按 ASCII 解码得到 'rld'。大端需要改为0x726c6400,不需要改变57616。

  

  问题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);

  打印结果如下:

6828 decimal is 15254 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
x=3 y=-267292872leaving test_backtrace 0

  答案5:第二个参数没有被指定,也就是栈上相应位置没有值,但指针仍然会移动到相应参数位置读取对应的值,读到什么算什么。va_arg取当前栈地址,没法确定有多少参数个数以及栈指针合法性,只会一直读直到结束。

 

  问题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?

  答案6:栈内是从高地址往低地址的方向增长,所以想要从左到右的顺序读取变量,那么编译器需要从右往左把参数压入栈中。


#ifndef _STDARG_H
#define _STDARG_H
 
typedef char *va_list;
 
/* Amount of space required in an argument list for an arg of type TYPE.
   TYPE may alternatively be an expression whose type is used.  */
 
#define __va_rounded_size(TYPE)  \
  (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
 
#ifndef __sparc__
#define va_start(AP, LASTARG) 						\
 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#else
#define va_start(AP, LASTARG) 						\
 (__builtin_saveregs (),						\
  AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#endif
 
void va_end (va_list);		/* Defined in gnulib */
#define va_end(AP)
 
#define va_arg(AP, TYPE)						\
 (AP += __va_rounded_size (TYPE),					\
  *((TYPE *) (AP - __va_rounded_size (TYPE))))
 
#endif /* _STDARG_H */

  从va_arg的定义中可以看出,每次是以增加地址读取出下一个变量的位置。 如果编译器更改了压栈的顺序,那么为了仍然能正确取出所有的参数, 那么需要改变va_start找到高位置,然后va_arg每次读取需要减少地址读取出下一个变量的位置。
 

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.

  要修改颜色要改变cga_putc(console.c)函数。c变量从后往前的0~7bit控制ASCII码,8~15bit控制颜色信息,其中8~11是前景色,12~15是背景色。

(二)6.828 Operating System lab1: Assembly, Tools, and Bootstrapping_第3张图片

  修改代码如下:

	// if no attribute given, then use black on white
	if (!(c & ~0xFF))	
		c |= 0x4200;   // 没有颜色信息就用对应设置,前景绿 背景红 不闪烁 的颜色码为0100 0010 = 4 2

  结果:

(二)6.828 Operating System lab1: Assembly, Tools, and Bootstrapping_第4张图片

 

Exercise 9

In the final exercise of this lab, we will explore in more detail the way the C language uses the stack on the x86, and in the process write a useful new kernel monitor function that prints a backtrace of the stack: a list of the saved Instruction Pointer (IP) values from the nested call instructions that led to the current point of execution.

Exercise 9. Determine where the kernel initializes its stack, and exactly where in memory its stack is located. How does the kernel reserve space for its stack? And at which "end" of this reserved area is the stack pointer initialized to point to?

  在entry.S中找到这段代码,这段代码前面是建立分页机制,接下来就该初始化栈

	# 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

	# now to C code
	call	i386_init

  寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。调用dbg开始继续观察

(gdb)  b kern/entry.S : 74
Breakpoint 1 at 0xf010002f: file kern/entry.S, line 74.
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0xf010002f :	mov    $0x0,%ebp

Breakpoint 1, relocated () at kern/entry.S:74
74		movl	$0x0,%ebp			# nuke frame pointer
(gdb) si
=> 0xf0100034 :	mov    $0xf0117000,%esp
relocated () at kern/entry.S:77
77		movl	$(bootstacktop),%esp

  初始时esp指向栈的底部,由于栈是从内存高地址往低地址增长,图中看出是从esp:0xf0117000的地方开始减小的。

  从entry.S开始观察栈大小,entry.S里提到

bootstack:
	.space		KSTKSIZE

  在inc/memlayout.h中找到

#define KSTACKTOP	KERNBASE
#define KSTKSIZE	(8*PGSIZE)   		// size of a kernel stack

  再找到inc/mmu.h中找到

#define PGSIZE		4096		// bytes mapped by a page

  所以栈的大小是4096bytes * 8 = 32KB。

  ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(下一个压入栈的活动记录的顶部),是栈指针。EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部(当前活动记录的底部),是帧指针。栈是从高地址向低地址延伸的。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。

知识点:stack pointer

The x86 stack pointer (esp register) points to the lowest location on the stack that is currently in use. Everything below that location in the region reserved for the stack is free. Pushing a value onto the stack involves decreasing the stack pointer and then writing the value to the place the stack pointer points to. Popping a value from the stack involves reading the value the stack pointer points to and then increasing the stack pointer. In 32-bit mode, the stack can only hold 32-bit values, and esp is always divisible by four. Various x86 instructions, such as call, are "hard-wired" to use the stack pointer register.

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. If all the functions in a program obey this convention, then at any given point during the program's execution, it is possible to trace back through the stack by following the chain of saved ebp pointers and determining exactly what nested sequence of function calls caused this particular point in the program to be reached. This capability can be particularly useful, for example, when a particular function causes an assert failure or panic because bad arguments were passed to it, but you aren't sure who passed the bad arguments. A stack backtrace lets you find the offending function.

  X86栈指针(esp寄存器)指向当前使用的最小地址,esp指针地址下的所有栈地址空间都是可用的。从栈中弹出一个值涉及到读取栈指针指向的值以及增加栈指针。ebp寄存器,指向当前栈的底部(最大地址),当涉及到链式函数调用的时候,将每一个函数的ebp都压入栈,然后设置当前esp在当前ebp的基础上减少,这样做的好处是在程序执行的过程中,可以追溯当前函数调用的顺序,有的时候很有用,比如某个函数因为错误参数造成错误,可以用stack backtrace找到传递错误参数的函数。因为ebp给每个调用都留下了记录,出错就找上一个?

Exercise 10

Exercise 10. To become familiar with the C calling conventions on the x86, find the address of the test_backtrace function in obj/kern/kernel.asm, set a breakpoint there, and examine what happens each time it gets called after the kernel starts. How many 32-bit words does each recursive nesting level of test_backtrace push on the stack, and what are those words?

Note that, for this exercise to work properly, you should be using the patched version of QEMU available on the tools page or on Athena. Otherwise, you'll have to manually translate all breakpoint and memory addresses to linear addresses.

  每次函数调用会分为以下几步

  1. 参数入栈:参数从右向左被压入栈。
  2. 返回地址入栈: eip中的内容 入栈,在 call 指令执行
  3. 将上一个函数的 ebp 入栈,保存该ebp是为了函数返回后的现场恢复
  4. 将 ebx 入栈,保护寄存器状态
  5. 在栈上开辟一个空间存储局部变量

知识点:C语言细节

Here are a few specific points you read about in K&R Chapter 5 that are worth remembering for the following exercise and for future labs.

  • If int *p = (int*)100, then (int)p + 1 and (int)(p + 1) are different numbers: the first is 101 but the second is 104. When adding an integer to a pointer, as in the second case, the integer is implicitly multiplied by the size of the object the pointer points to.
  • p[i] is defined to be the same as *(p+i), referring to the i'th object in the memory pointed to by p. The above rule for addition helps this definition work when the objects are larger than one byte.
  • &p[i] is the same as (p+i), yielding the address of the i'th object in the memory pointed to by p.

Although most C programs never need to cast between pointers and integers, operating systems frequently do. Whenever you see an addition involving a memory address, ask yourself whether it is an integer addition or pointer addition and make sure the value being added is appropriately multiplied or not.

  Exercise 11

Exercise 11. 

The backtrace function should display a listing of function call frames in the following format:

Stack backtrace:
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
  ...

Implement the backtrace function as specified above. Use the same format as in the example, since otherwise the grading script will be confused. When you think you have it working right, run make grade to see if its output conforms to what our grading script expects, and fix it if it doesn't. After you have handed in your Lab 1 code, you are welcome to change the output format of the backtrace function any way you like.

If you use read_ebp(), note that GCC may generate "optimized" code that calls read_ebp() before mon_backtrace()'s function prologue, which results in an incomplete stack trace (the stack frame of the most recent function call is missing). While we have tried to disable optimizations that cause this reordering, you may want to examine the assembly of mon_backtrace() and make sure the call to read_ebp() is happening after the function prologue.

  改写kern/monitor.c里的mon_backtrace函数:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{

	// Your code here.
	cprintf("Stack backtrace\n")
 	// 利用read_ebp() 函数获取当前ebp值
	uint32_t *ebp = (uint32_t *) read_ebp();
	// 利用 ebp 的初始值0判断是否停止
	while(ebp){
		// 利用数组指针运算来获取 eip 以及 args
		cprintf("ebp %08x eip %08x args",ebp,ebp[1]);
		for(int j=2;j<7;++j){
			cprintf(" %08x",ebp[j]);
		}
		cprintf("\n")
		ebp = (uint32_t *)(*ebp)
	}
	return 0;
}
Stack backtrace
ebp f0117f18 eip f0100087 args 00000000 00000000 00000000 00000000 f01008fc
ebp f0117f38 eip f0100069 args 00000000 00000001 f0117f78 00000000 f01008fc
ebp f0117f58 eip f0100069 args 00000001 00000002 f0117f98 00000000 f01008fc
ebp f0117f78 eip f0100069 args 00000002 00000003 f0117fb8 00000000 f01008fc
ebp f0117f98 eip f0100069 args 00000003 00000004 00000000 00000000 00000000
ebp f0117fb8 eip f0100069 args 00000004 00000005 00000000 00010074 00010074
ebp f0117fd8 eip f01000ea args 00000005 00001aac 00000644 00000000 00000000
ebp f0117ff8 eip f010003e args 00119021 00000000 00000000 00000000 00000000

 

问题1:The return instruction pointer typically points to the instruction after the call instruction (why?).

  回答1:为了返回调用函数后继续执行。

  问题2:Why can't the backtrace code detect how many arguments there actually are? How could this limitation be fixed?

  回答2:因为判断有几个参数这种事情是编译器干的,编译器通过函数原型来判断有几个参数。函数内部是没有方法直接获取到有几个参数传过来了这种事情的。修复的方法可以是把参数的数字当作第一个参数传递过来。

 

Exercise 12

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

In debuginfo_eip, where do __STAB_* come from? This question has a long answer; to help you to discover the answer, here are some things you might want to do:

  • look in the file kern/kernel.ld for __STAB_*
  • run i386-jos-elf-objdump -h obj/kern/kernel
  • run i386-jos-elf-objdump -G obj/kern/kernel
  • run i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
  • see if the bootloader loads the symbol table in memory as part of loading the kernel binary

Complete the implementation of debuginfo_eip by inserting the call to stab_binsearch to find the line number for an address.

Add a backtrace command to the kernel monitor, and extend your implementation of mon_backtrace to call debuginfo_eip and print a line for each stack frame of the form:

K> backtrace
Stack backtrace:
  ebp f010ff78  eip f01008ae  args 00000001 f010ff8c 00000000 f0110580 00000000
         kern/monitor.c:143: monitor+106
  ebp f010ffd8  eip f0100193  args 00000000 00001aac 00000660 00000000 00000000
         kern/init.c:49: i386_init+59
  ebp f010fff8  eip f010003d  args 00000000 00000000 0000ffff 10cf9a00 0000ffff
         kern/entry.S:70: +0
K> 

Each line gives the file name and line within that file of the stack frame's eip, followed by the name of the function and the offset of the eip from the first instruction of the function (e.g., monitor+106 means the return eip is 106 bytes past the beginning of monitor).

Be sure to print the file and function names on a separate line, to avoid confusing the grading script.

Tip: printf format strings provide an easy, albeit obscure, way to print non-null-terminated strings like those in STABS tables. printf("%.*s", length, string) prints at most length characters of string. Take a look at the printf man page to find out why this works.

You may find that some functions are missing from the backtrace. For example, you will probably see a call to monitor() but not to runcmd(). This is because the compiler in-lines some function calls. Other optimizations may cause you to see unexpected line numbers. If you get rid of the -O2 from GNUMakefile, the backtraces may make more sense (but your kernel will run more slowly).

  首先,Complete the implementation of debuginfo_eip by inserting the call to stab_binsearch to find the line number for an address,补充kern/kdebug.c中的debuginfo_eip函数中添加stab_binsearch函数寻找行数。

  

	// 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  to find
	//	which one.
	// Your code here.
	stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
	if (lline <= rline) {
		// N_SLINE在 inc/x86.h中定义,对应的行号存储在 n_desc 中,通过stabs[lline].n_desc获取行号
        info->eip_line = stabs[lline].n_desc;
    } else {
        return -1;
    }

  接着,在monitor.c中添加Command指令backtrace:

static struct Command commands[] = {
	{ "help", "Display this list of commands", mon_help },
	{ "kerninfo", "Display information about the kernel", mon_kerninfo },
	{ "backtrace", "Display backtrace info", mon_backtrace },
};

  最后,修改monitor.c中的mon_backtrace添加输出的信息:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{

	// Your code here.
	cprintf("Stack backtrace:\n");
 	// 利用read_ebp() 函数获取当前ebp值
	uint32_t *ebp = (uint32_t *) read_ebp();
	struct Eipdebuginfo info;
	// 利用 ebp 的初始值0判断是否停止
	while(ebp){
		// 利用数组指针运算来获取 eip 以及 args
		cprintf("ebp %08x eip %08x args",ebp,ebp[1]);
		for(int j=2;j<7;++j){
			cprintf(" %08x",ebp[j]);
		}
		cprintf("\n");
		// 调用debuginfo_eip函数
		if (debuginfo_eip(ebp[1], &info) == 0) {
            cprintf("\n     %s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, ebp[1] - info.eip_fn_addr);
        }
		ebp = (uint32_t *)(*ebp);
	}
	return 0;
}

  结果:

Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K> backtrace
Stack backtrace:
ebp f0117f68 eip f0100901 args 00000001 f0117f80 00000000 f0117fc8 f011a540

     kern/monitor.c:142: monitor+243
ebp f0117fd8 eip f01000f6 args 00000000 00001aac 00000644 00000000 00000000

     kern/init.c:43: i386_init+89
ebp f0117ff8 eip f010003e args 00119021 00000000 00000000 00000000 00000000

     kern/entry.S:83: +0
K> 

 

你可能感兴趣的:(MIT,6.828,Operating,System)