MIT6.828 Fall2018 笔记 - Lab 1: Booting a PC

参考文章:

Lab 1: Booting a PC

Part 1: PC Bootstrap

Simulating the x86

下载 JOS 源码,然后编译

# 让 git 忽略 ssl 认证,否则 git clone 可能会失败
export GIT_SSL_NO_VERIFY=1
# 建议使用 proxychains 代理,加快 git clone 速度
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
cd lab
make

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

make qemu

输出:

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

使用Ctrl-a x可以退出qemu

The PC's Physical Address Space

PC的物理地址空间布局:

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

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

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

The ROM BIOS

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

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

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

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

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

Part 2: The Boot Loader

如果阅读了xv6 book的附录B,并且看了对应的xv6源码,会更容易理解JOS的boot loader

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

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

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

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

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

Exercise 3

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

关于 Real Mode 和 Protected Mode

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

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

关于 A20 line 和 PS/2 Controller

xv6 book 的附录B中:

A virtual segment:offset can yield a 21-bit physical address, but the Intel 8088 could only address 20 bits of memory, so it discarded the top bit: 0xffff0+0xffff = 0x10ffef, but virtual address 0xffff:0xffff on the 8088 referred to physical address 0x0ffef. Some early software relied on the hardware ignoring the 21st address bit, so when Intel introduced processors with more than 20 bits of physical address, IBM provided a compatibility hack that is a requirement for PC-compatible hardware. If the second bit of the keyboard controller’s output port is low, the 21st physical address bit is always cleared; if high, the 21st bit acts normally. The boot loader must enable the 21st address bit using I/O to the keyboard controller on ports 0x64 and 0x60 (91209136).

boot/boot.S

加上注释的部分 boot.S

#include <inc/mmu.h>

# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

# .set 相当于 #define,用于设置常量
.set PROT_MODE_CSEG, 0x8         # kernel code segment selector
.set PROT_MODE_DSEG, 0x10        # kernel data segment selector
.set CR0_PE_ON,      0x1         # protected mode enable flag

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

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

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

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

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

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

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

  # 跳转到下一个指令,但是让 cs 引用 gdt 中的代码描述符条目
  # 这个描述符描述了一个 32-bit 代码段,所以处理器转换成 32-bit mode
  ljmp    $PROT_MODE_CSEG, $protcseg

  .code32                     # Assemble for 32-bit mode
protcseg:
  # 设置保护模式的段寄存器
  movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: Stack Segment

  # 设置esp,让0x7c00作为boot loader的栈顶
  movl    $start, %esp
  # 调用main.c中的bootmain函数
  call bootmain

  # 如果bootmain返回了(本不应该返回),就死循环
spin:
  jmp spin

# Bootstrap GDT 引导全局描述符表
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULL				# null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)	# code seg
  SEG(STA_W, 0x0, 0xffffffff)	        # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

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

  1. 启用A20 line,第21条地址线
  2. 加载GDT,并且从实模式切换到保护模式
  3. 进入mian.c的bootmain函数

别人对 Lab 1 Exercise 3 的分析

TODO:GDT的部分暂时先不管

answer questions

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

(gdb) si
[   0:7c2d] => 0x7c2d:	ljmp   $0xb866,$0x87c32
0x00007c2d in ?? ()

不知为何这里是$0xb866,$0x87c32,但实际上还是跳转到了0x7c32。实际上导致切换到保护模式的是将 cr0 寄存器置1。CR0 - Wikipedia

(gdb) si
[   0:7c23] => 0x7c23:	mov    %cr0,%eax
0x00007c23 in ?? ()
(gdb) si
[   0:7c26] => 0x7c26:	or     $0x1,%ax
0x00007c26 in ?? ()
(gdb) si
[   0:7c2a] => 0x7c2a:	mov    %eax,%cr0
0x00007c2a in ?? ()
  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

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

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

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

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

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

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

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

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

0x0010000c

  • How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

通过ELFHDR->e_phnum知晓sector数量,它从ELF header找到这个信息(关于ELF header和boot/main.c请看下文)

Loading the Kernel

inc/elf.h

ELF headers 的定义在 inc/elf.h,部分注释:

