MIT 6.828 操作系統工程 2018 fall lab1 part3 內核 筆記 and 中文註釋源代碼閱讀

mit 6.828 lab 代碼和筆記,以及中文註釋源代碼已放置在github中:
https://github.com/yunwei37/xv6-labs

Part 3: The Kernel 內核

使用虛擬內存解決位置依賴性

內核的鏈接地址(由objdump打印)與加載地址之間存在(相當大的)差異;操作系統內核通常喜歡被鏈接並在很高的虛擬地址(例如0xf0100000)上運行,以便將處理器虛擬地址空間的下部留給用戶程序使用。

  • 鏈接地址 f0100000
  • 加載地址 00100000

許多機器在地址0xf0100000上沒有任何物理內存,因此我們不能指望能夠在其中存儲內核;將使用處理器的內存管理硬件將虛擬地址0xf0100000(內核代碼期望在其上運行的鏈接地址)映射到物理地址0x00100000(引導加載程序將內核加載到物理內存中)。

這樣,儘管內核的虛擬地址足夠高,可以爲用戶進程留出足夠的地址空間,但是它將被加載到PC RAM中1MB點的BIOS ROM上方的物理內存中。

在這個階段中,僅映射前4MB的物理內存;

映射:kern/entrypgdir.c 中手寫,靜態初始化的頁面目錄和頁面表。
直到kern / entry.S設置了CR0_PG標誌,內存引用才被視爲物理地址。

  • 將範圍從0xf0000000到0xf0400000的虛擬地址轉換爲物理地址0x00000000到0x00400000

  • 將虛擬地址0x00000000到0x00400000轉換爲物理地址0x00000000到0x00400000

  • kern/entrypgdir.c:

#include <inc/mmu.h>
#include <inc/memlayout.h>

pte_t entry_pgtable[NPTENTRIES];

// entry.S頁面目錄從虛擬地址KERNBASE開始映射前4MB的物理內存
// (也就是說,它映射虛擬地址
// 地址[KERNBASE,KERNBASE + 4MB)到物理地址[0,4MB)
// 我們選擇4MB,因爲這就是我們可以在一頁的空間中映射的表
// 這足以使我們完成啓動的早期階段。我們也映射
// 虛擬地址[0,4MB)到物理地址[0,4MB)這個
// 區域對於entry.S中的一些指令至關重要,然後我們
// 不再使用它。
//
// 頁面目錄(和頁面表)必須從頁面邊界開始,
// 因此是“ __aligned__”屬性。 另外,由於限制
// 與鏈接和靜態初始化程序有關, 我們在這裏使用“ x + PTE_P”
// 而不是更標準的“ x | PTE_P”。  其他地方
// 您應該使用“ |”組合標誌。
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
	// 將VA的[0,4MB)映射到PA的[0,4MB)
	[0]
		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
	// 將VA的[KERNBASE,KERNBASE + 4MB)映射到PA的[0,4MB)
	[KERNBASE>>PDXSHIFT]
		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

