目錄
32位 DO_CALL (位於 i386 目錄下 sysdep.h)
64位 DO_CALL (位於 x86_64 目錄下 sysdep.h)
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 保護模式
- 在32位的架構下,將前一種模式稱爲實模式(Real Pattern),後一種模式稱爲保護模式(Protected Pattern)
- 系統剛剛啓動的時候,CPU處於實模式,此時和原來的模式是兼容的。即32位的CPU,也支持在原來的模式下運行,
- 當需要更多內存時,可以遵循一定的規則,進行一系列操作,然後切換到保護模式,就能夠用32位CPU更強大的能力
- 如果不能無縫兼容,但通過切換模式兼容,也是可以接受的
系統交互
常用匯編指令
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位系統調用分析