// ELF header
// 详见 https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-43405/index.html
struct Elf {
	uint32_t e_magic;		// must equal ELF_MAGIC
	uint8_t e_elf[12];  	// 机器无关数据,通过它们可以解码和解释文件的内容
	uint16_t e_type;		// 标识目标文件类型
	uint16_t e_machine;		// 指定单个文件所需的机器架构
	uint32_t e_version;		// 标识目标文件版本
	uint32_t e_entry;		// 程序入口,虚拟地址
	uint32_t e_phoff; 		// program header table的文件偏移量。指与文件起始位置的offset
	uint32_t e_shoff; 		// section header table的文件偏移量
	uint32_t e_flags;		// 与文件关联的特定于处理器的flags
	uint16_t e_ehsize; 		// ELF头的大小,以字节为单位
	uint16_t e_phentsize;	// program header table一项的大小,每一项大小都相同
	uint16_t e_phnum; 		// program header条目数
	uint16_t e_shentsize;	// section header table一项的大小,每一项大小都相同
	uint16_t e_shnum;		// section header条目数
	uint16_t e_shstrndx;	// 与section name string table相关的项的section header table索引
};

// program header
// 每个program header描述一个segment
// 一个segment由几个section组成,为能被映射进内存映像的最小独立单元
// 详见 https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-83432/index.html
struct Proghdr {
	uint32_t p_type;		// segment类型
	uint32_t p_offset; 		// segment相对于ELF文件开头的偏移
	uint32_t p_va;			// segment的第一个字节在内存中的虚拟地址
	uint32_t p_pa; 			// 物理地址
	uint32_t p_filesz;		// segment的文件映像中的字节数
	uint32_t p_memsz; 		// segment的内存映像中的字节数
	uint32_t p_flags;  		// 与segment相关的flags,读写执行权限
	uint32_t p_align;		// 在内存和文件中的对齐
};

// section header
// section header table可以帮助你定位该文件所有的sections
// section为ELF文件中能被处理的最小不可分割单元,比如.text .rodat .data
// 详见 https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-94076/index.html
struct Secthdr {
	uint32_t sh_name;		// The name of the section
	uint32_t sh_type;		// 对section的内容和语义进行分类
	uint32_t sh_flags;		// flags
	uint32_t sh_addr;		// section的地址
	uint32_t sh_offset;		// 从文件起始处到section第一个字节的偏移量
	uint32_t sh_size;		// section的大小
	uint32_t sh_link;		// section header table index
	uint32_t sh_info;		// Extra information
	uint32_t sh_addralign;	// 地址对齐
	uint32_t sh_entsize;	// 定长的条目表(如符号表)的大小
};

我们所要关心的program sections是:

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

objdump -f obj/kern/kernel可以查看ELF header的概括信息

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

obj/kern/kernel:     文件格式 elf32-i386
体系结构:i386,标志 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
起始地址 0x0010000c

objdump -h obj/kern/kernel可以查看ELF format二进制文件的section headers信息:

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

VMA(virtual memory address) 指 link address,表明该段应该在哪个内存地址执行,LMA(load memory address) 指 load address,表明该段应该被加载到哪个内存地址。一般而言两者相同。File off指该section与文件起始处的偏移量。

有些section存放debugging information

objdump -x obj/kern/kernel可以查看所有headers和符号表信息:

objdump -x obj/kern/kernel

输出内容分别是ELF header、program header、section header、SYMBOL TABLE。

可以看出这些输出都对应于inc/elf.h中的那些结构体的定义。更多命令选项请使用objdump --help查看。

boot/main.c

boot/main.c部分注释:

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

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

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

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

	// 检查是否为 ELF 格式的二进制文件
	if (ELFHDR->e_magic != ELF_MAGIC)
		goto bad;

	// program header table地址的偏移量
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
	// program header table的结束位置偏移量
	eph = ph + ELFHDR->e_phnum;
	// 根据每个program header将每一个segment加载进内存
	for (; ph < eph; ph++)
		// 注意ph++,ph是指针,所以是指向下一个program header
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

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

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

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

	end_pa = pa + count;

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

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

	// If this is too slow, we could read lots of sectors at a time.
	// We'd write more to memory than asked, but it doesn't matter --
	// we load in increasing order.
	while (pa < end_pa) {
		// Since we haven't enabled paging yet and we're using
		// an identity segment mapping (see boot.S), we can
		// use physical addresses directly.  This won't be the
		// case once JOS enables the MMU.
		readsect((uint8_t*) pa, offset);
		pa += SECTSIZE;
		offset++;
	}
}

