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 },
};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM