MIT_JOS 學習筆記_Lab1.3


內核

操作系統的內核往往運行在高的虛擬地址空間, 使低的地址空間留給用戶程序.上一節我們知道, 內核運行的入口物理地址是 0x0010000c , 這個地址是在 0~ 4MB 地址空間范圍內的, 這個空間完全足夠內核開始運行. 內核的虛擬地址是內核希望執行的地址, 但是內存並沒有那么大的空間, 所以內核實際執行的地址是物理地址. 內核的虛擬地址是一個高地址, 是怎么映射到 0x000000000x00400000 這個低的物理地址空間的呢? 我們知道, boot loader 也是在實模式下運行的, 在 ./boot/boot.S 中啟用了保護模式, 但是 boot loader 的物理地址與虛擬地址是相同的,

這里使用的是 kern/entrypgdir.c 中的靜態映射表, 將虛擬地址映射到這個地址空間上的. 這個靜態映射表只能映射一部分的內存空間, 也就是將 0xf00000000xf04000000x00000000 through 0x00400000 都映射到物理地址為 0x00000000 through 0x00400000 的地址空間中. 所以我們在 ./kern/entry.S 中要做的就是確定頁目錄的物理地址與, 將這個地址存入 CR3 寄存器, 這個寄存器的作用就是存儲頁目錄的物理地址, 然后開啟頁表機制, 這樣就可以使用 kern/entrypgdir.c 中的 entry_pgtable 將 4MB 的虛擬地址映射到物理地址上, 那么 kern/entrypgdir.c 又是如何實現的呢?

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

pte_t entry_pgtable[NPTENTRIES];
// 頁表, 表示的是從  0x00000000 到 0x00400000 這 4MB 物理內存對應的頁表

__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
	// Map VA's [0, 4MB) to PA's [0, 4MB)
	[0] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
	// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
	[KERNBASE>>PDXSHIFT] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
	// KERNBASE>>PDXSHIFT是得到頁目錄號, 該頁目錄號對應的頁表也是 entry_ptable這個頁表
	// 所以, 頁目錄號為 0, 與頁目錄號為 3C00 對應的頁表都是 entry_pgdir, 最后是加上寫使能, 與頁表中的存在標志
};

// Entry 0 of the page table maps to physical page 0, entry 1 tophysical page 1, etc.
// 頁表的項到物理頁的地址, 靜態聲明的頁表
__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {
	0x000000 | PTE_P | PTE_W,
}

我們在回過頭來看 ./kern/entry.S 的內容就十分明顯了:

.globl		_start
_start = RELOC(entry)
# 將 entry 的虛擬地址變成物理地址, entry 本身也是個虛擬地址,

# Load the physical address of entry_pgdir into cr3.  entry_pgdir is defined in entrypgdir.c.
movl	$(RELOC(entry_pgdir)), %eax
# 需要注意的是這里, entry_pgdir 只有虛擬地址, 並且他不在 boot loader里面, 而是在內核里面, 對應的虛擬地址是在 0xf0000000 之后
movl	%eax, %cr3
# 將 頁目錄的物理地址存入 cr3寄存器

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

# Now paging is enabled, but we're still running at a low EIP
# (why is this okay?).  Jump up above KERNBASE before entering
# C code.

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 之前, 未啟用頁表機制, 0xf0100000 的數據要么內存沒有那么大, 要么未初始化, 啟用頁表機制后, 我們在查詢內存時候的 memory references 就是虛擬地址通過頁表機制將其轉化為的物理地址, 此時 0xf0100000 和 0x00100000 地址處的數據變成相同的了.

控制台格式化輸出

這一部分我分成兩部分講, 從控制台的角度講一下操作系統的 I/O 設備, 以及緩沖區, 這部分主要在 console.c 文件中,

先來介紹一下控制台使用的端口有哪些:

/***** Serial I/O code *****/
// 串口的地址, 注意 x86 的 I/O 編址是獨立編址
#define COM1		0x3F8