总结一下boot/main.c干了什么:

  1. 从硬盘中将kernel的elf header读入内存
  2. 根据elf header和program header table提供的信息,将kernel的每个segment读入内存
  3. 然后进入kernel。

Exercise 5

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

boot/Makefrag里的-Ttext 0x7C00改为-Ttext 0x8C00,使用objdump -h obj/boot/boot.out可以看到VMA和LMA都是0x8c00然后make clean,再调试。出错的指令:

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

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

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

此外,obj/kern/kernel的VMA与LMA并不相同,kernel告诉boot loader在1M(LMA)处将kernel载入内存,但是在一个高地址(VMA)执行,VMA会被映射到LMA

Exercise 6

在0x7c00和0x7d30处打断点,根据obj/boot/boot.asm可知,0x7d30是读取完ELF header后的指令。

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

Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x/4x 0x10000
0x10000:        0x00000000      0x00000000      0x00000000      0x00000000
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x7d30:      add    $0x10,%esp

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

内存地址0x10000处的数据发生了变化,说明kernel的ELF header确实已经被加载进了内存。

Part 3: The Kernel

如果阅读了xv6 book第1章的Code: the first address space部分,并且看了对应的xv6源码,会更容易理解JOS的kern/entry.S

链接器 ld 根据kern/kernel.ld这个linker script的配置对kernel进行链接。

操作系统内核通常被链接到非常高的虚拟地址(例如JOS的kernel被链接到0xf0100000)下运行,以便留下处理器虚拟地址空间的低地址部分供用户程序使用。 在下一个lab中,这种安排的原因将变得更加清晰。许多机器在地址范围无法达到0xf0100000,因此我们无法指望能够在那里存储内核。但是,我们可以使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码期望运行的链接地址)映射到物理地址0x00100000(引导加载程序将内核加载到物理内存中)。尽管它在0xf0100000处执行,但实际上在0x00100000的物理内存地址上。

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

kern/entry.S中,在设置CR0寄存器的PG标志为1前,内存引用被当作物理地址,一旦CR0寄存器的PG标志被设置为1后,分页机制开启,entry_pgdir会把虚拟地址翻译为物理地址。CR即Control Register。

Exercise 7

