問題:linux
過程:
按下電源開關 ——> 主板發送信號給電源 ——> 電源收到信號給電腦供電 ——> 主板收到「電源備妥信號」 ——> 嘗試啓動CPU ——> CPU復位全部寄存器數據,並設置預約值
注意:
處理器開始在「實模式」工做,它有20位的尋址總線,尋址空間是0~2^20(1MB),但它的寄存器卻只有16位(2^16即64KB),因此實模式使用「段式內存管理」來管理整個內存空間。
替代方法:chrome
PhysicalAddress = Segment * 16 + offset
但:數組
>>> hex((0xffff << 4) + 0xffff) '0x10ffef'
已經超出1MB範圍。既然實模式下, CPU 只能訪問 1MB 地址空間,0x10ffef變成有A20缺陷的0x00ffef(CPU只有20位,最高位將被捨棄)網絡
CS:IP 兩個寄存器指示了 CPU 當前將要讀取的指令的地址 電腦復位後,CPU寄存器中的預約義數據:app
IP 0xfff0 CSselector 0xf000 CSbase 0xffff000
邏輯地址: CS:IPide
0xffff0000:0xfff0 >>> 0xffff0000 + 0xfff0 '0xfffffff0'
這個地方是復位向量(Reset vector)。這是CPU在重置後指望執行的第一條指令的內存地址。它包含一個jump指令,這個指令一般指向BIOS入口點。
在初始化和檢查硬件以後,須要尋找到一個可引導設備。可引導設備列表存儲在在 BIOS 配置中, BIOS 將根據其中配置的順序,嘗試從不一樣的設備上尋找引導程序。對於硬盤,BIOS 將嘗試尋找引導扇區。
一個真實的啓動扇區包含了分區表,已經用來啓動系統的指令,而不是像咱們上面的程序,只是輸出了一個感嘆號就結束了。從啓動扇區的代碼被執行開始,BIOS 就將系統的控制權轉移給了引導程序。
實模式下的1MB地址空間分配表:函數
0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table 實模式中斷向量表 0x00000400 - 0x000004FF - BIOS Data Area BIOS數據區 0x00000500 - 0x00007BFF - Unused 未被使用 0x00007C00 - 0x00007DFF - Our Bootloader 咱們的引導加載程序 0x00007E00 - 0x0009FFFF - Unused 0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory 視頻RAM(VRAM)存儲器 0x000B0000 - 0x000B7777 - Monochrome Video Memory 單色視頻存儲器 0x000B8000 - 0x000BFFFF - Color Video Memory 彩色視頻存儲器 0x000C0000 - 0x000C7FFF - Video ROM BIOS 視頻ROMD的BIOS 0x000C8000 - 0x000EFFFF - BIOS Shadow Area BIOS陰影區 0x000F0000 - 0x000FFFFF - System BIOS 系統BIOS
問題:CPU 執行的第一條指令是在地址0xFFFFFFF0處,這個地址遠遠大於0xFFFFF ( 1MB )。那麼實模式下的 CPU 是如何訪問到這個地址的呢?文檔coreboot給出了答案:spa
0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space
0xFFFFFFF0這個地址被映射到了 ROM,所以 CPU 執行的第一條指令來自於 ROM,而不是RAM。
小總結:操作系統
1. CPU寄存器復位(0xffff0) 2. 讀取ROM指令(jump) 3. 跳轉至BIOS入口點 4. 尋找可引導設備(可引導程序,可引導扇區)
拓展知識:指針
遺留問題:
Linux有多種引導程序,好比GRUB 2和syslinux.
當 BIOS 已經選擇了一個啓動設備,而且將控制權轉移給了啓動扇區中的代碼(boot.img,很是簡單隻作必要初始化),跳轉到 GRUB 2's core image(diskboot.img,通常是在磁盤上存儲在啓動扇區以後到第一個可用分區以前)去執行。
core image 的初始化代碼會把整個 core image (包括 GRUB 2的內核代碼和文件系統驅動)引導到內存中。引導完成以後,grub_main將被調用。
grub_main 初始化控制檯,計算模塊基地址,設置 root 設備,讀取 grub 配置文件,加載模塊。最後,將 GRUB 置於 normal 模式,在這個模式中,grub_normal_execute (from grub-core/normal/main.c) 將被調用以完成最後的準備工做,而後顯示一個菜單列出所用可用的操從引導加載程序內核1做系統。當某個操做系統被選擇以後,grub_menu_execute_entry 開始執行,它將調用 GRUB的boot命令,來引導被選中的操做系統。
正如kernel boot protocol 所描述的,引導程序必須填充 kernel setup header (位於 kernelsetup code 偏移0x01f1處)的必要字段。
當 bootloader 完成任務,將執行權移交給 kernel.
小總結:
// 讀取可引導程序,包括: 1. 啓動扇區代碼(boot.img),必要的初始化,跳轉至 GRUB 2's core image 2. core image 的初始化代碼將core image(內核代碼、文件系統驅動)引導到內存中 3. 引導完成後,調用grub_main,初始化控制檯、計算模塊基地址、設置root設備、讀取grub配置文件、加載模塊。將GRUB置於normal模式,最後調用grub_normal_execute顯示可用的操做系統 4. 當操做系統被選擇以後,grub_menu_execute_entry 開始執行,將調用GRUB的 boot 命令,引導被選中的操做系統 5. 當 bootloader完成任務後,將執行權移交給kernel
遺留問題:
從技術上說,內核尚未被運行起來,由於首先咱們須要正確設置內核,啓動內存管理,進程管理等等。
而內核設置代碼的運行起點是 _start函數,但在其開始以前,還有不少代碼(bootloader)。去除這些做爲 bootloader 使用的代碼,真正的內核代碼就從_start開始了。其餘的 bootloader (grub2 and others) 知道 _start 所在的位置(從MZ頭開始偏移0x200字節),因此這些 bootloader 就會忽略全部在這個位置前的代碼(這些以前的代碼位於.bstext段中),直接跳轉到這個位置啓動內核。從引導加載程序內核。
_start 開始就是一個 jmp 語句,短跳轉至 start_of_setup - 1f。在_start標號以後的第一個標號爲1的代碼段中包含了剩下的 setup header 結構。在標號爲1的代碼段結束以後,緊接着就是標號爲start_of_setup的代碼段(這個代碼段位於.entrytext代碼區,這個代碼段中的第一條指令其實是內核開始執行以後的第一條指令)
從start_of_setup標號開始的代碼須要完成下面這些事情:
將DS和ES段寄存器的內容設置同樣:
movw %ds, %ax movw %ax, %es sti
將DS和CS段寄存器設置相同的值:
pushw %ds //將DS寄存器的值入棧 pushw $6f //將標號爲6的代碼段地址入棧 lretw //執行lretw指令將把標號爲6的內存地址放入ip寄存器
絕大部分的 setup 代碼都是爲 C 語言運行環境作準備。在設置了ds和es寄存器以後,接下來step的代碼將檢查ss寄存器的內容,若是寄存器的內容不對,那麼將進行更正:
movw %ss, %dx cmpw %ax, %dx //比較ss是否等於ax movw %sp, %dx //將sp值保存到dx je 2f //兩數相等跳轉至標號2段代碼
2: andw $~3, %dx //將dx寄存器的值(就是當前sp寄存器的值)4字節對齊 jnz 3f //檢查是否爲0,不爲0跳轉 movww $0xfffc, %dx //爲0表示棧區已滿,須要將sp重置至棧底一個字節前 0xfffc 3: movw %ax, %ss //由於ss和ax相等,因此設置ss棧底地址爲0x10000 movzwl %dx, %esp //不爲0保留當前sp的值 sti
特別地,當 ss != ds 時,要先將setup code 的結束地址 _end 寫入 dx寄存器,而後再檢查 loadflags 中是否設置了 CAN_USE_HEAP 標誌。
movw heap_end_ptr, dx
overflow dx + STACK_SIZE, CF_flag //判斷dx是否在棧頂,意思是若是在棧頂 0+STACK_SIZE=ss jn 2f
movw dx + STACK_SIZE, dx //同理 jmp 2f
在咱們正式執行C代碼以前,還有2件事情須要完成:
首先檢查 magic 簽名 setup_sig,若是簽名不對,直接跳轉到 setup_bad 部分執行代碼:
cmpl $0x5a5aaa55, setup_sig jne setup_bad
若是 magic 簽名是對的,那麼咱們只要設置好 BSS 段就能夠考試執行C代碼了。
BSS 段用來存儲那些沒有被初始化的靜態變量。對於這個段使用的內存, Linux 首先使用下面的代碼將其所有清零:
movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stopsl
在這段代碼中,首先將__bss_start地址放入di寄存器,而後將_end + 3(4字節對齊)地址放入cx,接着使用xor指令將ax寄存器清零,接着計算 BSS 段的大小(cx -di),讓後將大小放入cx寄存器。接下來將cx寄存器除4,最後使用rep; stosl指令將ax寄存器的值(0)寫入寄存器整個 BSS 段。
BSS段 :一般是指用來存放程序中 未初始化的全局變量、靜態變量(全局變量未初始化時默認爲0)的一塊內存區域
數據段 :一般是指用來存放程序中 初始化後的全局變量和靜態變量
代碼段 :一般是指用來存放程序中 代碼和常量
堆 :一般是指用來存放程序中 進程運行時被動態分配的內存段 ( 動態分配:malloc / new,者動態釋放:free / delete)
棧 :一般是指用來存放程序中 用戶臨時建立的局部變量、函數形參、數組(局部變量未初始化則默認爲垃圾值)也就是說咱們函數括弧「{}」中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除 之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進後出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。它是由操做系統分配的,內存的申請與回收都由OS管理。
到目前爲止,咱們完成了堆棧和 BSS 的設置,如今咱們能夠正式跳入main()函數來執行 C代碼了:
calll main
小總結:
1. 執行_start函數 2. 開始是一個jmp語句,跳轉至start_of_setip - 1f (該代碼段中包含了剩下的setup header結構) 3. 在 start_of_setip - 1f 代碼段結束以後,執行 start_of_setup 的代碼段,實際上這是內核開始執行以後的第一條指令 4. start_of_setup 代碼開始後,將完成如下工做: - 將全部段寄存器的值設置成同樣的內容 - 設置堆棧 - 設置bss(靜態變量區) - 跳轉到main.c開始執行代碼
遺留問題:
main() 函數定義在 arch/x86/boot/main.c, 下一篇文章將會詳細介紹在Linux內核設置過程當中調用的第一個C代碼(main()),也將介紹諸如 memset, memcpy, earlyprintk 這些底層函數的實現,敬請期待!