// 頁表的條目0映射到物理頁0,條目1映射到
// 物理頁面1,依此類推
__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {
	0x000000 | PTE_P | PTE_W,
	0x001000 | PTE_P | PTE_W,
	0x002000 | PTE_P | PTE_W,
	0x003000 | PTE_P | PTE_W,
	0x004000 | PTE_P | PTE_W,
	0x005000 | PTE_P | PTE_W,
  ................

  • kern/entry.S
/* See COPYRIGHT for copyright information. */

#include <inc/mmu.h>
#include <inc/memlayout.h>

# 邏輯右移
#define SRL(val, shamt)		(((val) >> (shamt)) & ~(-1 << (32 - (shamt))))


###################################################################
# 內核(此代碼)鏈接到地址〜(KERNBASE + 1 Meg),
# 但引導加載程序會將其加載到地址〜1 Meg。
#	
# RELOC(x)將符號x從其鏈接地址映射到其在
# 物理內存中的實際位置(其加載地址)。	 
###################################################################

#define	RELOC(x) ((x) - KERNBASE)

#define MULTIBOOT_HEADER_MAGIC (0x1BADB002)
#define MULTIBOOT_HEADER_FLAGS (0)
#define CHECKSUM (-(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS))

###################################################################
# 進入點
###################################################################

.text

# Multiboot標頭
.align 4
.long MULTIBOOT_HEADER_MAGIC
.long MULTIBOOT_HEADER_FLAGS
.long CHECKSUM

# '_start'指定ELF入口點。  既然當引導程序進入此代碼時我們還沒設置
# 虛擬內存,我們需要
# bootloader跳到入口點的*物理*地址。
.globl		_start
_start = RELOC(entry)

.globl entry
entry:
	movw	$0x1234,0x472			# 熱啓動

	# 我們尚未設置虛擬內存, 因此我們從
	# 引導加載程序加載內核的物理地址爲:1MB
	# (加上幾個字節)處開始運行.  但是,C代碼被鏈接爲在
	# KERNBASE+1MB 的位置運行。  我們建立了一個簡單的頁面目錄,
	# 將虛擬地址[KERNBASE,KERNBASE + 4MB)轉換爲
	# 物理地址[0,4MB)。  這4MB區域
	# 直到我們在實驗2 mem_init中設置真實頁面表爲止
	# 是足夠的。

	# 將entry_pgdir的物理地址加載到cr3中。   entry_pgdir
	# 在entrypgdir.c中定義。
	movl	$(RELOC(entry_pgdir)), %eax
	movl	%eax, %cr3
	# 打開分頁功能。
	movl	%cr0, %eax
	orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
	movl	%eax, %cr0

	# 現在啓用了分頁,但是我們仍在低EIP上運行
	# (爲什麼這樣可以?) 進入之前先跳到上方c代碼中的
	# KERNBASE
	mov	$relocated, %eax
	jmp	*%eax
relocated:

	# 清除幀指針寄存器(EBP)
	# 這樣,一旦我們調試C代碼,
	# 堆棧回溯將正確終止。
	movl	$0x0,%ebp			# 空幀指針

	# 設置堆棧指針
	movl	$(bootstacktop),%esp

	# 現在轉到C代碼
	call	i386_init

	# 代碼永遠不會到這裏,但如果到了,那就讓它循環死機吧。
spin:	jmp	spin


.data
###################################################################
# 啓動堆棧
###################################################################
	.p2align	PGSHIFT		# 頁面對齊
	.globl		bootstack
bootstack:
	.space		KSTKSIZE
	.globl		bootstacktop   
bootstacktop:

不在這兩個範圍之一內的任何虛擬地址都將導致硬件異常:導致QEMU轉儲計算機狀態並退出。

練習7:

使用QEMU和GDB跟蹤到JOS內核並在movl %eax, %cr0處停止。檢查0x00100000和0xf0100000的內存。現在,使用stepiGDB命令單步執行該指令。同樣,檢查內存爲0x00100000和0xf0100000。

在movl %eax, %cr0處停止:

(gdb) x 0x00100000
   0x100000:	add    0x1bad(%eax),%dh
(gdb) x 0xf0100000
   0xf0100000 <_start-268435468>:	add    %al,(%eax)

si:

0x00100028 in ?? ()
(gdb) x 0x00100000
   0x100000:	add    0x1bad(%eax),%dh
(gdb) x 0xf0100000
   0xf0100000 <_start-268435468>:	add    0x1bad(%eax),%dh

建立新映射後 的第一條指令是:

mov $relocated, %eax

這時的eax是:

(gdb) info registers
eax 0xf010002f -267386833

格式化打印到控制檯:

  • kern/printf.c

    內核的cprintf控制檯輸出的簡單實現,
    基於printfmt()和內核控制檯的cputchar()。

  • lib/printfmt.c

// 精簡的基本printf樣式格式化例程,
// 被printf,sprintf,fprintf等共同使用
// 內核和用戶程序也使用此代碼。

#include <inc/types.h>
#include <inc/stdio.h>
#include <inc/string.h>
#include <inc/stdarg.h>
#include <inc/error.h>

/*
 * 數字支持空格或零填充和字段寬度格式。
 * 
 *
 * 特殊格式%e帶有整數錯誤代碼
 * 並輸出描述錯誤的字符串。
 * 整數可以是正數或負數,
 * ,使-E_NO_MEM和E_NO_MEM等效。
 */

static const char * const error_string[MAXERROR] =
{
	[E_UNSPECIFIED]	= "unspecified error",
	[E_BAD_ENV]	= "bad environment",
	[E_INVAL]	= "invalid parameter",
	[E_NO_MEM]	= "out of memory",
	[E_NO_FREE_ENV]	= "out of environments",
	[E_FAULT]	= "segmentation fault",
};

/*
 * 使用指定的putch函數和關聯的指針putdat
 * 以相反的順序打印數字(基數<= 16).
 */
static void
printnum(void (*putch)(int, void*), void *putdat,
	 unsigned long long num, unsigned base, int width, int padc)
{
	// 首先遞歸地打印所有前面的(更重要的)數字
	if (num >= base) {
		printnum(putch, putdat, num / base, base, width - 1, padc);
	} else {
		// 在第一個數字前打印任何需要的填充字符
		while (--width > 0)
			putch(padc, putdat);
	}

	// 然後打印此(最低有效)數字
	putch("0123456789abcdef"[num % base], putdat);
}

// 從varargs列表中獲取各種可能大小的unsigned int,
// 取決於lflag參數。
static unsigned long long
getuint(va_list *ap, int lflag)
{
	if (lflag >= 2)
		return va_arg(*ap, unsigned long long);
	else if (lflag)
		return va_arg(*ap, unsigned long);
	else
		return va_arg(*ap, unsigned int);
}

// 與getuint相同
// 符號擴展
static long long
getint(va_list *ap, int lflag)
{
	if (lflag >= 2)
		return va_arg(*ap, long long);
	else if (lflag)
		return va_arg(*ap, long);
	else
		return va_arg(*ap, int);
}


// 用於格式化和打印字符串的主要函數
void printfmt(void (*putch)(int, void*), void *putdat, const char *fmt, ...);

void
vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{
	register const char *p;
	register int ch, err;
	unsigned long long num;
	int base, lflag, width, precision, altflag;
	char padc;

	while (1) {
		while ((ch = *(unsigned char *) fmt++) != '%') {
			if (ch == '\0')
				return;
			putch(ch, putdat);
		}

		// 處理%轉義序列
		padc = ' ';
		width = -1;
		precision = -1;
		lflag = 0;
		altflag = 0;
	reswitch:
		switch (ch = *(unsigned char *) fmt++) {

		// 標記以在右側填充
		case '-':
			padc = '-';
			goto reswitch;

		// 標記以0代替空格
		case '0':
			padc = '0';
			goto reswitch;

		// 寬度字段
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			for (precision = 0; ; ++fmt) {
				precision = precision * 10 + ch - '0';
				ch = *fmt;
				if (ch < '0' || ch > '9')
					break;
			}
			goto process_precision;

		case '*':
			precision = va_arg(ap, int);
			goto process_precision;

		case '.':
			if (width < 0)
				width = 0;
			goto reswitch;

		case '#':
			altflag = 1;
			goto reswitch;

		process_precision:
			if (width < 0)
				width = precision, precision = -1;
			goto reswitch;

		// long標誌(對long long加倍)
		case 'l':
			lflag++;
			goto reswitch;

		// 字符
		case 'c':
			putch(va_arg(ap, int), putdat);
			break;

		// 錯誤信息
		case 'e':
			err = va_arg(ap, int);
			if (err < 0)
				err = -err;
			if (err >= MAXERROR || (p = error_string[err]) == NULL)
				printfmt(putch, putdat, "error %d", err);
			else
				printfmt(putch, putdat, "%s", p);
			break;

		// 字符串
		case 's':
			if ((p = va_arg(ap, char *)) == NULL)
				p = "(null)";
			if (width > 0 && padc != '-')
				for (width -= strnlen(p, precision); width > 0; width--)
					putch(padc, putdat);
			for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--)
				if (altflag && (ch < ' ' || ch > '~'))
					putch('?', putdat);
				else
					putch(ch, putdat);
			for (; width > 0; width--)
				putch(' ', putdat);
			break;

		// (帶符號)十進制
		case 'd':
			num = getint(&ap, lflag);
			if ((long long) num < 0) {
				putch('-', putdat);
				num = -(long long) num;
			}
			base = 10;
			goto number;

		// 無符號十進制
		case 'u':
			num = getuint(&ap, lflag);
			base = 10;
			goto number;

		// (無符號)八進制
		case 'o':
			num = getint(&ap, lflag);
			if ((long long) num < 0) {
				putch('-', putdat);
				num = -(long long) num;
			}
			base = 8;
			goto number;

		// 指針
		case 'p':
			putch('0', putdat);
			putch('x', putdat);
			num = (unsigned long long)
				(uintptr_t) va_arg(ap, void *);
			base = 16;
			goto number;

		// (無符號)十六進制
		case 'x':
			num = getuint(&ap, lflag);
			base = 16;
		number:
			printnum(putch, putdat, num, base, width, padc);
			break;

		// 跳過 %
		case '%':
			putch(ch, putdat);
			break;

		// 遇到不符合規範的%格式,跳過
		default:
			putch('%', putdat);
			for (fmt--; fmt[-1] != '%'; fmt--)
				/* do nothing */;
			break;
		}
	}
}


  • kern/console.c