(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/4x 0x100000
0x100000:       0x1badb002      0x00000000      0xe4524ffe      0x7205c766
(gdb) x/4x 0xf0100000
0xf0100000 <_start-268435468>:  0x00000000      0x00000000      0x00000000      0x00000000
(gdb) si
=> 0x100028:    mov    $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/4x 0xf0100000
0xf0100000 <_start-268435468>:  0x1badb002      0x00000000      0xe4524ffe      0x7205c766
(gdb) si
=> 0x10002d:    jmp    *%eax
0x0010002d in ?? ()
(gdb) si
=> 0xf010002f <relocated>:      mov    $0x0,%ebp
relocated () at kern/entry.S:74
74              movl    $0x0,%ebp                       # nuke frame pointer
(gdb)

执行mov %eax,%cr0后,0xf0100000的数据从0变成了与0x100000的数据一致,这说明,虚拟地址0xf0100000已经被映射到了0x100000。继续单步执行,执行完jmp *%eax后,程序开始在高地址0xf010002f执行(实际上在物理地址0x10002f)。如果我们注释掉movl %eax,%cr0,当访问高位地址时,会出现RAM or ROM 越界错误。

文件kern/entry.S

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

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

kern/entry.S

kern/entry.S的部分注释

# _start 是 ELF entry 点
# RELOC将虚拟地址转为物理地址
# 由于还没设置虚拟内存,所以我们的_start是物理地址
.globl		_start
_start = RELOC(entry)

.globl entry
entry:
	movw	$0x1234,0x472			# warm boot

	# 将 entry_pgdir 的物理地址给 cr3寄存器,entry_pgdir中定义了VA到PA的映射
	# cr3 寄存器使得处理器可以翻译线性地址为物理地址
	# 关于cr3:https://en.wikipedia.org/wiki/Control_register#CR3
	movl	$(RELOC(entry_pgdir)), %eax
	movl	%eax, %cr3
	# 保护模式、分页、写保护
	# 关于cr0:https://en.wikipedia.org/wiki/Control_register#CR0
	movl	%cr0, %eax
	orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
	movl	%eax, %cr0

	# 进入高地址
	mov	$relocated, %eax
	jmp	*%eax
relocated:

	movl	$0x0,%ebp			# nuke frame pointer

	movl	$(bootstacktop),%esp

	# 跳转到 c 代码
	call	i386_init

Formatted Printing to the Console

看一下kern/printf.c lib/printfmt.c kern/console.c这几个文件,了解它们间的关联即可。

Exercise 8

实现打印8进制数,在lib/printfmt.cvprintfmt函数中,改为:

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

answer 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,比如函数 cputchar

  2. Explain the following from console.c:

    // 如果缓冲区满了
    if (crt_pos >= CRT_SIZE) {
    	int i;
    	// 将第 2~80 行往上移,这样就空出了一行
    	memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    	// 将最后一行全部变成空的
    	for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    		crt_buf[i] = 0x0700 | ' ';
    	// 光标位置移到行首
    	crt_pos -= CRT_COLS;
    }
    
    • In the call to cprintf(), to what does fmt point? To what does ap point?
    • List (in order of execution) each call to cons_putc, va_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.

    kern/init.ci386_init函数的cprintf("6828 decimal is %o octal!\n", 6828);后面添加代码:

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

    查看文件obj/kern/kernel.asm,加点注释:

    	int x = 1, y = 3, z = 4;
    	cprintf("x %d, y %x, z %d\n", x, y, z);
    f01000b8:	6a 04                	push   $0x4 				# 从右到左将参数压入栈中
    f01000ba:	6a 03                	push   $0x3
    f01000bc:	6a 01                	push   $0x1
    f01000be:	8d 83 af 07 ff ff    	lea    -0xf851(%ebx),%eax 	# 取字符串的地址到 eax
    f01000c4:	50                   	push   %eax					# 将字符串地址压入栈中
    f01000c5:	e8 8c 09 00 00       	call   f0100a56 <cprintf>	# 调用 cprintf
    

    在函数vcprintf处打断点,开始调试,得fmt=0xf0101ab7,指向字符串。ap=0xf010ffe4,指向栈顶。

    (gdb) b vcprintf
    Breakpoint 1 at 0xf0100a1f: file kern/printf.c, line 18.
    (gdb) c
    Continuing.
    The target architecture is assumed to be i386
    => 0xf0100a1f <vcprintf>:       push   %ebp
    
    Breakpoint 1, vcprintf (fmt=0xf0101ab7 "x %d, y %x, z %d\n", ap=0xf010ffe4 "\001")
        at kern/printf.c:18
    18      {
    (gdb) x/s 0xf0101ab7
    0xf0101ab7:     "x %d, y %x, z %d\n"
    (gdb) x/4xw 0xf010ffe4
    0xf010ffe4:     0x00000001      0x00000003      0x00000004      0x00000000
    (gdb)
    
  3. Run the following code.

    和上面一个一样添加代码:

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

    然后make qemu,可以找到He110 World。这里要注意的就是小端序。

  4. 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);
    
  5. 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?

    调用cprintf时指定参数个数

The Stack

kern/entry.S中定义了栈区,KSTKSIZE是栈的大小,在inc/memlayout.h中被定义,为32KB。

.data
###################################################################
# boot stack
###################################################################
	.p2align	PGSHIFT		# force page alignment
	.globl		bootstack
bootstack:
	.space		KSTKSIZE	# 栈大小
	.globl		bootstacktop
bootstacktop:

函数调用时,先将ebp压入栈中,然后将当前esp的值给ebp。在程序执行期间的任何时候,都可以通过已保存的ebp指针链来回溯堆栈。当出现assertpanic错误时,堆栈回溯可以帮你找到出错的函数调用链。

Exercise 10

只记录了从test_backtrace(5)递归到test_backtrace(0)然后准备进入mon_backtrace(0, 0, 0);时:

(gdb) x/52x 0xf010ff20
0xf010ff20:     0x00000000      0x00000000      0x00000000      0xf010004a
0xf010ff30:     0xf0111308      0x00000001      0xf010ff58      0xf0100076
0xf010ff40:     0x00000000      0x00000001      0xf010ff78      0xf010004a
0xf010ff50:     0xf0111308      0x00000002      0xf010ff78      0xf0100076
0xf010ff60:     0x00000001      0x00000002      0xf010ff98      0xf010004a
0xf010ff70:     0xf0111308      0x00000003      0xf010ff98      0xf0100076
0xf010ff80:     0x00000002      0x00000003      0xf010ffb8      0xf010004a
0xf010ff90:     0xf0111308      0x00000004      0xf010ffb8      0xf0100076
0xf010ffa0:     0x00000003      0x00000004      0x00000000      0xf010004a
0xf010ffb0:     0xf0111308      0x00000005      0xf010ffd8      0xf0100076
0xf010ffc0:     0x00000004      0x00000005      0x00000000      0xf010004a
0xf010ffd0:     0xf0111308      0x00010094      0xf010fff8      0xf01000f4
0xf010ffe0:     0x00000005      0x00001aac      0x00000640      0x00000000
(gdb)

其中0xf010ffa0到0xf010ffe0:

... 省略
test_backtrace参数3     传给cprintf的参数4  不知道          __x86.get_pc_thunk.bx返回后应执行的指令
保存ebx                 保存esi             保存ebp          test_backtrace函数返回后应执行的指令
test_backtrace参数4     传给cprintf的参数5  不知道          __x86.get_pc_thunk.bx返回后应执行的指令
保存ebx                 保存esi             保存ebp          test_backtrace函数返回后应执行的指令
test_backtrace参数5

Exercise 11

kern/monitor.c中:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	// Your code here.
    uint32_t ebp, *ptr_ebp;
    ebp = read_ebp();
    while (ebp != 0) {
        ptr_ebp = (uint32_t*)ebp;
        cprintf("ebp %x eip %x args %08x %08x %08x %08x %08x\n", ebp, ptr_ebp[1], ptr_ebp[2], ptr_ebp[3], ptr_ebp[4], ptr_ebp[5], ptr_ebp[6]);
        ebp = *ptr_ebp;
    }

	return 0;
}