#define COM_RX		0	// In:	Receive buffer (DLAB=0)
#define COM_TX		0	// Out: Transmit buffer (DLAB=0)
#define COM_DLL		0	// Out: Divisor Latch Low (DLAB=1)
#define COM_DLM		1	// Out: Divisor Latch High (DLAB=1)
#define COM_IER		1	// Out: Interrupt Enable Register
#define   COM_IER_RDI	0x01	//   Enable receiver data interrupt
#define COM_IIR		2	// In:	Interrupt ID Register
#define COM_FCR		2	// Out: FIFO Control Register
#define COM_LCR		3	// Out: Line Control Register
#define	  COM_LCR_DLAB	0x80	//   Divisor latch access bit
#define	  COM_LCR_WLEN8	0x03	//   Wordlength: 8 bits
#define COM_MCR		4	// Out: Modem Control Register
#define	  COM_MCR_RTS	0x02	// RTS complement
#define	  COM_MCR_DTR	0x01	// DTR complement
#define	  COM_MCR_OUT2	0x08	// Out2 complement
#define COM_LSR		5	// In:	Line Status Register
#define   COM_LSR_DATA	0x01	//   Data available
#define   COM_LSR_TXRDY	0x20	//   Transmit buffer avail
#define   COM_LSR_TSRE	0x40	//   Transmitter off

具體的這些端口的用法在 一些常用端口的地址與用途 中提到了. 這里需要注意的是第一個地址, 0x3F8. 我們看一下網站的介紹

03F8	w	serial port, transmitter holding register, which contains the character to be sent. Bit 0 is sent first.
		 bit 7-0   data bits when DLAB=0 (Divisor Latch Access Bit)
	r	receiver buffer register, which contains the received character
		Bit 0 is received first
		 bit 7-0   data bits when DLAB=0 (Divisor Latch Access Bit)
	r/w	divisor latch low byte when DLAB=1

這一個端口有三種用途, 所以在上面的宏定義中使用了三種符號定義着同一個端口, 表示三種用途.

我們看一下下面這個例子:

// 返回從 COM1 端口讀入的數據
// COM_LSR 寄存器的 bit 0 = 1 表示 data ready. 
// a complete incoming character has been received and sent to the receiver buffer register.
static int serial_proc_data(void)
{
	if (!(inb(COM1+COM_LSR) & COM_LSR_DATA))
		return -1;
	return inb(COM1+COM_RX);
}

// 將讀入的數據放入輸入串口
void
serial_intr(void)
{
	if (serial_exists)
		cons_intr(serial_proc_data);
}

上面使用的 COM1 + COM_LSR 的地址為 0xFD , 這是一個只可讀的端口. 作用就是上面代碼的注釋部分. 需要注意的是, 源碼中所說的串口作為緩沖區, (串口的本質就是一個緩沖區), 真正輸出的位置是在控制台.

// called by device interrupt routines to feed input characters into the circular console input buffer.
// 將輸入字符放入緩沖區
// 這里的設備中斷是指, 比如說正在運行其他程序, 鍵盤開始輸入, 需要中斷其他程序
static void
cons_intr(int (*proc)(void))
{
	int c;
	// 這個函數的變量是一個函數的返回值, 這樣寫的目的是, 比如說對於鍵盤輸入, 下面的while 循環就需要不斷地從函數 proc 中獲取返回值
	while ((c = (*proc)()) != -1) {
		if (c == 0)
			continue;
		cons.buf[cons.wpos++] = c;
		if (cons.wpos == CONSBUFSIZE)
			cons.wpos = 0;
	}
}

這個緩沖區的定義為:

#define CONSBUFSIZE 512

static struct {
	uint8_t buf[CONSBUFSIZE];
	// 大小是 512 字節
	uint32_t rpos;
    // 從緩沖區讀數據的時候的位置
	uint32_t wpos;
	// 向緩沖區寫數據的時候的位置,也就是寫入的個數與位置
} cons;

以上部分是串口以及緩沖區的部分. 這是數據的中間部分, 也許現在還不是很清楚串口的作用, 之后會具體說明, 下面我們先說明一下控制台與鍵盤輸入,

控制台輸出

控制台的輸出主要分為兩個部分, 控制台光標的獲取與從光標位置輸出一個字符. 這一部分的代碼比較多, 中間部分的代碼就不詳細說明了,

static unsigned addr_6845;
// 控制台的輸出地址
static uint16_t *crt_buf;
// 控制台的輸出內容
static uint16_t crt_pos;
// 光標的位置, 輸出緩沖字符的個數

static void
cga_init(void)
{
	volatile uint16_t *cp;
	uint16_t was;
	unsigned pos;
	// 這個 cp 也是控制台的輸出地址, 相當於一個輸出的緩沖區
	cp = (uint16_t*) (KERNBASE + CGA_BUF);
	was = *cp;
	*cp = (uint16_t) 0xA55A;
	if (*cp != 0xA55A) {
		cp = (uint16_t*) (KERNBASE + MONO_BUF);
		addr_6845 = MONO_BASE;
	} else {
		*cp = was;
		addr_6845 = CGA_BASE;
	}

	/* Extract cursor location */
	outb(addr_6845, 14);
	pos = inb(addr_6845 + 1) << 8;
	outb(addr_6845, 15);
	pos |= inb(addr_6845 + 1);

	crt_buf = (uint16_t*) cp;
	crt_pos = pos;
}