控制檯IO相關代碼;

練習8:

我們省略了一小段代碼-使用“%o”形式的模式打印八進制數字所必需的代碼。查找並填寫此代碼片段。

		case 'o':
			num = getint(&ap, lflag);
			if ((long long) num < 0) {
				putch('-', putdat);
				num = -(long long) num;
			}
			base = 8;
			goto number;

參考:https://blog.csdn.net/weixin_30466039/article/details/97003339?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.compare&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.compare

  1. 解釋printf.c和 console.c之間的接口。

    console.c 提供了輸入輸出字符的功能,大部分都在處理IO接口相關。

  2. 從console.c解釋以下內容:

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

當crt_pos >= CRT_SIZE,其中CRT_SIZE = 80*25,由於我們知道crt_pos取值範圍是0(80*25-1),那麼這個條件如果成立則說明現在在屏幕上輸出的內容已經超過了一頁。所以此時要把頁面向上滾動一行,即把原來的179號行放到現在的0~78行上,然後把79號行換成一行空格(當然並非完全都是空格,0號字符上要顯示你輸入的字符int c)。所以memcpy操作就是把crt_buf字符數組中179號行的內容複製到078號行的位置上。而緊接着的for循環則是把最後一行,79號行都變成空格。最後還要修改一下crt_pos的值。

  1. 參考上述代碼
  2. “Hello World”
  3. 不確定值
  4. 在vprintfmt中倒序處理參數