ebp是mon_backtrace函数当前使用的ebp,eip为函数return后要执行的指令的地址,args为进入mon_backtrace函数前push压入栈的参数。然后把GNUmakefile第204行的./grade-lab$(LAB) $(GRADEFLAGS)加个python,然后运行make grade检查是否写对了(我只有两个OK🙃)。

注:如果make grade不能成功运行,更改一下GNUmakefile的部分内容:

grade:
	@echo $(MAKE) clean
	@$(MAKE) clean || \
	  (echo "'make clean' failed.  HINT: Do you have another running instance of JOS?" && exit 1)
	python ./grade-lab$(LAB) $(GRADEFLAGS)

Exercise 12

先按照提示完成kern/kdebug.cdebuginfo_eip函数的实现,将代码放在// Your code here.处:

// Your code here.
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline) {
    info->eip_line = stabs[lline].n_desc;
} else {
    return -1;
}

更改mon_backtrace函数:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    uint32_t ebp, *ptr_ebp;
    struct Eipdebuginfo info;
    ebp = read_ebp();
	ptr_ebp = (uint32_t*)ebp;
    cprintf("Stack backtrace:\n");
    while (ebp != 0 && debuginfo_eip(ptr_ebp[1], &info) == 0) {
        cprintf(" ebp %x  eip %x  args %08x %08x %08x %08x %08x\n", ebp, ptr_ebp[1], ptr_ebp[2], ptr_ebp[3], ptr_ebp[4], ptr_ebp[5], ptr_ebp[6]);
        cprintf("     %s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, ptr_ebp[1] - info.eip_fn_addr);
		ebp = *ptr_ebp;
		ptr_ebp = (uint32_t*)ebp;
    }

	return 0;
}

kern/monitor.cstatic struct Command commands[]语句里添加命令。

static struct Command commands[] = {
	{ "help", "Display this list of commands", mon_help },
	{ "kerninfo", "Display information about the kernel", mon_kerninfo },
	{ "backtrace", "Display backtrace information", mon_backtrace },
};
posted @ 2020-03-05 00:36  hyuuko  阅读(1706)  评论(0编辑  收藏  举报