#知識點 操作系統的啟動知識和中斷的建立與初始化 涉及到Intel 806386寄存器,AT&T彙編,gcc內聯彙編,C函數堆棧,Makefile等知識 筆記主要按照操作系統的啟動和中斷的建立兩個部分來記錄 ##理論課的介紹 ###系統啟動 當CPU剛加電初始化時,CS:IP寄存器根據設定的初始值跳 ...
知識點
- 操作系統的啟動知識和中斷的建立與初始化
- 涉及到Intel 806386寄存器,AT&T彙編,gcc內聯彙編,C函數堆棧,Makefile等知識
筆記主要按照操作系統的啟動和中斷的建立兩個部分來記錄
理論課的介紹
系統啟動
當CPU剛加電初始化時,CS:IP寄存器根據設定的初始值跳轉到BIOS固件處執行第一條指令,根據指令跳轉到BIOS數據區執行BIOS代碼。BIOS在完成硬體的自檢後,會將操作系統的啟動代碼載入到記憶體。此時CPU還處於實模式,只能定址20位,也就是1MB的記憶體空間(通常處於記憶體空間的低位地址),所以操作系統的啟動代碼需要載入在這1MB的定址空間內。實驗環境下,啟動代碼需載入到記憶體地址0x7C00處,啟動代碼再將CPU從實模式轉成保護模式,得以獲得32位的定址空間(4GB),去載入代碼量龐大的操作系統。理論課視頻截圖較為系統地展示了這個過程,如下圖所示
為什麼不利用BIOS直接載入操作系統?原因是不同操作系統可能擁有不同的文件系統,BIOS無法編寫所有文件系統的解析代碼,所以將載入程式作為操作系統的一部分,讓操作系統可以“定製化”實現自己的載入程式。主引導記錄和活動分區的存在主要是硬碟分區的原因。
中斷建立
中斷源的類型
- 系統調用(system call):應用程式主動向操作系統發出的服務請求
- 異常(exception):非法指令或者其它原因導致指令執行失敗(如:記憶體出錯)後的處理請求
- 中斷(hardware interrupt):來自硬體設備的處理請求
如上圖所示,中斷向量表可以建立起中斷源和服務常式之間的聯繫,在實操課程中中斷向量表的建立也是一項重要的內容。
實驗課的一些知識
在Lab1中,代碼主要分為bootblock
和kernel
兩個部分。bootblock
完成了主引導記錄、活動分區文件系統識別以及載入程式的功能,kernel
則是完成系統內核的功能。
bootblock
從make和makefile來看,bootblock
主要涉及到的源文件有bootmain.c
,bootasm.S
,sign.c
,其中,bootmain.c
,bootasm.S
實現了bootblock
的大部分功能,而sign.c
從代碼上看來,只是將第二個命令行參數(argv[1])的文件指針指向內容拷貝到argv[2]指向的文件,併在文件結尾加入主引導記錄標誌0x55, 0xAA,控制拷貝完的文件大小為512位元組。在make編譯鏈接的過程中,傳入bin/sign
(sign.c生成)的文件為obj/bootblock.out
,輸出的文件為bin/bootblock
,bootblock.out
由bootmain.c
和bootasm.S
生成。
bootasm.S
在實模式下,段寄存器和ip寄存器只提供16位的操作空間。為了定址1MB的記憶體空間,此時段寄存器存儲的基值(16位,base)會左移四位,加上偏移量(IP寄存器,offset)作為邏輯地址。
一開始CPU還處於實模式,段機制還未啟動,但是將CPU轉成保護模式之前需要將重要的段寄存器設置好,轉換前段寄存器DS,ES,SS需置0
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
在進入保護模式之前,需要開啟A20地址線https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html,開啟步驟為
- 等待8042 Input buffer為空;
- 發送Write 8042 Output Port (P2)命令到8042 Input buffer;
- 等待8042 Input buffer為空;
- 將8042 Output Port(P2)得到位元組的第2位置1,然後寫入8042 Input buffer;
對應代碼為
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
接下來載入GDT,
lgdt gdtdesc
...
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
從代碼上看,CPU會先讀取GDT的描述信息,確定GDT的大小(24位元組),再跳轉到gdt的所在地址,執行記憶體分段。實驗環境中,記憶體被分為代碼段和數據段,大小都為4G(0x0~0xffffffff).設置完成之後,進入32位的保護模式,進行相應的準備工作。
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector, $PROT_MODE_DSEG=0x10,ax最高位為1,使ds等段寄存器相應位置1,以支持保護模式
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp # start為bootasm.S入口地址
一切準備工作就緒後,調用C函數bootmain.c
繼續執行以至可以載入操作系統內核call bootmain
,bootmain負責將內核載入到記憶體0x10000處,並檢查是否為合法的ELF文件。
kernel
在Lab1中,提及最多的知識是關於C函數堆棧的內容。見https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_3_3_1_function_stack.html,調試的編程參考於此
Lab1關於中斷的內容主要是實現LDT的初始化。線索從kern/trap/vector.S
開始,其中以彙編的形式記錄了__vectors[].從功能上看,__vectors像是一個函數指針數組,每個數組成員對應某個函數操作的入口。不同類型的中斷向量被觸發之後都會跳轉到kern/trap/trapentry.S
的__alltraps處。目前來講,這些功能使用C語言都能實現類似的效果,但是操作系統在這裡使用了彙編的原因據推斷是這裡需要獲取段寄存器的值,使用彙編會直接很多。__alltraps的目的是輔助C函數trap()的實現。trap的代碼如下:
void
trap(struct trapframe *tf) {
trap_dispatch(tf);
}
# trapframe的結構如下
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
彙編代碼
.text
.globl __alltraps
__alltraps:
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
pushl %esp
call trap
push語句主要是為了模擬C函數的堆棧形式。最先入棧的是C函數的實際參數,該參數為指向trapframe的指針,但在此之前需要將指針指向內容壓棧,可以看到彙編壓棧順序和結構成員聲明相反(說明:結構成員tf_paddingx是作為填充消除編譯器的記憶體對齊問題)。之後調用trap_dispatch,根據中斷號執行不同的中斷常式。
回到LDT的初始化,現在已經大概清楚從中斷向量到觸發中斷常式的過程,但是怎麼聯繫起中斷向量__vectors[]和中斷源呢?這是idt_init需要實現的東西。中斷源用結構體gatedesc描述,再將數組idt[256]對應不同的中斷源。idt_init做的很重要的一件事是怎麼將__vectors[]和idt[]相同下標的gatedesc成員賦值好。最後利用彙編命令lidt載入LDT.
結尾
筆記寫的不是非常嚴謹,此筆記主要功能是提供自己能夠快速回憶操作系統知識。有其他重要知識點就等以後意識到再補充吧