堆棧

在此過程中編寫一個有用的新內核監視器函數,該函數將顯示堆棧的回溯信息:保存的列表來自導致當前執行點的嵌套調用指令的指令指針(IP)值。

練習10:

http://www.cnblogs.com/fatsheep9146/p/5079930.html

練習11:

實現上述指定的backtrace函數。(默認參數下,並沒有遇到文中的bug

先了解一下test_backtrace是做什麼的;然後打印出堆棧信息和ebp函數調用鏈鏈信息,觀察即可發現。

代碼:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	cprintf("Stack backtrace:\n");
	uint32_t *ebp;
	ebp = (uint32_t *)read_ebp();
	while(ebp!=0){
		cprintf("  ebp %08x",ebp);
		cprintf(" eip %08x  args",*(ebp+1));
		for(int i=2;i<7;++i)
			cprintf(" %08x",*(ebp+i));
		cprintf("\n");
		ebp = (uint32_t *)*ebp;
	}
	return 0;
}

打印輸出:

  ebp f0110f18 eip f01000a5  args 00000000 00000000 00000000 f010004e f0112308
  ebp f0110f38 eip f010007a  args 00000000 00000001 f0110f78 f010004e f0112308
  ebp f0110f58 eip f010007a  args 00000001 00000002 f0110f98 f010004e f0112308
  ebp f0110f78 eip f010007a  args 00000002 00000003 f0110fb8 f010004e f0112308
  ebp f0110f98 eip f010007a  args 00000003 00000004 00000000 f010004e f0112308
  ebp f0110fb8 eip f010007a  args 00000004 00000005 00000000 f010004e f0112308
  ebp f0110fd8 eip f01000fc  args 00000005 00001aac 00000640 00000000 00000000
  ebp f0110ff8 eip f010003e  args 00000003 00001003 00002003 00003003 00004003

(爲什麼回溯代碼無法檢測到實際有多少個參數?如何解決此限制?):可以利用後續的獲取調試信息的方法;

練習12:

通過objdump打印出符號表信息,並嘗試找到函數;

yunwei@ubuntu:~/lab$ objdump -G obj/kern/kernel | grep f01000
0      SO     0      0      f0100000 1      {standard input}
1      SOL    0      0      f010000c 18     kern/entry.S
2      SLINE  0      44     f010000c 0      
3      SLINE  0      57     f0100015 0      
4      SLINE  0      58     f010001a 0      
5      SLINE  0      60     f010001d 0      
6      SLINE  0      61     f0100020 0      
7      SLINE  0      62     f0100025 0      
8      SLINE  0      67     f0100028 0      
9      SLINE  0      68     f010002d 0      
10     SLINE  0      74     f010002f 0      
11     SLINE  0      77     f0100034 0      
12     SLINE  0      80     f0100039 0      
13     SLINE  0      83     f010003e 0      
14     SO     0      2      f0100040 31     kern/entrypgdir.c
72     SO     0      0      f0100040 0      
73     SO     0      2      f0100040 2889   kern/init.c
108    FUN    0      0      f0100040 2973   test_backtrace:F(0,25)
118    FUN    0      0      f01000aa 3014   i386_init:F(0,25)

看看kdebug.h裏面的debuginfo_eip函數:

#ifndef JOS_KERN_KDEBUG_H
#define JOS_KERN_KDEBUG_H

#include <inc/types.h>