上面最重要的是最后兩部分, cp 是根據不同的端口情況等計算出來的控制台輸出緩沖區, 最后將輸出的控制台內容指向這個緩沖區, 就得到控制台的輸出了, 而最后光標的位置就在 crt_pos 處, 獲取位置之后在控制台輸出一個字符:

// 從光標處輸出一個字符
static void
cga_putc(int c)
{
	// if no attribute given, then use black on white
	if (!(c & ~0xFF))
		c |= 0x0700;
	// 改變輸出背景顏色
	/*
    這里省略了一些轉義字符的輸出, 是一種功能性輸出
    */
	// What is the purpose of this?
	if (crt_pos >= CRT_SIZE) {
        // 位置超過了屏幕大小
		int i;
		// crt_buf + CRT_COLS 表示添加一行的數目, CRT_COLS 表示的是列的個數
		memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
        // crt_buf + CRT_COLS 表示添加一行的數目, CRT_COLS 表示的是列的個數
		// 將所有的行往前移動一行
		// 下面是將最后一行換成空格, 黑色的底
		for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
			crt_buf[i] = 0x0700 | ' ';
		crt_pos -= CRT_COLS;
        // 更新光標的位置
	}

	// 移動光標
	/* move that little blinky thing */
	outb(addr_6845, 14);
	outb(addr_6845 + 1, crt_pos >> 8);
	outb(addr_6845, 15);
	outb(addr_6845 + 1, crt_pos);
}

以上就是控制台的內容, 這是從輸出的角度來看 I/O 設備的, 接下來是鍵盤的內容, 從輸入的角度看 I/O 設備.

 /* Get data from the keyboard.  If we finish a character, return it.  Else 0.
 * Return -1 if no data.
 */
static int
kbd_proc_data(void)
{
	int c;
	uint8_t stat, data;
	static uint32_t shift;
	// 得到鍵盤控制器的狀態
	stat = inb(KBSTATP);
	// 如果鍵盤控制器的數據在緩沖區
	if ((stat & KBS_DIB) == 0)
		return -1;
	// Ignore data from mouse.
	if (stat & KBS_TERR)
		return -1;

	// 從鍵盤數據寄存器讀取數據
	data = inb(KBDATAP);
     
    // 中間省略大部分內容, 不是很重要的
     
    // Process special keys
	// Ctrl-Alt-Del: reboot
	if (!(~shift & (CTL | ALT)) && c == KEY_DEL) {
		cprintf("Rebooting!\n");
		outb(0x92, 0x3); // courtesy of Chris Frost
	}

	return c;
}

其實這部分內容本質也調用接口, 返回數據, 輸入與輸出講完, 下面再講一下完整的 I/O 輸入與輸出的過程, 輸入過程:

// return the next input character from the console, or 0 if none waiting
int cons_getc(void)
{
	int c;

	// poll for any pending input characters,
	// so that this function works even when interrupts are disabled
	// (e.g., when called from the kernel monitor).
	serial_intr();
    // 將 COM 端口讀入的數據放入輸入串口
	kbd_intr();
    // 將鍵盤的輸入放入緩沖區

	// grab the next character from the input buffer.
    // 從緩沖區讀數據
	if (cons.rpos != cons.wpos) {
		c = cons.buf[cons.rpos++];
		if (cons.rpos == CONSBUFSIZE)
			cons.rpos = 0;
		return c;
        // 返回控制台的輸入
	}
	return 0;
}

對於輸入的過程就是:

// output a character to the console
static void
cons_putc(int c)
{
	// 將輸出放入輸出串口
	serial_putc(c);
	// 串口並行化
	lpt_putc(c);
	// 從光標處輸出一個字符
	cga_putc(c);
}

輸出的格式化

在此之前, 必須要講一下 C語言中可變參數傳參, 這是 C語言的一個庫宏 va_arg(), 對於固定參數的函數, 在調用的時候, 會將棧指針向下移動, 將參數壓入棧頂端, 然后在進入函數后取出, 其實對於可變參數, 本質上也是一樣的. 宏定義的代碼是:

#define va_arg(ap, type) __builtin_va_arg(ap, type)
參數
  • ap -- 這是一個 va_list 類型的對象,存儲了有關額外參數和檢索狀態的信息。該對象應在第一次調用 va_arg 之前通過調用 va_start 進行初始化。
  • type -- 這是一個類型名稱。該類型名稱是作為擴展自該宏的表達式的類型來使用的。
返回值

該宏返回下一個額外的參數,是一個類型為 type 的表達式。例如 va_arg(ap, int), 就返回一個 int 類型的參數.

使用這個的目的是, 在 printf 函數中參數往往不止一個, 所以要用多個參數方式決定輸出,

void
vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{
	// putch 是簡單的cputchar(),然后對已經輸出的字符個數進行統計:
	// putch 是控制台輸出函數
	// putdat 是輸出最后一個字符的指針
	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);
			// 將 % 前面的全部輸出到控制台
			// 將 ch 輸出到控制台, putdat 是指向記錄輸出的個數的指針
		}

		// Process a %-escape sequence
		// 處理一系列的 % 的過程
		padc = ' ';
		width = -1;
		precision = -1;
		lflag = 0;
		altflag = 0;
		// Alt 的 Flag
	reswitch:
		switch (ch = *(unsigned char *) fmt++) {
		
		// flag to pad on the right
		case '-':
			padc = '-';
			goto reswitch;

		// flag to pad with 0's instead of spaces
		case '0':
			padc = '0';
			goto reswitch;

		// width field
		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 flag (doubled for long long)
		case 'l':
			// long 類型flag ++
			lflag++;
			goto reswitch;
		// 上面這些都是一些沒有意義的標志, 所以需要再讀取一個字符標志
		// character
		case 'c':
			putch(va_arg(ap, int), putdat);
			break;

		// error message
		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;

		// string
		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;

		// (signed) decimal
		case 'd':
			num = getint(&ap, lflag);
			if ((long long) num < 0) {
				putch('-', putdat);
				num = -(long long) num;
			}
			base = 10;
			goto number;

		// unsigned decimal
		case 'u':
			num = getuint(&ap, lflag);
			base = 10;
			goto number;

		// (unsigned) octal
		case 'o':
			// Replace this with your code.
			num = getuint(&ap, lflag);
			base = 8;
			goto number;
			break;

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

		// (unsigned) hexadecimal
		case 'x':
			num = getuint(&ap, lflag);
			base = 16;
		number:
			printnum(putch, putdat, num, base, width, padc);
			break;

		// escaped '%' character
		case '%':
			putch(ch, putdat);
			break;

		// unrecognized escape sequence - just print it literally
		default:
			putch('%', putdat);
			for (fmt--; fmt[-1] != '%'; fmt--)
				/* do nothing */;
			break;
		}
	}
}

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.

這一步根據上面的代碼很容易得出, 和16進制對比即可. 下面回答一下后面的問題:

Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?

這個很顯然, 在 printf.c 文件中寫的很清楚

static void
putch(int ch, int *cnt)
{
	cputchar(ch);
	// 向控制台輸出一個字符
	*cnt++;
	// 指向最近輸出字符的指針
}
int
vcprintf(const char *fmt, va_list ap)
{
	int cnt = 0;
	// 指定vprintfmt的字符輸出函數putch()
	vprintfmt((void*)putch, &cnt, fmt, ap);
	return cnt;
}

第二個問題前面代碼部分解釋過了,

  1. 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);
    
    • 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.

根據我們前面所解釋的 C語言的多參數調用, fmt 是指向字符串 "x %d, y %x, z %d\n" 的指針, ap 是一個對象指針, 這個對象是參數對象, 由於前面字符串中的聲明符號, 例如 %d 這些與后面的變量是一一對應的, 所以 ap 中的參數也是一一對應的. 因此 ap 是指向棧頂的那一個指針, call 之后就指向下一個參數. 注意, 在 GCC 中, 函數調用時的參數壓棧順序是與聲明的順序相反的, 所以 ap 指針會向上移動.

Run the following code.

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

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.

嘗試之后的結果很明顯, 輸出是 "He110 World" , 怎么來的也很明顯, 需要注意的是 0x00646c72, 在 x86 架構中是小端存儲的, 也就是說從高到低存儲了 00646c72 , 而輸出 %s 是從低到高讀取的, 其實是這樣的

00 64 6c 72

