(二)系統初始化

目錄

x86架構概述

計算機的工作模式

8086處理器原理

32位處理器

系統交互

常用匯編指令

從BIOS到bootloader

BIOS 時期

bootloader 時期

從實模式切換到保護模式

kernel.img 運行

啓動過程總結

內核初始化

1-創建樣板進程,及各個模塊初始化  【各個職能部門的創建】

2-用戶態祖先進場創建-1號進程

3-內核態祖先進程的創建-2號進程

總結

系統調用過程

glibc 對系統調用的封裝

32位 DO_CALL (位於 i386 目錄下 sysdep.h)

64位 DO_CALL (位於 x86_64 目錄下 sysdep.h)

系統調用表 sys_call_table

總結-64位系統調用分析


x86架構概述

計算機的工作模式

CPU 包括三個部分,運算單元、數據單元 控制單元

  • 運算單元:負責計算
  • 數據單元:暫存數據,包括緩存和寄存器組
  • 控制單元:控制中心 取址執行

當前指令分兩部分:一部分做什麼操作 一部分操作那些數據

  • 數據單元根據數據的地址,從數據段裏讀到數據寄存器裏,就可以參與運算了。運算單元做完運算,產生的結果會暫存在數據單元的數據寄存器裏。最終,會有指令將數據寫回內存中的數據段。

總線上主要有兩類數據

  • 一個是地址數據,也就是我想拿內存中哪個位置的數據,這類總線叫地址總線(Address Bus);
  • 另一類是真正的數據,這類總線叫數據總線(Data Bus)

數據總線的位數,決定了一次能拿多少個數據進來。

8086處理器原理

數據單元:

  • 8086內部有8個16位的通用寄存器:AX、BX、CX、DX、SP、BP、SI、DI。
  • 這些寄存器主要用於在計算過程中暫存數據。
  • 這些寄存器比較靈活,其中 AX、BX、CX、DX 可以分成兩個 8 位的寄存器來使用
  • 這樣,比較長的數據也能暫存,比較短的數據也能暫存。

控制單元:

IP 寄存器就是指令指針寄存器(Instruction Pointer Register).指向代碼段中下一條指令的位置。

  • -CPU 會根據它來不斷地將指令從內存的代碼段中,加載到 CPU 的指令隊列中,然後交給運算單元去執行。

每個進程都分代碼段和數據段,爲了指向不同進程的地址空間,有四個 16 位的段寄存器。分別是CS、DS、SS、ES。

  • -CS就是代碼段寄存器,通過它可以找到代碼在內存中的位置
  • -CSDS 是數據段的寄存器,通過它可以找到數據在內存中的位置
  • -CSSS是棧寄存器
  • -CSES是附加段寄存器,存放當前執行程序中一個輔助數據段的段地址。 段寄存器 偏移地址寄存器

如果運算中需要加載內存中的數據,需要通過DS找到內存中 的數據,加載到通用寄存器中

  • 對於一個段,有一個起始的地址,而段內的具體位置,稱爲偏移量(Offset),在CS和DS中都存着一個段的起始地址。
  • 代碼段的偏移量在IP寄存器中,數據段的偏移量會放到通用寄存器中。

起始地址都是16位的,IP寄存器和通用寄存器都是16位的,偏移量也是16位的,但8086的地址總線地址是20位

  • 湊夠20位方法:起始地址16+偏移量,
  • 也就是把CS和DS中的值左移4位,變成20位,加上16位的偏移量,這樣就可以得到20位的數據地址

從這個計算方式可以算出,對於只有20位地址8086來講,能夠分出的地址也就2^20=1M,超過這個空間就訪問不到了。

  • 如果你想訪問1M+X的地方,這個位置已經超出20位了,由於地址總線只有20位,在總線上超過20位的部分是發不出去的,所以發出去的還是X,最後還是會訪問1M內的X的位置。

那麼一個段最大是多大,因爲偏移量只能是16位的,所以一個段最大的大小爲2^16=64K。

32位處理器

在32位的CPU中,有32根地址總線,可以訪問2^32=4G的內存

x86架構是開放的,因此32位的CPU需要兼容原來的架構