// 調試有關特定指令指針的信息
struct Eipdebuginfo {
	const char *eip_file;		// EIP的源代碼文件名
	int eip_line;			//  EIP的源代碼行號

	const char *eip_fn_name;	// 包含EIP的函數的名稱
					//  - 注意:不爲空終止!
	int eip_fn_namelen;		// 函數名稱的長度
	uintptr_t eip_fn_addr;		// 函數開始地址
	int eip_fn_narg;		// 函數參數的數量
};

int debuginfo_eip(uintptr_t eip, struct Eipdebuginfo *info);

#endif

由於包含EIP的函數的名稱不爲空終止,因此需要使用提示:

提示:printf格式字符串爲打印非空終止的字符串(如STABS表中的字符串)提供了一種簡單而又晦澀的方法。 printf("%.*s", length, string)最多可打印的length字符string。查看printf手冊頁,以瞭解其工作原理。

在 mon_backtrace() 中繼續修改,使用 debuginfo_eip 獲取相關信息並打印:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	cprintf("Stack backtrace:\n");
	uint32_t *ebp;
	int valid;
	struct Eipdebuginfo ei;
	ebp = (uint32_t *)read_ebp();
	while(ebp!=0){
		cprintf("  ebp %08x",ebp);
		cprintf(" eip %08x  args",*(ebp+1));
		valid = debuginfo_eip(*(ebp+1),&ei);
		for(int i=2;i<7;++i)
			cprintf(" %08x",*(ebp+i));
		cprintf("\n");
		if(valid == 0)
			cprintf("         %s:%d: %.*s+%d\n",ei.eip_file,ei.eip_line,ei.eip_fn_namelen,ei.eip_fn_name,*(ebp+1) - ei.eip_fn_addr);
		ebp = (uint32_t *)*ebp;
	}
	return 0;
}

可以參考 inc/stab.h:

//JOS uses the N_SO, N_SOL, N_FUN, and N_SLINE types.
#define	N_SLINE		0x44	// text segment line number

知道我們需要使用N_SLINE進行搜索;以及stab的數據結構:

// Entries in the STABS table are formatted as follows.
struct Stab {
	uint32_t n_strx;	// index into string table of name
	uint8_t n_type;         // type of symbol
	uint8_t n_other;        // misc info (usually empty)
	uint16_t n_desc;        // description field
	uintptr_t n_value;	// value of symbol
};

參考 的註釋部分:

// stab_binsearch(stabs, region_left, region_right, type, addr)
//
//	某些stab類型按升序排列在地址中
//	例如, N_FUN stabs ( n_type ==
//	N_FUN 的 stabs 條目), 標記了函數, 和 N_SO stabs,標記源文件。
//
//	給定指令地址,此函數查找單個 stab
//	條目, 包含該地址的'type'類型。
//
//	搜索在[* region_left,* region_right]範圍內進行。
//	因此,要搜索整個N個stabs,可以執行以下操作:
//
//		left = 0;
//		right = N - 1;     /* rightmost stab */
//		stab_binsearch(stabs, &left, &right, type, addr);
//

在 kern/kdebug.c 中 debuginfo_eip 相應位置修改,添加行數搜索:

	stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
	if(lline<=rline){
		info->eip_line = stabs[rline].n_value;
	}else{
		info->eip_line = 0;
		return -1;
	}

pass

running JOS: (1.4s) 
  printf: OK 
  backtrace count: OK 
  backtrace arguments: OK 
  backtrace symbols: OK 
  backtrace lines: OK 
Score: 50/50


結果是:

Stack backtrace:
  ebp f0110f18 eip f01000a5  args 00000000 00000000 00000000 f010004e f0112308
         kern/init.c:6: test_backtrace+101
  ebp f0110f38 eip f010007a  args 00000000 00000001 f0110f78 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f58 eip f010007a  args 00000001 00000002 f0110f98 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f78 eip f010007a  args 00000002 00000003 f0110fb8 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f98 eip f010007a  args 00000003 00000004 00000000 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110fb8 eip f010007a  args 00000004 00000005 00000000 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110fd8 eip f01000fc  args 00000005 00001aac 00000640 00000000 00000000
         kern/init.c:70: i386_init+82
  ebp f0110ff8 eip f010003e  args 00000003 00001003 00002003 00003003 00004003
         kern/entry.S:-267386818: <unknown>+0

雖然似乎eip並不一定指向對應的行…

總結:

這兩天大致搞清楚了boot的方式,然後瀏覽了一小部分的對應源代碼(雖然也不是很多的樣子),gdb還不算很熟練,大部分情況下還是使用cprintf打log;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章