如果是%d 輸出就會從 00 之前輸出, 因為 int 是四字節, 就是這個范圍, 而 %s 是根據字符輸出的, 所以每次讀兩個字節, 也就是從 72 到 6c 到 64 再到 0, 所以輸出了一個字符串.

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

這里 x = 3 輸出之后, 因為 printf 的 ap 指針從 3 再往上移動一位, 這一指針所指的數據可能就是 ESP 寄存器存儲的棧頂了.

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?

相當於轉返了參數調用時候的壓棧順序, 所以需要改變上面的 ap 指針的移動方向, 這個方向可能定義在 va_start 函數中.

深入理解棧

這里先看一下虛擬內存分布:

/*
 * Virtual memory map:                                Permissions
 *                                                    kernel/user
 *
 *    4 Gig -------->  +------------------------------+
 *                     |                              | RW/--
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     :              .               :
 *                     :              .               :
 *                     :              .               :
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
 *                     |                              | RW/--
 *                     |   Remapped Physical Memory   | RW/--
 *                     |                              | RW/--
 *    KERNBASE, ---->  +------------------------------+ 0xf0000000      --+
 *    KSTACKTOP        |     CPU0's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     +------------------------------+                   |
 *                     |     CPU1's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                 PTSIZE
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     +------------------------------+                   |
 *                     :              .               :                   |
 *                     :              .               :                   |
 *    MMIOLIM ------>  +------------------------------+ 0xefc00000      --+
 *                     |       Memory-mapped I/O      | RW/--  PTSIZE
 * ULIM, MMIOBASE -->  +------------------------------+ 0xef800000
 *                     |  Cur. Page Table (User R-)   | R-/R-  PTSIZE
 *    UVPT      ---->  +------------------------------+ 0xef400000
 *                     |          RO PAGES            | R-/R-  PTSIZE
 *    UPAGES    ---->  +------------------------------+ 0xef000000
 *                     |           RO ENVS            | R-/R-  PTSIZE
 * UTOP,UENVS ------>  +------------------------------+ 0xeec00000
 * UXSTACKTOP -/       |     User Exception Stack     | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebff000
 *                     |       Empty Memory (*)       | --/--  PGSIZE
 *    USTACKTOP  --->  +------------------------------+ 0xeebfe000
 *                     |      Normal User Stack       | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebfd000
 *                     |                              |
 *                     |                              |
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     .                              .
 *                     .                              .
 *                     .                              .
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
 *                     |     Program Data & Heap      |
 *    UTEXT -------->  +------------------------------+ 0x00800000
 *    PFTEMP ------->  |       Empty Memory (*)       |        PTSIZE
 *                     |                              |
 *    UTEMP -------->  +------------------------------+ 0x00400000      --+
 *                     |       Empty Memory (*)       |                   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |  User STAB Data (optional)   |                 PTSIZE
 *    USTABDATA ---->  +------------------------------+ 0x00200000        |
 *                     |       Empty Memory (*)       |                   |
 *    0 ------------>  +------------------------------+                 --+
 *
 * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
 *     "Empty Memory" is normally unmapped, but user programs may map pages
 *     there if desired.  JOS user programs map pages temporarily at UTEMP.
 */

這一部分的解釋主要在 /inc/memlayout.c 文件中, 是對內存空間的描述. 所以我們先要明白虛擬內存地址與物理內存地址之間的關系. 內核的本質可以看做一系列的特定程序的集合. 用戶態與內核態的主要區別以我目前的理解是, 虛擬內存空間的划分, 在物理空間的位置, 程序對數據段, 代碼段的訪問權限. 根據之前解釋 ELF 文件的時候可以知道, 每個進程都有自己的虛擬空間, 而且對於程序而言, 數據段, 代碼段, 以及額外段的划分都是在虛擬空間中的. 學過編譯的都知道, 虛擬空間本質是不存在的, 我們常說的虛擬存儲系統, 比如說硬盤與虛擬空間並沒有關系, 也就是說, 虛擬存儲系統上並沒有存儲虛擬空間, 因為虛擬空間本身被程序划分了, 程序在編譯的過程中決定了虛擬空間的划分, 舉個例子, 一個 .c 程序在編譯成一個匯編程序的時候, 匯編程序中使用的就是虛擬地址, 比如說他會確定棧的大小, 以及參數在棧中的位置, 所以在虛擬磁盤上存儲的是一個完整的程序. 這個程序加載到物理內存的時候, 虛擬地址會轉變為物理地址, 最后在物理內存上運行.