在開放架構的基礎上,如何保持兼容?

1. 通用寄存器 - 將8個16位的通用寄存器擴展到8個32位的通用寄存器,但依然保留16位和8位的使用方式

  • - 高16位不能分成兩個8位使用,因爲這是不兼容的

2. IP寄存器 - 指向下一條指令的指令指針寄存器IP

  • -會擴展成32位的,同樣兼容16位

3. 段寄存器 - CS、DS、SS和ES仍然是16位,但不再是段的起始地址,段的起始地址放在內存的某個地方(表格)

  • - 表格中的一項是段描述符,裏面纔是段真正的起始地址 - 而段寄存器裏面保存的是這個表格中某一項,稱爲選擇子
  • - 獲取段起始地址的流程:先間接地從段寄存器中找到表格中的一項,再從表格中的一項拿到段真正的起始地址
  • - 爲了快速拿到段的起始地址,段寄存器會從內存中拿到CPU的描述符高速緩存器中
  • - 這種模式與8086的模式不兼容,但非常靈活,可以保持未來的兼容性

實模式 VS 保護模式

  1. 在32位的架構下,將前一種模式稱爲實模式(Real Pattern),後一種模式稱爲保護模式(Protected Pattern)
  2. 系統剛剛啓動的時候,CPU處於實模式,此時和原來的模式是兼容的。即32位的CPU,也支持在原來的模式下運行,
  3. 當需要更多內存時,可以遵循一定的規則,進行一系列操作,然後切換到保護模式,就能夠用32位CPU更強大的能力
  4. 如果不能無縫兼容,但通過切換模式兼容,也是可以接受的

系統交互

常用匯編指令

move a b :把b值賦給a,使a=b


call和ret   :call調用子程序,子程序以ret結尾
jmp          :無條件跳
int            :中斷指令

or    :或運算
xor  :異或運算
shl   :算術左移
ahr  :算術右移


push xxx :xxx入棧
pop xxx  : xxx出棧

 

add a b :a=a+b
inc         : 加1
dec        : 減1
sub a b : a=a-b
cmp      : 減法比較,修改標誌位

從BIOS到bootloader

BIOS 時期

在主板上,有一個東西叫ROM(Read Only Memory,只讀存儲器)。

上面早就固化了一些初始化的程序,也就是BIOS(Basic Input and Output System,基本輸入輸出系統)。  

在x86系統中,將1M空間最上面的0xF0000到0xFFFFF這64K映射給ROM

當電腦剛加電的時候,會做一些重置的工作,

將CS設置爲0xFFFF,將IP設置爲0x0000,所以第一條指令就會指向0xFFFF0,正是在ROM的範圍內。

在這裏,有一個JMP命令會跳到ROM中做初始化工作的代碼,於是,BIOS開始進行初始化的工作

  • 1.BIOS 要檢查一下系統的硬件
  • 2.要建立一箇中斷向量表和中斷服務程序
  • 3.檢查正常則屏幕顯示系統BIOS信息

bootloader 時期

BIOS初始化之後,我們需要操作系統。那麼操作系統會在哪裏呢?一般都會安裝在硬盤上,在BIOS的界面上。你會看到一個啓動盤的選項。一般在第一個扇區,佔 512 字節,而且以 0xAA55 結束。

