1.打開電源 (1)x86 PC開機時CPU處於實模式,實模式的定址方式是CS:IP (CS左移4位+IP) (2)開機時段寄存器CS=0xFFFF,偏移量IP=0x0000,段寄存器左移4位加上偏移量是實際地址,也就是定址地址為0xFFFF0 (ROM BIOS映射區) (3)檢查RAM,鍵盤,顯 ...
1.打開電源
(1)x86 PC開機時CPU處於實模式,實模式的定址方式是CS:IP (CS左移4位+IP)
(2)開機時段寄存器CS=0xFFFF,偏移量IP=0x0000,段寄存器左移4位加上偏移量是實際地址,也就是定址地址為0xFFFF0 (ROM BIOS映射區)
(3)檢查RAM,鍵盤,顯示器,磁碟
(4)將0磁軌0扇區512個位元組讀入0x7c00處(操作系統的引導扇區)
(5)設置cs=0x7c0,ip=0x0000
2.引導扇區代碼bootsect.s
(1)將bootsect從0x7c00處移動到0x90000處
(2)將setup讀入到0x90200處
(3)將system讀入0x91000處
(4)將PC移動到0x90200處,bootsect結束
1. 移動bootsect.s。讀取0x7c00處的記憶體,首先設置源地址和目標地址,並且將0x7c00記憶體處的256個字(512位元組)移動到了0x90000
start: mov ax, #BOOTSEG mov ds, ax ! 將 ds 段寄存器設置為0x07C0(16位彙編,後面的放入前面) mov ax, #INITSEG mov es, ax ! 將 es 段寄存器設置為0x9000 sub si, si ! 源地址 ds:si 0x7c00 sub di, di ! 目標地址 es:di 0x90000 mov cx, #256 ! 設置移動計數值256字(512位元組) rep movw ! 重覆移動直到cx為0,將bootsect移動到了0x90000處 jmpi go, INITSEG ! 跳轉到0x9000:go處,將設置es、ss、sp都為0x9000(相當於是初始化)
2. 0x13讀磁碟中斷,讀取setup代碼。在磁碟第2個扇區開始,讀取4個扇區的內容,並將這4個扇區的內容寫入到0x90200記憶體處。因為bootsect占用512位元組,所以需要寫在0x90000的512個位元組之後,16位表示就是在0x90200處寫入。
go: mov ax, cs ! 0x9000 ! 將ds、es、ss都置成移動後代碼所在段處0x9000、棧頂地址設置為0x9fff00,來遠離代碼位置 mov ds, ax mov es, ax mov ss, ax mov sp, #0xFF00 load_setup:
// 載入setup模塊, 從磁碟第2個扇區開始讀4個扇區, 寫入0x90200處 mov dx, #0x0000 ! 數據寄存器 mov cx, #0x0002 ! 計數寄存器 ch:cx高8位, 0x00(柱面號0); cl:低8位, 0x02(開始扇區為2) mov bx, #0x0200 ! 基址寄存器 寫到es:bx 0x90200處 mov ax, #0x0200+SETUPLEN ! 累加器 ah: ax高8位, 0x20(讀磁碟); al:ax低8位, 0x04(讀取4個扇區) int 0x13 ! BIOS讀磁碟扇區中斷 jnc ok_load_setup ! 跳轉指令, CF=0則跳轉。如果沒有讀錯,則跳轉到載入setup執行;如果讀錯,則複位驅動器重新讀。 mov dx, #0x0000 ! 對驅動器0進行操作 mov ax, # 0x0000 ! 複位 int 0x13 ! 執行0x13中斷(BIOS讀磁碟扇區的中斷) j load_setup ! 重讀
3. 0x10顯示字元中斷,顯示字元在顯示器上。ok_load_setup,顯示歡迎頁面,調用read_it讀入system,最後將控制權交接給setup模塊,也就是將PC移動到0x90200處,開始執行setup。bootsect正式結束。
ok_load_setup: ! 載入setup,顯示開機文字,讀取system ! 讀取setup,顯示開機文字 mov dl, #0x00 mov ax, #0x0800 ! ah=8,獲得磁碟驅動器的參數 int 0x13 mov ch, #0x00 mov sectors, cx mov ah, #0x03 xor bh, bh int 0x00 ! 讀游標 mov cx, #24 mov bx, #0x0007 ! 7是顯示屬性 mov bp, #msg1 mov ax, #1301 int 0x10 !顯示字元 ! 讀取system模塊到0x10000處 mov ax, #SYSSEG ! SYSSEG=0X1000 mov es, ax call read_it ! 讀入system jmpi 0, SETUPSEG ! 轉入0x9020:0X0000執行setup.s
3.引導扇區代碼setup.s
讀取硬體參數,將操作系統移動到0地址處,進入保護模式,初始化一個很簡單的gdt表,跳轉到system模塊
1. setup主要任務是讀取硬體參數,初始化一個數據結構來管理這些硬體設備,並移動操作系統到記憶體0地址處。其中0x15中斷是讀取擴展記憶體大小(Intel原先記憶體只有1m,我們把1m以外的記憶體稱為擴展記憶體)放在ax寄存器里,之後放到記憶體0x90002處。接下來do_move將0x90000處開始的數據移動到記憶體絕對地址0x00000處。
! 取游標位置,包括其他硬體參數,放到0x90000處 start : mov ax, #INITSEG mov ds, ax mov ah, #0x03 xor bh, bh int 0x10 mov [0], dx ! 游標位置設置為0x90000 mov ah, #0x88 int 0x15 mov [2], ax ... ! 讀取擴展記憶體的大小(1m以外的記憶體稱為擴展記憶體), 放在0x90002處 cli ! 不允許中斷 mov ax, #0x0000, cld ! 設置讀取目標位置和遞增讀取方式 ! 將system模塊從0x10000動到0x00000處 do_mov: mov es, ax add ax, #0x1000 cmp ax, #0x9000 jz end_mov ! 若ax=0x9000,表示移動完成,則跳出 mov ds, ax sub di, di sub si, si ! 設置源地址0x90000和目標地址0x00000 mov cx, #0x8000 ! 設置移動數據的長度 rep movsw ! 將system模塊移動到0地址 jmp do_move
2. 進入保護模式。cs左移4位+ip最大有20位地址,也就是最大記憶體空間只有1M。setup最後一步要從16位機切換到32定址方式。
實模式:CS:IP,CS << 4 + IP,共20位定址空間
保護模式:CS實際上是查表GDT(Global Descriptor Table),地址為CS查表 + IP
mov ax,#0x0001 ! 將0x0001放入ax寄存器 mov cr0,ax ! 將ax放入cr0寄存器,這一步非常重要 ! cr0寄存器最後一位是0的時候是CS:IP實模式 ! cr0寄存器最後一位是1的時候是保護模式,需要走另外一條解釋指令的電路 jmpi 0,8 ! cs:8, ip:0; 這裡實際上跳轉的地址就是0地址
4.head.s
head.s是system中的第一個模塊,負責重新載入各個數據段寄存器,重新設置中斷描述符表,重新設置全局描述符表,設置分頁處理機制(一個頁目錄表和4個頁表),打開20號地址線訪問4G記憶體,並且將main函數壓棧,head執行完後彈出main,轉到main函數。
startup_32: movl $0x10, %eax ! 現在開始是32位彙編,前面的放入後面 mov %ax, %ds mov %ax, %es mov %as, %fs mov %as, %gs ! 指向gdt的0x10數據項 lss _stact_start, %esp ! 設置系統棧 call setup_idt ! 設置中斷描述符表子程式 call setup_gdt ! 設置全局描述符表子程式 mov $0x10, %eax ! 重載所有的段寄存器 ...
5.main函數
main函數中有大量初始化函數,例如初始化硬碟,初始化記憶體,將每4K記憶體作為一個頁等等
6.系統調用
普通函數調用是通過call跳轉對應函數的地址繼續執行
系統調用是調用系統庫中為該系統調用編寫的一個介面函數,叫 API(Application Programming Interface)。API 並不能完成系統調用的真正功能,它要做的是去調用真正的系統調用,過程是:
- 把系統調用的編號存入 EAX
- 把函數參數存入其它通用寄存器
- 觸發 0x80 號中斷(int 0x80)
這裡研究 close()
中的API,以巨集 _syscall1(int, close, int, fd)
為例子,它在 include/unistd.h
中定義了展開的形式
這就是 API 的定義。它先將巨集 __NR_close
存入 EAX,將參數 fd 存入 EBX,然後進行 0x80 中斷調用。調用返回後,從 EAX 取出返回值,存入 __res
,再通過對 __res
的判斷決定傳給 API 的調用者什麼樣的返回值。
其中 __NR_close
在 include/unistd.h
中定義為 6
int close(int fd) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_close),"b" ((long)(fd))); if (__res >= 0) return (int) __res; errno = -__res; return -1; }
在內核初始化時,主函數調用了 sched_init()
初始化函數:
void main(void) { // …… time_init(); sched_init(); buffer_init(buffer_memory_end); // …… }
sched_init()
在 kernel/sched.c
中定義為:
void sched_init(void) { // …… set_system_gate(0x80,&system_call); }
set_system_gate
是個巨集,在 include/asm/system.h
有定義。這段巨集就是填寫 IDT(中斷描述符表),將 system_call
函數地址寫到 0x80
對應的中斷描述符中,也就是在中斷 0x80
發生後,自動調用函數 system_call
下麵是 system_call
。該函數純彙編打造,定義在 kernel/system_call.s
中。system_call
用 .globl
修飾為其他函數可見
call sys_call_table(,%eax,4)
之前是一些壓棧保護,修改段選擇子為內核段,call sys_call_table(,%eax,4)
之後是看看是否需要重新調度,這些都與本實驗沒有直接關係,此處只關心 call sys_call_table(,%eax,4)
這一句。
根據彙編定址方法它實際上是:call sys_call_table + 4 * %eax
,其中 eax 中放的是系統調用號,即 __NR_xxxxxx
。
顯然,sys_call_table
一定是一個函數指針數組的起始地址,它定義在 include/linux/sys.h
中:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...
增加實驗要求的系統調用,需要在這個函數表中增加兩個函數引用 ——sys_iam
和 sys_whoami
。當然該函數在 sys_call_table
數組中的位置必須和 __NR_xxxxxx
的值對應上。
!…… ! # 這是系統調用總數。如果增刪了系統調用,必須做相應修改 nr_system_calls = 72 !…… .globl system_call .align 2 system_call: ! # 檢查系統調用編號是否在合法範圍內 cmpl \$nr_system_calls-1,%eax ja bad_sys_call push %ds push %es push %fs pushl %edx pushl %ecx ! # push %ebx,%ecx,%edx,是傳遞給系統調用的參數 pushl %ebx ! # 讓ds, es指向GDT,內核地址空間 movl $0x10,%edx mov %dx,%ds mov %dx,%es movl $0x17,%edx ! # 讓fs指向LDT,用戶地址空間 mov %dx,%fs call sys_call_table(,%eax,4) ! # 關鍵代碼 pushl %eax movl current,%eax cmpl $0,state(%eax) jne reschedule cmpl $0,counter(%eax) je reschedule