從上面的表可以得出的直接結論是, 內核程序與用戶程序運行的位置在虛擬空間的不同位置, KERNBASE 我們並不陌生, 在 boot loader 的過程中我們將內核地址減去的就是這個 KERNBASE, 所以這個節點往上就是內核的代碼段, 將會被映射成物理地址, 再往下看:

KSTACKTOP* 表示內核棧的開頭, 之后我們會看到內核棧與用戶棧在虛擬空間不同的位置, 內核棧的大小KSTKSIZE 在 entry.S 里面, 在 GDB 里面調試可以獲得,

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

Breakpoint 1, relocated () at kern/entry.S:80
80              movl    $0x0,%ebp                       # nuke frame pointer
(gdb) si
=> 0xf0100034 <relocated+5>:    mov    $0xf0110000,%esp
relocated () at kern/entry.S:83
83              movl    $(bootstacktop),%esp
(gdb) b *0xf0100076
Breakpoint 2 at 0xf01

對於 x86 的寄存器已經不用多介紹了, 所以內核棧的大小就是 esp 的位置減去 ebp, 所以大小是 32KB.

然后我們從地址高處向下看, 到達了 MMIOLIM, 表示的是內存映射 I/O 的結尾, 可以看出, 內存映射 I/O 分配的空間大小為 4MB, 一個頁表的大小:

// Memory-mapped IO.
#define MMIOLIM		(KSTACKTOP - PTSIZE)
#define MMIOBASE	(MMIOLIM - PTSIZE)

下面的一部分是用戶的頁表, UVPT 表示當前進程頁表的基地址, 下面一部分是 UPAGES 開始的頁表的副本. 再往下的 UENVS 是全局虛擬環境結構的副本, 再往下是用戶空間, 用戶空間的數據段是由棧與堆構成的, 其余的內容就不贅述了. 很容易理解.

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?

其實這部分直接看源碼就可以理解, 在實驗的時候, 我們調用兩次看一下有什么不同,

從 C語言的角度來說, 這是個遞歸函數:

// Test the stack backtrace function (lab 1 only)
void
test_backtrace(int x)
{
	cprintf("entering test_backtrace %d\n", x);
	if (x > 0)
		test_backtrace(x-1);
	else
		mon_backtrace(0, 0, 0);
	cprintf("leaving test_backtrace %d\n", x);
}

obj/kern/kernel.asm 文件中, 我們找到 test_backtrace 的匯編代碼可以得到, 在遞歸調用的時候執行的代碼段為:


// Test the stack backtrace function (lab 1 only)
void
test_backtrace(int x)
{
f0100040:	55                   	push   %ebp 							# 存入 ebp 寄存器
f0100041:	89 e5                	mov    %esp,%ebp
f0100043:	56                   	push   %esi
f0100044:	53                   	push   %ebx
f0100045:	e8 72 01 00 00       	call   f01001bc <__x86.get_pc_thunk.bx>
f010004a:	81 c3 be 12 01 00    	add    $0x112be,%ebx
f0100050:	8b 75 08             	mov    0x8(%ebp),%esi
	cprintf("entering test_backtrace %d\n", x);
f0100053:	83 ec 08             	sub    $0x8,%esp
f0100056:	56                   	push   %esi
f0100057:	8d 83 18 07 ff ff    	lea    -0xf8e8(%ebx),%eax
f010005d:	50                   	push   %eax
f010005e:	e8 e6 09 00 00       	call   f0100a49 <cprintf>
	if (x > 0)
f0100063:	83 c4 10             	add    $0x10,%esp
f0100066:	85 f6                	test   %esi,%esi
f0100068:	7f 2b                	jg     f0100095 <test_backtrace+0x55>
		test_backtrace(x-1);
		# 跳轉到下面的 test_backtrace(x-1)
		test_backtrace(x-1);
f0100095:	83 ec 0c             	sub    $0xc,%esp
f0100098:	8d 46 ff             	lea    -0x1(%esi),%eax
f010009b:	50                   	push   %eax
f010009c:	e8 9f ff ff ff       	call   f0100040 <test_backtrace>
f01000a1:	83 c4 10             	add    $0x10,%esp
f01000a4:	eb d5                	jmp    f010007b <test_backtrace+0x3b>

這一段有 4 次 push,其中,在printf的過程中,調用完printf函數后,棧指針回到調用前的位置, 最后是 call 下一個的時候,會首先將%eip push入棧,在上述的計算中, 在(x>0)時, 棧的地址變化為 4+4+4+16+4, 在(x<0) 時,地址變化為 4+4+4+12+4+4, 所以一共是32bytes

對於匯編語言, mov %esp,%ebp 可以看做是函數開始的標志, 因為 ebp 標志了函數棧的 Top, esp 是棧底, 所以在進入函數之前的指令為:

# 在進入之前, 需要將 eip 存入棧底, 但是這個棧是調用函數棧
test_backtrace
f0100040:	55                   	push   %ebp 							# 存入 ebp 寄存器
f0100041:	89 e5                	mov    %esp,%ebp
# 所以在 ebp 的上面有兩個前一個函數棧的內容, 按照從下往上的順序是 ebp 自己, 和 eip

Exercise 11. 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

需要注意的是:

Finally, the five hex values listed after args are the first five arguments to the function in question, which would have been pushed on the stack just before the function was called

也就是說, 這五個參數空間是靜態的, 有可能有的函數不足五個參數. 對於 Exercise 11 的代碼實現就很簡單了:

int mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	// Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp();
    uint32_t *eip = (uint32_t *)ebp[1];
    // ebp 與 eip 的值
    uint32_t args[5], i;
    for (i = 0; i < 5; i++)
        args[i] = ebp[i + 2];
    // 參數的位置還在 eip 上面
    cprintf("Stack_backtrace:\n");
    while (ebp != NULL)
    {
        cprintf(" ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x 0x%08x\n", ebp, eip, args[0], args[1], args[2], args[3], args[4]);
        ebp = (uint32_t *)ebp[0];
        eip = (uint32_t *)ebp[1];
        for (i = 0; i < 5; i++)
            args[i] = ebp[i + 2];
    }
    return 0;
}

