從機器上電到執行OS發生了什麼?
在電腦主機板上有一個Flash塊,存放了BIOS的可執行程式碼。它是ROM,斷電不會丟掉資料。在機器上電的時候,CPU要求記憶體控制器從0地址讀取資料(程式第一條指令)的時候,記憶體控制器去主機板上的BIOS所在ROM讀取資料,此時CPU執行著BIOS。這裡BIOS主要做了以下3個任務:
- 檢測存在的硬體,並測試其是否正常工作。
- 初始化顯示卡、視訊記憶體,檢驗視訊訊號和同步訊號,對顯示器介面進行測試。
- 根據配置選擇某個外存(U盤、CD-ROM、硬碟這些)作為啟動,將其第一個扇區(BootLoader預設在儲存器的第一個扇區)載入到記憶體上某固定區段,然後設定CPU的CS:IP暫存器指向這個記憶體區域的起點。此時CPU執行著BootLoader。
在JOS實驗中, BootLoader的原始碼是boot/boot.S和boot/main.c。經過編譯連結得到ELF格式的二進位制檔案obj/boot/boot。這便是存放在0號扇區裡的BootLoader。
BootLoader會完成兩個主要功能:
- BootLoader將處理器從真實模式轉換為保護模式。
- BootLoader使用x86特定的IO指令直接訪問IDE磁碟裝置暫存器,從外存載入核心(也就是OS)到記憶體上,並設定CPU的CS:IP暫存器指向這個記憶體區域的起點,此時CPU正式開始執行作業系統。
Questions
At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
在boot/boot.S中,計算機首先工作於16bit工作模式(真實模式),當執行完 " ljmp $PROT_MODE_CSEG, $protcseg "語句後,正式進入32位工作模式(保護模式)。
What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
- bootmain子程式的最後一條語句
((void (*)(void)) (ELFHDR->e_entry))();
,即跳轉到作業系統核心程式的起始指令處。 - 第一條指令位於/kern/entry.S。為第一句
movw $0x1234, 0x472
。
How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?
- 作業系統檔案中的Program Header Table儲存了作業系統一共有哪些段,每個段有多少扇區等資訊。每個表項對應作業系統一個段。找到這個表後即可確定作業系統核心佔用了多少個扇區。
- 作業系統核心映像檔案的ELF頭部資訊記錄了這個表的儲存位置。
BootLoader載入作業系統核心的詳細過程
在JOS實驗中,作業系統核心最後編譯得到的是一個二進位制映像檔案obj/kern/kernel,這個檔案就是UNIX標準下的ELF格式檔案。
在JOS實驗中,可以簡單地認為obj/kern/kernel由三部分組成:
- 帶有載入資訊的檔案頭
- 程式段表
- 幾個程式段
大致如下圖所示:
這裡使用objdump -x obj/kern/kernel
檢視JOS核心的程式段表和所有段資訊:
VMA即連結地址,這個段希望被存放到的邏輯地址。
LMA即載入地址,這個段被載入到記憶體中後所在的實體地址。
BootLoader首先將ELF的header從外存載入到記憶體上,
然後根據程式段表依次將需要載入的程式段從外存載入到記憶體上。
最後將CPU的CS:IP設定成作業系統核心的入口位置,作業系統核心正式啟動。
核心準備就緒
在JOS實驗中,JOS核心的入口點的原始碼是/kern/entry.S的39行,從39行到77行這部分先開啟了paging,後初始化了堆疊。然後轉移到C語言寫的i386_init。
核心的記憶體機制
核心的設計者希望為使用者提供儘量大的記憶體空間,但是RAM的物理空間大小就那麼大,怎麼辦,段頁記憶體機制。
前面在讀程式段表的時候有兩個屬性,VMA和LMA。LMA是提供給BootLoader的,BootLoader根據LMA將核心的段們載入到記憶體的指定位置(就是段的LMA)。 VMA是提供給核心看的。
從軟體視角(核心的安排設計)的記憶體(虛擬記憶體)上看,kernel被載入到高位地址空間上,低位地址空間留給上層應用使用.堆疊記憶體就在這裡.
在JOS實驗中,我們使用GDB的si從核心入口0x10000C開始除錯,會碰到一條指令movl %eax, %cr0
,這條指令開啟paging,從而支援虛擬地址。
Problems / 動手實現printf格式化輸出到螢幕
- 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中的cprintf()實現依賴於vcprintf()的實現,vcprintf()的實現依賴於putch()的實現,putch()的實現依賴於console.c提供的cputchar().
- Explain the following from console.c:
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';
6 crt_pos -= CRT_COLS;
7 }
考慮上下文的變數宣告,
變數crt_buf: 一個字元陣列緩衝區,裡面存放著要顯示到螢幕上的字元.
變數crt_pos: 當前最後一個字元顯示在螢幕上的位置.
給出的程式碼是cga_putc的中間部分,cga_putc的上部分是根據字元值int c來判斷到底要顯示成什麼樣子. cpga_putc的下部分則把決定要顯示的字元顯示到螢幕指定位置.
- 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.
- 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.
The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
Here's a description of little- and big-endian and a more whimsical description.
(PASS)
- 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);
(PASS)
- 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?
The stack
kernel從哪條指令開始初始化堆疊?
kern/entry.S中,
call i386_init
指令前的這兩句:
movl $0x0,%ebp # nuke frame pointer
movl $(bootstacktop),%esp
JOS堆疊位於記憶體的什麼位置?
kern/entry.S中的這幾句初始化了JOS核心的分頁機制:
1 movl $(RELOC(entry_pgdir)), %eax
2 movl %eax, %cr3
3 movl %cr0, %eax
4 orl $(CR0_PE|CR0_PG|CR0_WP), %eax
5 movl %eax, %cr0
第1、2句,把entry_pgdir頁表的起始地址送入%eax暫存器和%cr3暫存器
第3、4、5句,修改cr0暫存器的值,把cr0的PE位,PG位, WP位都置位1。其中PE位是啟用保護標識位,如果被置1代表將會執行在保護模式下。PG位是分頁標識位,如果這一位被置1,則代表開啟了分頁機制。WP位是防寫標識,如果被置位為1,則處理器會禁止超級使用者程式向使用者級只讀頁面執行寫操作。
緊接著的下面這兩句
1 mov $relocated, %eax
2 jmp *%eax
把當前執行程式的地址空間提高到[0xf0000000-0xf0400000]範圍內。
然後
1 movl $0x0,%ebp # nuke frame pointer
2 movl $(bootstacktop),%esp
3 call i386_init
在entry.S的末尾還定義了一個值,bootstack。注意,在資料段中定義棧頂bootstacktop之前,首先分配了KSTKSIZE這麼多的儲存空間,專門用於堆疊,這個KSTKSIZE = 8 * PGSIZE = 8 * 4096 = 32KB。所以用於堆疊的地址空間為 0xf0108000-0xf0110000,其中棧頂指標指向0xf0110000. 那麼這個堆疊實際坐落在記憶體的 0x00108000-0x00110000實體地址空間中。