這是一個約定,當滿足這個條件的時候,就說明這是一個啓動盤,在 512 字節以內會啓動相關的代碼(這個扇區通常稱爲MBR(Master Boot Record主引導記錄 / 扇區)

1-BIOS完成任務後,會將boot.img從硬盤加載到內存中的0x7c00來運行 

2-由於512字節實在有限boot.img做不了太多的事情。它能做最重要一個事情就是加載grub2另一個鏡像core.img

  • boot.img 先加載的是 core.img 的第一個扇區。如從硬盤啓動,這個扇區裏是diskboot.img,對應代碼是 diskboot.S。
  • boot.img 將控制權交給 diskboot.img 後,diskboot.img 的任務就是將 core.img 的其他部分加載進來,

------先是解壓縮程序 lzma_decompress.img,再往下是 kernel.img,最後是各個模塊 module對應的映像。

這裏需要注意,它不是 Linux 的內核,而是 grub 的內核。
lzma_decompress.img 對應的代碼是 startup_raw.S,本來 kernel.img 是壓縮過的,現在執行的時候,需要解壓縮。

在真正的解壓縮之前,lzma_decompress.img 需要調用 real_to_prot,切換到保護模式,這樣就能在更大的尋址空間裏面,加載更多的東西

從實模式切換到保護模式

第一項是啓用分段,

  • 就是在內存裏面建立段描述符表,將寄存器裏面段寄存器變成段選擇子,指向某個段描述符,實現不同進程的切換

第二項是啓動分頁。

  • 能夠管理的內存變大了,就需要將內存分成相等大小的塊

第三項打開 Gate A20,也就是第 21根地址線的控制線。

  • (在實模式 8086 下面,一共就 20 個地址線,可訪問 1M 的地址空間。如果超過了這個限度怎麼辦呢?當然是繞回來了。在保護模式下,第 21 根要起作用了,於是我們就需要打開 Gate A20。 )
  • 切換保護模式的函數 DATA32 call real_to_prot 會打開 Gate A20,也就是第 21 根地址線的控制線

kernel.img 運行

kernel.img 對應的代碼是 startup.S 以及一堆 c 文件

在 startup.S 中會調用 grub_main,這是grub kernel 的主函數。

正常啓動,grub_main 最後會調用 grub_command_execute (“normal”, 0, 0),最終會調用 grub_normal_execute() 函數

  • 在這個函數裏面,grub_load_config() 開始解析, grub.conf 文件裏的配置信息。
  • 在這個函數裏面,grub_show_menu() 會顯示出讓你選擇的那個操作系統的列表
  • 一旦選定那個操作系統,就要開始調用 grub_menu_execute_entry()

開始執行選擇的那一項,例如裏面的linux16命令,表示裝載指定的內核文件,並傳遞內核啓動參數,

於是grub_cmd_linux()函數被調用,首先會讀取linux內核頭部的數據結構,加載到內存中來,檢查通過,會加載整個linux內核鏡像到內存

當都做完,調用grub_command_execute("boot",0,0),開始真正的啓動內核。

啓動過程總結

內核初始化

 內核初始化, 運行 `start_kernel()` 函數(位於 init/main.c), 初始化做三件事

  •     - 創建樣板進程, 及各個模塊初始化
  •     - 創建【管理/創建用戶態進程】的進程
  •     - 創建【管理/創建內核態進程】的進程

1-創建樣板進程,及各個模塊初始化  【各個職能部門的創建】

  • 1- (項目管理初始化)創建第一個進程, 0號進程. ` set_task_stack_end_magic(&init_task)                                                                                                                              struct task_struct init_task = INIT_TASK(init_task)`
  • 2- (辦事大廳初始化)初始化中斷, `trap_init()`. 系統調用也是通過發送中斷進行, 由 `set_system_intr_gate()` 完成.
  • 3- (會議室管理系統初始化) 初始化內存管理模塊, `mm_init()`
  • 4- (項目管理流程初始化)    初始化進程調度模塊, `sched_init()`
  • 5- (項目資料庫初始化)        初始化基於內存的文件系統 rootfs, `vfs_caches_init()` 
  • 6- (其他初始化)                   調用 `rest_init()` 完成其他初始化工作               

0 號進程。這是唯一一個沒有通過 fork 或者kernel_thread 產生的進程,是進程列表的第一個。

VFS(虛擬文件系統)將各種文件系統抽象成統一接口

2-用戶態祖先進場創建-1號進程

創建【管理/創建用戶態進程】的進程:1號進程 

1- rest_init()通過 kernel_thread(kernel_init,...) 創建 1號進程(工作在用戶態).

2- 權限管理

  •  x86 提供 4個 Ring 分層權限
  • 操作系統: Ring0-內核態(訪問核心資源); Ring3-用戶態(普通程序)

3- 用戶態調用系統調用:

  • 用戶態-系統調用-保存寄存器-內核態執行系統調用-恢復寄存器-返回用戶態 

4- 新進程執行 kernel_init 函數, 先運行 ramdisk 的 /init 程序(位於內存中)

  •  首先加載 ELF 文件
  •  設置用於保存用戶態寄存器的結構體
  •  返回進入用戶態
  •   /init 加載存儲設備的驅動

5- kernel_init 函數啓動存儲設備文件系統上的 init

3-內核態祖先進程的創建-2號進程

創建【管理/創建內核態進程】的進程: 2號進程 

- `rest_init()` 通過 `kernel_thread(kthreadd,...)` 創建 2號進程(工作在內核態).

- `kthreadd` 負責所有內核態線程的調度和管理

總結

系統調用過程

glibc 對系統調用的封裝

int open(const char *pathname, int flags, mode_t mode)glibc 裏面的 open 函數

make-syscall.sh  syscall-template.S

本節解析 glibc 函數如何調用到內核的 open

用戶進程調用 open 函數

  •     - glibc 的 syscal.list 列出 glibc 函數對應的系統調用
  •     - glibc 的腳本 make_syscall.sh 根據 syscal.list 生成對應的宏定義(函數映射到系統調用)
  •     - glibc 的 syscal-template.S 使用這些宏, 定義了系統調用的調用方式(也是通過宏)
  •     - 其中會調用 DO_CALL (也是一個宏), 32位與 64位實現不同

32位 DO_CALL (位於 i386 目錄下 sysdep.h)

  • 1- 將調用參數放入寄存器中, 由系統調用名得到系統調用號, 放入 eax
  • 2- 執行 ENTER_KERNEL(一個宏), 對應 int $0x80 觸發軟中斷, 進入內核
  • 3- 調用軟中斷處理函數 entry_INT80_32(內核啓動時, 由 trap_init() 配置)
  • 4- entry_INT80_32 將用戶態寄存器存入 pt_regs 中(保存現場以及系統調用參數), 調用 do_syscall_32_iraq_on 
  • 5- do_syscall_32_iraq_on 從 pt_regs 中取系統調用號(eax), 從系統調用表得到對應實現函數, 取 pt_regs 中存儲的參數, 調用系統調用
  • 6- entry_INT80_32 調用 INTERRUPT_RUTURN(一個宏)對應 iret 指令, 系統調用結果存在 pt_regs 的 eax 位置, 根據 pt_regs 恢復用戶態進程

64位 DO_CALL (位於 x86_64 目錄下 sysdep.h)

 - 通過系統調用名得到系統調用號, 存入 rax; 不同中斷, 執行 syscall 指令
    - MSR(特殊模塊寄存器), 輔助完成某些功能(包括系統調用)
    - trap_init() 會調用 cpu_init->syscall_init 設置該寄存器
    - syscall 從 MSR 寄存器中, 拿出函數地址進行調用, 即調用 entry_SYSCALL_64
    - entry_SYSCALL_64 先保存用戶態寄存器到 pt_regs 中
    - 調用 entry_SYSCALL64_slow_pat->do_syscall_64
    - do_syscall_64 從 rax 取系統調用號, 從系統調用表得到對應實現函數, 取 pt_regs 中存儲的參數, 調用系統調用
    - 返回執行 USERGS_SYSRET64(一個宏), 對應執行 swapgs 和 sysretq 指令; 系統調用結果存在 pt_regs 的 ax 位置, 根據 pt_regs 恢復用戶態進程

系統調用表 sys_call_table

    - 32位 定義在 arch/x86/entry/syscalls/syscall_32.tbl 
    - 64位 定義在 arch/x86/entry/syscalls/syscall_64.tbl
    - syscall_*.tbl 內容包括: 系統調用號, 系統調用名, 內核實現函數名(以 sys 開頭)
    - 內核實現函數的聲明: include/linux/syscall.h
    - 內核實現函數的實現: 某個 .c 文件, 例如 sys_open 的實現在 fs/open.c
        - .c 文件中, 以宏的方式替代函數名, 用多層宏構建函數頭
    - 編譯過程中, 通過 syscall_*.tbl 生成 unistd_*.h 文件
        - unistd_*.h 包含系統調用與實現函數的對應關係
    - syscall_*.h include 了 unistd_*.h 頭文件, 並定義了系統調用表(數組)

總結-64位系統調用分析

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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