通過符號表 Debug

調試器中的符號表

Stabs 是程序的一種信息格式, 用於在調試器中描述程序. 在 GNU 中使用“ -g”選項,GCC在.s文件中放入其他調試信息,這些信息由匯編器和鏈接器稍作轉換,並傳遞到最終的可執行文件中。這些調試信息描述了源文件的功能,例如行號,變量的類型和范圍以及函數名稱,參數和范圍。對於某些目標文件格式,調試信息被封裝在稱為stab(符號表)指令的匯編程序指令中,該指令散布在生成的代碼中。 Stabs 是a.out和XCOFF目標文件格式的調試信息的本機格式。GNU工具還可以以COFF和ECOFF對象文件格式產生 stabs。默認情況下匯編器將stab中的信息添加到要構建的.o文件的符號表和字符串表中的符號信息中。 鏈接器將.o文件合並為一個可執行文件,其中包含一個符號表和一個字符串表。 調試器使用可執行文件中的符號和字符串表作為有關程序的調試信息的來源。

符號表的基本格式

stab匯編程序指令的總體格式有三種 .stabs (string), .stabn (number), .stabd (dot). ,按stab的第一個單詞進行區分。 偽指令的名稱描述了以下四個可能的數據字段的組合:

.stabs "string",type,other,desc,value
.stabn type,other,desc,value
.stabd type,other,desc
.stabx "string",value,type,sdb-type

對於.stan.stand,沒有字符串, 對於.stabd,值字段是隱式的,並具有當前文件位置的值。對於.stabxsdb-type字段未用於stabs,並且始終可以設置為零。 另一個字段幾乎總是未使用,可以設置為零。

然后回頭看一下 /kern/kdebug.c 的文件的內容, 主要是兩個函數, 第一個函數我們只要知道功能是什么就可以了,

Given an instruction address, this function finds the single stab entry of type 'type' that contains that address.
//
static void stab_binsearch(const struct Stab *stabs, int *region_left, int *region_right, int type, uintptr_t addr)
//  參數的說明
//	stabs 是符號表
//  *region_left 是查找的左邊地址, *region_right 是查找的右邊地址
//  type 是類型, 比如說函數, 或者 .c文件
//  addr 是要查詢的地址

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.

首先看一下 stab 的結構體的定義:

// Entries in the STABS table are formatted as follows.
struct Stab {
	uint32_t ;	// 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
};
/*
stabstr 是對應的字符串數組
n_strx 是字符串索引,這里是對於文件名來說, 函數名來說, 是存儲字符串數組的下標(偏移)
n_type 是符號類型,FUN指函數名,SLINE指在text段中的行號
n_othr 目前沒被使用,其值固定為0
n_desc 表示在文件中的行號
n_value 表示地址。特別要注意的是,這里只有FUN類型的符號的地址是絕對地址,SLINE符號的地址是偏移量,
其實際地址為函數入口地址加上偏移量。比如第3行的含義是地址f01000b8(=0xf01000a6+0x00000012)對應文件第34行。
*/

