LAB1 啟動作業系統

從機器上電到運行OS發生了什麼?

在電腦主板上有一個Flash塊,存放了BIOS的可執行程式碼。它是ROM,斷電不會丟掉數據。在機器上電的時候,CPU要求記憶體控制器從0地址讀取數據(程式第一條指令)的時候,記憶體控制器去主板上的BIOS所在ROM讀取數據,此時CPU運行著BIOS。這裡BIOS主要做了以下3個任務:

  1. 檢測存在的硬體,並測試其是否正常工作。
  2. 初始化顯示卡、顯示記憶體,檢驗影片訊號和同步訊號,對顯示器介面進行測試。
  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會完成兩個主要功能:

  1. BootLoader將處理器從實模式轉換為保護模式。
  2. 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由三部分組成:

  • 帶有載入資訊的文件頭
  • 程式段表
  • 幾個程式段

大致如下圖所示:
avatar

這裡使用objdump -x obj/kern/kernel查看JOS內核的程式段表和所有段資訊:
avatar

VMA即鏈接地址,這個段希望被存放到的邏輯地址。

LMA即載入地址,這個段被載入到記憶體中後所在的物理地址。

BootLoader首先將ELF的header從外存載入到記憶體上,
然後根據程式段表依次將需要載入的程式段從外存載入到記憶體上。
最後將CPU的CS:IP設置成作業系統內核的入口位置,作業系統內核正式啟動。

內核準備就緒

在JOS實驗中,JOS內核的入口點的源程式碼是/kern/entry.S的39行,從39行到77行這部分先開啟了paging,後初始化了堆棧。然後轉移到C語言寫的i386_init。

內核的記憶體機制

內核的設計者希望為用戶提供盡量大的記憶體空間,但是RAM的物理空間大小就那麼大,怎麼辦,段頁記憶體機制。

avatar

前面在讀程式段表的時候有兩個屬性,VMA和LMA。LMA是提供給BootLoader的,BootLoader根據LMA將內核的段們載入到記憶體的指定位置(就是段的LMA)。 VMA是提供給內核看的。

從軟體視角(內核的安排設計)的記憶體(虛擬記憶體)上看,kernel被載入到高位地址空間上,低位地址空間留給上層應用使用.堆棧記憶體就在這裡.

在JOS實驗中,我們使用GDB的si從內核入口0x10000C開始調試,會碰到一條指令movl %eax, %cr0,這條指令打開paging,從而支援虛擬地址。

Problems / 動手實現printf格式化輸出到螢幕

  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中的cprintf()實現依賴於vcprintf()的實現,vcprintf()的實現依賴於putch()的實現,putch()的實現依賴於console.c提供的cputchar().

  1. 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的下部分則把決定要顯示的字元顯示到螢幕指定位置.

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

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

  1. 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物理地址空間中。

Reference