完成 /kern/kdebug.c 的代碼,


	if (lfun <= rfun) {
		// stabs[lfun] points to the function name
		// in the string table, but check bounds just in case.
		if (stabs[lfun].n_strx < stabstr_end - stabstr)
			info->eip_fn_name = stabstr + stabs[lfun].n_strx;
		info->eip_fn_addr = stabs[lfun].n_value;
		// 被調用函數的地址
		addr -= info->eip_fn_addr;
		// Search within the function definition for the line number.
		lline = lfun;
		rline = rfun;
	} else {
		// Couldn't find function stab!  Maybe we're in an assembly
		// file.  Search the whole file for the line number.
		info->eip_fn_addr = addr;
		lline = lfile;
		rline = rfile;
	}
	// Ignore stuff after the colon.
	info->eip_fn_namelen = strfind(info->eip_fn_name, ':') - info->eip_fn_name;


	// 類似於上面的尋找函數與文件名的方法
	// 查找得到 N_SLINE 表示 line 類型
	// lline 與 rline 都是從前面繼承過來的
	stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
    if (lline > rline)
        return -1;
    info->eip_line = stabs[lline].n_desc;
	// 對於 line 類型來說, n_desc 存儲行號
	// Hint:
	//	There's a particular stabs type used for line numbers.
	//	Look at the STABS documentation and <inc/stab.h> to find
	//	which one.
	// Your code here.

需要注意的是, 后面還有一段代碼,

// 從符號表的 line 部分減到有效的文件部分, 
while (lline >= lfile && stabs[lline].n_type != N_SOL && (stabs[lline].n_type != N_SO || !stabs[lline].n_value))
		lline--;
if (lline >= lfile && stabs[lline].n_strx < stabstr_end - stabstr)
	info->eip_file = stabstr + stabs[lline].n_strx;

這一段代碼的作用是, 在查找行數之后要重新查找一次源文件, 因為有可能該函數是內聯函數, 來自其他文件, 那種文件的類型定義為 N_SOL, 最后在對應的字符串數組中找到文件名稱.

下一個問題:

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: <unknown>+0
K>

這里需要修改的是 kern/monitor.c 文件中的 mon_kerninfo 函數, 上面的部分完成就很明顯了, 主要是使用 debuginfo_eip 函數獲得文件名, 函數名與行號, 所以定義一個 struct Eipdebuginfo *info 類型的指針即可. 補全的函數如下:

int mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	// Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp();
    uint32_t *eip = (uint32_t *)ebp[1];
    uint32_t args[5], i;
	struct Eipdebuginfo dbg_info;
    for (i = 0; i < 5; i++)
        args[i] = ebp[i + 2];
    
    cprintf("Stack_backtrace:\n");
    while (ebp != NULL)
    {
        cprintf(" ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x 0x%08x\n",
            ebp, eip, args[0], args[1], args[2], args[3], args[4]);
		debuginfo_eip((uintptr_t)eip, &dbg_info);
		// 獲取信息

		cprintf("\t%s:%d %.*s+%d\n", dbg_info.eip_file, dbg_info.eip_line, dbg_info.eip_fn_namelen, dbg_info.eip_fn_name, ebp[1] - dbg_info.eip_fn_addr);
        ebp = (uint32_t *)ebp[0];
        eip = (uint32_t *)ebp[1];
        for (i = 0; i < 5; i++)
            args[i] = ebp[i + 2];
    }
    return 0;
}

至此 Lab1 的內容就全部完成了.

總結開機的過程

我們將整個開機的過程以及對應的地址關系, 以及執行的文件對應一下, 就是下面的內容:

執行的過程 執行的物理地址 代碼所在的虛擬地址 對應的文件 功能
BIOS 0x000F0000 ~ 0x00100000 在 ROM 中 開機引導磁盤,找到boot loader文件, 並將其導入物理內存
boot loader 0x7c00 ~ 0x7dff 系統盤的第一個扇區 ./boot/boot.S 和 ./boot/main.c 先將內核的 ELF 頭部導入物理地址為 0x10000 處, 然后將內核數據段與代碼段導入到物理地址為 0x00100000 處
entry 0x0010000c 0xf010000c entry.S 啟用頁表機制, 將頁目錄的物理地址存入 CR0 寄存器


免責聲明!

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



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