[書]x86彙編語言:從實模式到保護模式 -- 第17章 中斷、任務切換、分頁機制、平坦模型

# 任務切換

內核任務、用戶任務1、用戶任務2,之前的輪詢切換

利用RTC芯片的硬件中斷來實現任務切換

    計算機主板上有實時時鐘芯片RTC,可以設置RTC芯片,使得它每次更新CMOS中的時間信息後,發出更新週期結束的中斷信號0x70;

    編寫0x70號中斷處理程序,操作TCB鏈表,實現任務切換。

    操作TCB鏈表:找到當前任務(即,狀態爲忙的任務)、將該任務從TCB鏈表的當前位置刪除並添加至鏈表末尾、遍歷TCB鏈表找到第一個空閒的任務、重新設置當前任務和所找到的空閒任務的忙碌狀態、跳轉到新任務去執行

備註:主流的操作系統一般都不會用處理器固件來實現任務切換,因爲這種方法代價比較高,可以用軟件算法代替硬件實現任務切換

; -----------------------------------------------------------------
; Function: 實時時鐘中斷處理過程,實現任務切換
;           利用硬件中斷實現任務切換
; Input: 
handler_interrupt_rtm_0x70:
; 計算機主板上有實時時鐘芯片RTC,可以設置RTC芯片,使得它每次更新CMOS中的時間信息後,發出更新週期結束的中斷信號,從而進行任務切換
; 其實,用實時時鐘更新週期結束中斷來實施任務切換並不是一個好主意,和別的中斷相比,它更囉嗦

    pushad
    
    ; 向8259A芯片發送中斷結束命令EOI(End of Interrupt)
    ; 否則,它不會再向處理器發送另一箇中斷通知
    mov al, 0x20    
    out 0xa0, al    ; 向8259A從片發送
    out 0x20, al    ; 向8259A主片發送
    
    ; 必須讀一下CMOS芯片內的寄存器C,使它復位一下, 才能使RTC產生下一個中斷信號。否則,它只產生一次中斷信號
    mov al, 0x0c    ; 寄存器C的索引,且放開NMI
    out 0x70, al
    in al, 0x71     ; 讀一下RTC的寄存器C
    
    ; 找到當前任務(狀態爲忙的任務)在鏈表中的位置,即狀態值爲0xFFFF的節點
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    or ebx, ebx
    jz .irtn    ; 鏈表爲空,或已到末尾,從中斷返回
    cmp word [ebx+0x04], 0xffff     ; 判斷狀態是否爲忙,即是否爲當前任務
    je .move_current_to_chaintail
    mov eax, ebx ; TCB內偏移爲0x00處,是下一個TCB的線性地址
    jmp .search_current_task
    
    ; 把當前任務(狀態爲忙的任務)移到鏈尾
 .move_current_to_chaintail:
    ; 從鏈表中刪除該節點
    mov ecx, [ebx]  ; 下一個TCB節點的線性地址
    mov [eax], ecx  ; 將當前任務從TCB鏈表中刪除
 .goto_chaintail:
    ; 遍歷至鏈表尾部
    mov edx, [eax]
    or edx, edx     ; 判斷是否已到鏈表尾部
    jz .add_current_to_chaintail
    mov eax, edx
    jmp .goto_chaintail
 .add_current_to_chaintail:
    ; 將該節點添加至鏈表尾部
    mov [eax], ebx
    mov dword [ebx], 0 ; 將當前任務標記爲鏈尾。TCB內偏移爲0處,是下一個TCB的線性地址
    
    ; 從TCB鏈表中搜索第一個空閒任務
    mov eax, tcb_chain_head
 .search_next_task:
    mov eax, [eax]
    or eax, eax     ; 已到鏈尾,即未發現空閒任務
    jz .irtn        ; 從中端返回
    cmp word [eax+0x04], 0x0000     ; 是空閒任務。空閒爲0,忙爲0xffff
    jnz .search_next_task
    
    ; 將空閒任務和當前任務的狀態都取反
    not word [eax+0x04]     ; 設置空閒任務的狀態爲忙
    not word [ebx+0x04]     ; 設置當前任務的狀態爲空閒
    
    ; 任務切換
    jmp far [eax+0x14] ; TCB內偏移爲0x14處,依次是該任務TSS的線性地址和TSS描述符選擇子

 .irtn:
    popad   ; 當前任務再次獲得執行權時的返回點
    iretd   ; 從中斷返回,正常執行任務的其他代碼

# 執行結果

內核任務、用戶任務1、用戶任務2,這三個任務不斷地切換執行。

# file_01: c17_mbr.asm    主引導程序代碼,Mast Boot Record

; FILE: c17_mbr.asm
; TITLE: 硬盤主引導扇區代碼
; DATE: 20200206 

; 內核程序的大小是不確定的,但可以規定它的起始位置
core_base_address   equ 0x00040000  ; 自定義mini內核加載的起始內存地址,即64KB
core_begin_sector   equ 1           ; 自定義mini內核的起始邏輯扇區號

; ===============================================================================
SECTION mbr vstart=0x7c00   ; 主引導程序的加載位置是物理地址0x7c00
; 如果沒有vstart子句,所有標號的地址都以程序開頭爲基準,從0開始計算
; 有vstart子句時,標號所代表的地址就以程序開頭爲基準,從給定的虛擬地址開始計算

; 設置堆棧段和指針
; 在32位處理器上,即使是在實模式下,也可以使用32位寄存器
; mov ax, cs
; mov ss, ax
mov eax, cs
mov ss, eax
mov sp, 0x7c00

; 計算GDT所在的邏輯段地址
; 從標號gdt_base處取得自定義的GDT物理地址,並計算它在實模式下的段地址和偏移地址
; 64位的被除數在edx:eax中,商爲eax,餘數爲edx
mov eax, [cs:gdt_base]
xor edx, edx
mov ebx, 16
div ebx
mov ds, eax         ; 商eax爲段地址,僅低16位有效
mov ebx, edx        ; 餘數edx爲偏移地址,僅低16位有效


; 進入保護模式之前,初始化程序(mbr主引導程序)需在gdt中安裝必要的描述符
; =====================================================================

; 創建gdt第#0號描述符
; 處理器規定,gdt中第一個描述符必須是空描述符
; 這2行代碼也可不寫
mov dword [ebx], 0x00000000
mov dword [ebx+0x04], 0

; 創建gdt第#1號描述符,保護模式下的代碼段描述符
; 該段:線性基地址爲0,段界限爲0xF FFFF,DPL=00
;       粒度爲4KB,向上擴展
mov dword [ebx+0x08], 0x0000_ffff     
mov dword [ebx+0x0c], 0x00cf_9800

; 創建gdt第#2號描述符,保護模式下的數據段和堆棧段描述符
; 該段:線性基地址爲0,段界限爲0xF FFFF,DPL=00
;       粒度爲4KB,向上擴展
mov dword [ebx+0x10], 0x0000_ffff     
mov dword [ebx+0x14], 0x00cf_9200

; 初始化描述符表寄存器GDTR
; 描述符表的界限(總字節數減1)。這裏共有5個描述符,每個描述符8字節,共40字節
mov word [cs:gdt_size], 23


; lgdt指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
; GDTR, 全局描述符表寄存器
lgdt [cs:gdt_size]

; 打開地址線A20
; 芯片ICH的處理器接口部分,有一個用於兼容老式設備的端口0x92,端口0x92的位1用於控制A20 
in al, 0x92
or al, 0000_0010B
out 0x92, al

; 禁止中斷,中斷機制尚未工作
; 保護模式和實模式下的中斷機制不同,在重新設置保護模式下的中斷環境之前,必須關中斷
cli

; 開啓保護模式
; CR0的第1位(位0)是保護模式允許位(Protection Enabel, PE)
mov eax, cr0
or eax, 1
mov cr0, eax

; 清空流水線、重新加載段選擇器
; 處理器建議,在進入保護模式後,執行的第一條指令應當是跳轉或者過程調用指令,以清空流水線和亂序執行的結果,並串行化處理器。
; 遇到jmp或call指令,處理器一般會清空流水線,另一方面,還會重新加載段選擇器,並刷新描述符高速緩存器中的內容
; 建議:在設置了控制寄存器CR0的PE位之後,立即用jmp或call指令
jmp dword 0x0008:flush      ; 16位的描述符選擇子:32位偏移
                            ; 不管是16位還是32位遠轉移,現在已經處於保護模式下,
                            ; 處理器將把第一個操作數0x0008視爲段選擇子,而不是是模式下的邏輯段地址
                            ; 段選擇子0x0008,即 0000_0000_00001_0_00(RPL爲00,TI爲0,索引號爲1)
                            ; 當指令執行時,處理器加載CS,從GDT中取出相應的描述符加載到CS描述符高速緩存
                            ; jmp dword, 32位的遠轉移指令。
                            ; 16位的絕對遠轉移指令只有5字節,使用16位的偏移量,它會使標號flush的彙編地址相應地變小
                            
[bits 32]       ; 從進入保護模式開始,之後的指令都應當按32位操作數方式編譯
                ; 當處理器執行到這裏時,它會按32位模式進行譯碼

flush:
    ; 段選擇子:15~3位,描述符索引;2, TI(0爲GDT,1爲LDT); 1~0位,RPL(特權級)
    ; mov eax, 0x0010
    mov eax, 0000_0000_00010_0_00B   ; 已初始化的GDT中,數據段爲第2號描述符
    ; 因爲平坦模式下所有段都指向4GB數據段,只不過棧段向下增長,其他各段都向上增長
    mov ds, eax  ; 當處理器執行任何改變段選擇器的指令時,就將指令中提供的索引號乘以8作爲偏移地址,同GDTR中提供的線性地址相加,
                 ; 以訪問GDT,將找到的描述符加載到不可見的描述符高速緩存部分

    ; 設置堆棧段ss和段指針esp
    mov es, eax
    mov fs, eax
    mov gs, eax
    mov ss, eax
    mov esp, 0x7000     ; 棧從地址0x7000開始向低地址方向擴展
    

    ; 加載mini內核
    ; 從硬盤把內核程序讀入內存
    mov edi, core_base_address      ; 自定義的mini內核物理內存地址
    mov eax, core_begin_sector      ; 自定義的mini內核在硬盤上的起始邏輯扇區號
    mov ebx, edi
    
    call read_hard_disk_0           ; 先讀一個扇區
                                    ; 包含了頭部信息:程序大小、入口點、段重定位表
    
    ; 判斷需要加載的整個程序有多大
    mov eax, [edi]                  ; 0x00, 應用程序的頭部包含了程序大小
    xor edx, edx
    mov ecx, 512
    div ecx
    cmp edx, 0               
    jnz @1
    dec eax                         ; 餘數edx爲0則商eax減1,已讀取一個扇區    
    
 @1:
    cmp eax, 0
    jz setup_page           ; 實際長度小於512字節,則已讀取完
    
    ; 讀取剩餘的扇區    
    mov ecx, eax            ; 循環次數(剩餘扇區數)
    mov eax, core_begin_sector
    inc eax                 ; 讀取下一個邏輯扇區
 @2:    
    add ebx, 512            ; 每次讀時,指向物理內存的下一個512字節
    call read_hard_disk_0
    inc eax                 ; 下一個扇區
    loop @2
    
setup_page:
;準備打開分頁機制。從此,再也不用在段之間轉來轉去
    
    ; 創建系統內核的頁目錄表PDT
    mov ebx, 0x2_0000   ; 自定義頁目錄表的物理地址爲0x20000,即128KB
    
    ; 創建PDT的第1023號表項,指向頁目錄表本身0x20000
    ; 令最後一個頁目錄項指向頁目錄表自己,便於修改頁目錄表本身
    ; 0x0002_0003, 前20位是物理地址的高20位;P=1,頁位於內存中;RW=1,該目錄項指向的頁表可讀可寫;
    ;   US位爲1,此目錄項指向的頁表不允許特權級爲3的程序和任務訪問
    mov dword [ebx+4092], 0x2_0003  ; 索引1023*每個表項4字節,得偏移
    
    ; 創建PDT的第0號表項,指向系統內核的頁表0x21000
    ; 這個目錄項只在開啓頁功能得時候使用,作爲臨時過渡
    ; 後續使用全局地址空間,即高2GB指向內核
    mov dword [ebx+0], 0x2_1003
    
    ; 創建與線性地址0x8000_0000對應的目錄項
    ; 作爲高2GB的全局空間指向內核
    mov dword [ebx+0x800], 0x2_1003 ; 0x800/4 * 1024*4KB = 0x800 00000, 即2GB
    
    
    ; 初始化內核頁表0x21000
    ; 將內存低端1MB所包含的那些頁的物理地址按順序一個一個地填寫到頁表中
    ; 這裏的mini內核佔用着內存的低端1MB,即256個頁表項
    mov ebx, 0x2_1000       ; 頁表的物理地址0x21000
    xor eax, eax            ; 起始頁的物理地址0x0
    xor esi, esi
 .pt_kernel_pre256:
    mov edx, eax
    or edx, 0x0000_0003     ; 低12位爲頁屬性
                            ; 屬性值3,P=1, RW=1, US=0
    
    mov [ebx+esi*4], edx    ; 在頁表中登記頁的物理地址
    add eax, 0x1000         ; 下一個相鄰頁的物理地址,每個頁4KB
    inc esi                 ; 頁表的下一個頁表項
    cmp esi, 256
    jl .pt_kernel_pre256    
    
    ; 將上面內核頁表的其餘頁表項置爲無效
 .pdt_1_others:
    mov dword [ebx+esi*4], 0    ; 頁表項內容爲0,即爲無效表項
    inc esi
    cmp esi, 1024               ; 每個頁表有1024個頁表項
    jl .pdt_1_others
    
    ; 令CR3寄存器指向頁目錄
    ; CR3寄存器的低12位除了用於控制高速緩存的PCD和PWT位,都沒有使用
    mov eax, 0x2_0000       ; PCD=PWT=0    
    mov cr3, eax

    ; 將GDT中的段描述符映射到線性地址0x8000_0000,即內核全局空間
    sgdt [gdt_size]
    mov ebx, [gdt_base]
    add dword [gdt_base], 0x8000_0000 ; 全局描述符表寄存器GDTR也用的是線性地址
    lgdt [gdt_size] ; 將修改後的GDT基地址和界限值加載到GDTR
    
    ; 開啓分頁機制
    ; 從此,段部件產生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址
    mov eax, cr0
    or eax, 0x8000_0000
    mov cr0, eax
    ; 這裏切換至分頁模式後,不需要重新加載各個段寄存器以刷新它們的描述符高速緩存器,因爲所有這些段都是4GB的

; 將內核棧映射到高端,這是非常容易被忽略的一件事。應當把內核的所有東西都移到高2GB
; 否則,一定會和正在加載的用戶任務局部空間裏的內容衝突,而且很難想到問題會出在這裏
    add esp, 0x8000_0000
    
    ; 跳轉到內核
    ; 內核已從硬盤上加載,線性地址爲0x8004_0000
    ; 從線性地址0x8004_0004處取得一個32位段內偏移,傳送到eip寄存器
    jmp [0x8004_0004]   ; 32位段內轉移
                        ; 0x04, 內核頭部包含了mini內核入口點地址
                        

    
    
    
; ===============================================================================    
; Function: 讀取主硬盤的1個邏輯扇區
; Input: 1) eax 起始邏輯扇區號 2) ds:ebx 目標緩衝區地址
read_hard_disk_0:

    push eax
    push ebx
    push ecx
    push edx                ; 保護現場

    push eax                ; 這裏後面要用到
    ; 1) 設置要讀取的扇區數
    ; ==========================
    ; 向0x1f2端口寫入要讀取的扇區數。每讀取一個扇區,數值會減1;
    ; 若讀寫過程中發生錯誤,該端口包含着尚未讀取的扇區數
    mov dx, 0x1f2           ; 0x1f2爲8位端口
    mov al, 1               ; 1個扇區
    out dx, al
    
    ; 2) 設置起始扇區號
    ; ===========================
    ; 扇區的讀寫是連續的。這裏採用早期的LBA28邏輯扇區編址方法,
    ; 28個比特表示邏輯扇區號,每個扇區512字節,所以LBA25可管理128G的硬盤
    ; 28位的扇區號分成4段,分別寫入端口0x1f3 0x1f4 0x1f5 0x1f6,都是8位端口
    inc dx                  ; 0x1f3
    pop eax
    out dx, al              ; LBA地址7~0
    
    inc dx                  ; 0x1f4
    mov cl, 8
    shr eax, cl
    out dx, al              ; in和out 操作寄存器只能是al或者ax
                            ; LBA地址15~8
                            
    inc dx                  ; 0x1f5
    shr eax, cl
    out dx, al              ; LBA地址23~16

    ; 8bits端口0x1f6,低4位存放28位邏輯扇區號的24~27位;
    ; 第4位指示硬盤號,0爲主盤,1爲從盤;高3位,111表示LBA模式
    inc dx                  ; 0x1f6
    shr eax, cl             
    or al, 0xe0             ; al 高4位設爲 1110
                            ; al 低4位設爲 LBA的的高4位
    out dx, al

    ; 3) 請求讀硬盤
    ; ==========================
    ; 向端口寫入0x20,請求硬盤讀
    inc dx                  ; 0x1f7
    mov al, 0x20
    out dx, al
    
 .wait:
    ; 4) 等待硬盤讀寫操作完成
    ; ===========================
    ; 端口0x1f7既是命令端口,又是狀態端口
    ; 通過這個端口發送讀寫命令之後,硬盤就忙乎開了。
    ; 0x1f7端口第7位,1爲忙,0忙完了同時將第3位置1表示準備好了,
    ; 即0x08時,主機可以發送或接收數據
    in al, dx               ; 0x1f7
    and al, 0x88            ; 取第8位和第3位
    cmp al, 0x08            
    jnz .wait
    
    ; 5) 連續取出數據
    ; ============================
    ; 0x1f0是硬盤接口的數據端口,16bits
    mov ecx, 256             ; loop循環次數,每次讀取2bytes
    mov dx, 0x1f0           ; 0x1f0
 .readw:
    in ax, dx
    mov [ebx], ax
    add ebx, 2
    loop .readw
    
    pop edx
    pop ecx
    pop ebx
    pop eax
    
    ret



; lgdt指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0x00008000


times 510-($-$$) db 0
                 db 0x55, 0xaa

# file_02: c17_core.asm    內核代碼

; FILE: c17_core.asm
; TITLE: mini內核
; DATE: 20200207

; 常數的定義僅在編譯期間有用,編譯之後不佔用任何地址空間
; 段選擇子:15~3位,描述符索引;2, TI(0爲GDT,1爲LDT); 1~0位,RPL(特權級)
sel_code_4gb_seg        equ 0x08 ; gdt第1號描述符
sel_data_4gb_seg        equ 0x10 ; gdt第2號描述符,未使用到

idt_linear_base     equ 0x8001_f000     ; 中斷描述符表的線性基地址

; -----------------------------------------------------------------
; 定義宏

; 在內核空間中分配虛擬內存
%macro allocate_memory_kernel 0
    mov ebx, [kernel_tcb+0x06]  ; 0x06, TCB中包含下一個可分配的線性地址
    add dword [kernel_tcb+0x06], 0x1000 ; 寫回下一個可分配的線性地址。每次分配一個頁(4KB)
    call sel_code_4gb_seg:allocate_install_memory_page
%endmacro

; 在用戶空間中分配虛擬內存
%macro allocate_memory_user 0
    mov ebx, [esi+0x06]  ; 0x06, TCB中包含下一個可分配的線性地址
    add dword [esi+0x06], 0x1000 ; 寫回下一個可分配的線性地址。每次分配一個頁(4KB)
    call sel_code_4gb_seg:allocate_install_memory_page
%endmacro




; ===============================================================================
[bits 32]


; ===============================================================================
SECTION core_code vstart=0x8004_0000               ; mini內核代碼

; mini內核的頭部,用於mbr加載mini內核
core_length         dd core_end                     ; mini內核總長度, 0x00
core_entry          dd beginning                    ; mini內核入口點(32位的段內偏移地址),0x04    


beginning:

; 創建保護模式下的中斷系統
; 保護模式下的中斷機制不同於實模式,因此,在進入保護模式之前,已經用cli指令關掉了外部硬件中斷,以免出現問題
; 只有在創建好了中斷描述符表,並安裝了中斷處理程序之後,才能使用sti指令放開硬件中斷


    ; 前20個向量是處理器異常使用的    
    ; 創建函數handler_exception_general的中斷門描述符
    mov eax, handler_exception_general  ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段選擇子
    mov cx, 0x8e00      ; 屬性值,32位的中斷門,特權級爲0
    call sel_code_4gb_seg:make_gate_descriptor
    ; 這裏還是採用遠調用,函數也是用retf返回
    ; 但該函數其實可以定義成用ret指令返回的近過程,因爲內核不會允許3特權級的用戶任務調用該過程
    
    ; 在IDT中安裝前20個描述符,都指向通用異常處理程序handler_exception_general
    mov ebx, idt_linear_base
    xor esi, esi
 .idt0_19:
    mov [ebx+esi*8], eax
    mov [ebx+esi*8+4], edx
    inc esi
    cmp esi, 19
    jle .idt0_19

    ; 20~255中斷向量是Intel保留的中斷向量,以及外部硬件中斷
    ; 創建函數handler_interrupt_general的中斷門描述符    
    mov eax, handler_interrupt_general  ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段選擇子
    mov cx, 0x8e00      ; 屬性值,32位的中斷門,特權級爲0
    call sel_code_4gb_seg:make_gate_descriptor

    ; 在IDT中安裝0~255描述符,都指向通用中斷處理程序handler_interrupt_general
    mov ebx, idt_linear_base
 .idt20_255:
    mov [ebx+esi*8], eax
    mov [ebx+esi*8+4], edx
    inc esi
    cmp esi, 255
    jle .idt20_255

    ; 設置實時時鐘中斷處理過程,用於
    ; 創建函數handler_interrupt_rtm_0x70的中斷門描述符    
    mov eax, handler_interrupt_rtm_0x70 ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段選擇子
    mov cx, 0x8e00      ; 屬性值,32位的中斷門,特權級爲0
    call sel_code_4gb_seg:make_gate_descriptor    

    ; 在IDT中安裝0x70號中斷,指向實時時鐘中斷處理過程handler_interrupt_rtm_0x70
    mov ebx, idt_linear_base
    mov [ebx+0x70*8], eax
    mov [ebx+0x70*8+4], edx
    
    ; 中斷描述符表寄存器IDTR
    ; 將中斷描述符表的基地址和界限值加載到IDTR
    ; 一旦設置了IDT,並加載了IDTR,處理器的中斷機制就開始起作用了。但現在還沒有放開硬件中斷
    mov word [idt_size], 256*8-1
    mov dword [idt_base], idt_linear_base
    lidt [idt_size]     ; Load interrupt descriptor table register
    
    ; 重新初始化中斷控制器芯片8259A
    ; 在保護模式下,需要重新初始化8259A,否則其主片的中斷向量和處理器的異常向量衝突
    ; 計算機啓動之後,主片的中斷向量爲0x08~0x0F, 從片爲0x70~0x77。但在32位處理器上,0x08~0x0F已被處理器用做異常向量
    ; 8259A是可編程的,允許重新設置中斷向量
    
    ; 對8259A編程需要使用初始化命令字ICW,以設置其工作方式
    mov al, 0x11
    out 0x20, al ; ICW1設置中斷請求的觸發方式,邊沿觸發/級聯方式
    mov al, 0x20
    out 0x21, al ; ICW2設置每個芯片的中斷向量,起始中斷向量
    mov al, 0x04
    out 0x21, al ; ICW3指定用哪個引腳實現芯片的級聯,從片級聯到IR2
    mov al, 0x01
    out 0x21, al ; ICW4控制芯片的工作方式: 非總線緩衝 全嵌套 正常EOI    
    
    ; 設置和主片相連的從片
    mov al, 0x11
    out 0xa0, al ; ICW1:邊沿觸發/級聯方式
    mov al, 0x70
    out 0xa1, al ; ICW2:起始中斷向量
    mov al, 0x04
    out 0xa1, al ; ICW3:從片級聯到IR2
    mov al, 0x01
    out 0xa1, al ; ICW4:非總線緩衝,全嵌套,正常EOI

    ; 設置和時鐘中斷相關的硬件狀態,包括RTC、8259A
    mov al, 0x0b    ; RTC寄存器B
    or al, 0x80     ; 阻斷NMI
    out 0x70, al
    mov al, 0x12    ; 設置寄存器B,禁止週期性中斷,開放更新結束後中斷,BCD碼,24小時制
    out 0x71, al

    in al, 0xa1     ; 讀8259從片的IMR寄存器
    and al, 0xfe    ; 清除bit 0(此位連接RTC)
    out 0xa1, al    ; 寫回此寄存器

    mov al, 0x0c
    out 0x70, al
    in al, 0x71     ; 讀RTC寄存器C,復位未決的中斷狀態    

    ; 放開硬件中斷
    sti     ; 用sti指令設置EFLAGS寄存器的IF位

    ; 顯示提示信息,內核已工作在保護和分頁模式
    mov ebx, message_kernel_at_protect_page_mode
    call sel_code_4gb_seg:show_string
    
    ; 獲取處理器品牌信息
    mov eax, 0          ; 先用0號功能探測處理器最大能支持的功能號
    cpuid               ; 會在eax中返回最大可支持的功能號
    
    ; 要返回處理器品牌信息,需使用0x80000002~0x80000004號功能,分3次進行
    mov eax, 0x80000002
    cpuid
    mov [cpu_brand], eax
    mov [cpu_brand+0x04], ebx
    mov [cpu_brand+0x08], ecx
    mov [cpu_brand+0x0c], edx
    
    mov eax, 0x80000003
    cpuid
    mov [cpu_brand+0x10], eax
    mov [cpu_brand+0x14], ebx
    mov [cpu_brand+0x18], ecx
    mov [cpu_brand+0x1c], edx

    mov eax, 0x80000004
    cpuid
    mov [cpu_brand+0x20], eax
    mov [cpu_brand+0x24], ebx
    mov [cpu_brand+0x28], ecx
    mov [cpu_brand+0x2c], edx
    
    ; 顯示處理器品牌信息
    mov ebx, cpu_brand0         ; 空行
    call sel_code_4gb_seg:show_string
    mov ebx, cpu_brand          ; 處理器品牌信息
    call sel_code_4gb_seg:show_string
    mov ebx, cpu_brand1         ; 空行
    call sel_code_4gb_seg:show_string
        
    ; 在全局表述符表GDT中安裝整個系統服務的調用門
    ; 特權級之間的控制轉移必須使用門
    mov edi, sys_api
    mov ecx, sys_api_items
 .make_call_gate:
    push ecx
    
    mov eax, [edi+256]          ; 該sys_api入口點的32位偏移地址
    mov bx, [edi+260]           ; 該sys_api所在代碼段的選擇子
    mov cx, 1_11_0_1100_000_00000B  ; 調用門屬性:P=1 DPL=3 參數數量=0
                                    ; 3以上的特權級才允許訪問
    call sel_code_4gb_seg:make_gate_descriptor   ; 創建調用門描述符
    call sel_code_4gb_seg:setup_gdt_descriptor   ; 將調用門描述符寫入gdt
    mov [edi+260], cx               ; 將門描述符的選擇子(即調用門選擇子)寫回

    add edi, sys_api_item_length    ; 指向下一個sys_api條目
    pop ecx
    loop .make_call_gate

    ; 測試一下剛安裝好的調用門
    ; 顯示字符串
    mov ebx, message_callgate_mount_succ
    call far [sys_api_1+256]        ; 取得32位偏移地址 和16位的段選擇子
                                    ; 處理器會檢查選擇子是調用門的描述符還是普通的段描述符

    ; 不通過調用門,以傳統方式調用系統api
    ; 顯示提示信息,開始加載用戶程序
    ; mov ebx, message_app_load_begin
    ; call sel_code_4gb_seg:show_string
    
    
    ; 初始化程序管理器任務的任務控制塊TCB
    mov word [kernel_tcb+0x04], 0xffff  ; 設置任務的狀態值,0xffff(忙)
    mov dword [kernel_tcb+0x06], 0x8010_0000 ; 自定義可分配的起始地址。內核虛擬空間的分配從這裏開始
    mov word [kernel_tcb+0x0a], 0xffff ; 設置該任務LDT的初識界限值。內核任務沒有LDT,這個值是用不上的
    
    mov ecx, kernel_tcb
    call append_to_tcb_link     ; 將該任務的TCB添加到TCB鏈表中
    
    ; 爲程序管理器任務的TSS分配內存空間
    allocate_memory_kernel  ; 宏,在內核空間中分配虛擬內存
    
    ; 在程序管理器的TSS中設置必要的項目
    mov eax, cr3
    mov dword [ebx+28], eax  ; 登記CR3(PDBR)
    mov word [ebx+0], 0          ; 反向鏈=0
    mov word [ebx+96], 0         ; 沒有LDT。這裏是將所有的段描述符安裝在GDT中
    mov word [ebx+100], 0        ; T=0。不需要0 1 2特權級堆棧,0特權級不會向低特權級轉移控制
    ; 登記I/O許可位映射區的地址
    ; 在這裏填寫的是TSS段界限(103),表明不存在該區域
    mov word [ebx+102], 103      ; 沒有I/O位圖。事實上0特權級不需要

    ; 創建TSS描述符,並安裝到GDT中
    mov eax, ebx        ; 起始地址
    mov ebx, 103        ; 段界限
    mov ecx, 0x0040_8900; 段屬性,特權級0
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    ; mov [prgman_tss+0x04], cx       ; 保存TSS描述符選擇子
    mov [kernel_tcb+0x18], cx   ; 登記內核任務的TSS選擇子到其TCB    
    

    ; 將當前任務的TSS選擇子傳送到任務寄存器TR
    ; TR中的內容是任務存在的標誌,表明當前任務是誰,表明當前任務正在執行中
    ; 執行這條指令後,處理器用該選擇子訪問GDT,找到相對應地TSS,將其B位置1,表示該任務正在執行中
    ; 同時,還將該描述符傳送到TR寄存器的描述符高速緩存器中
    ltr cx  ; 任務寄存器TR

    ; 現在可認爲"程序管理器"任務正在執行中    
    
    
    ; 創建用戶任務的任務控制塊TCB
    allocate_memory_kernel    ; 宏,在內核空間中分配虛擬內存
    
    mov word [ebx+0x04], 0  ; 設置任務的狀態值,0(空閒)
    mov dword [ebx+0x06], 0 ; 自定義可分配的起始地址。用戶任務局部空間的分配從0開始
    mov word [ebx+0x0a], 0xffff ; 設置該任務LDT的初始界限值    
    
    push dword 50   ; 用戶程序位於邏輯50扇區
    push ebx        ; TCB的起始線性地址
    call load_relocate_program
    mov ecx, ebx
    call append_to_tcb_link     ; 將該任務的TCB添加到TCB鏈表中
    ; 當中斷產生時,會切換到剛纔創建的那個用戶任務去執行
    
    
    ; 創建用戶任務的任務控制塊TCB
    allocate_memory_kernel    ; 宏,在內核空間中分配虛擬內存
    
    mov word [ebx+0x04], 0  ; 設置任務的狀態值,0(空閒)
    mov dword [ebx+0x06], 0 ; 自定義可分配的起始地址。用戶任務局部空間的分配從0開始
    mov word [ebx+0x0a], 0xffff ; 設置該任務LDT的初始界限值    
    
    push dword 100   ; 用戶程序位於邏輯100扇區
    push ebx        ; TCB的起始線性地址
    call load_relocate_program
    mov ecx, ebx
    call append_to_tcb_link     ; 將該任務的TCB添加到TCB鏈表中    

 .kernel_task:
    ; 這裏是內核任務,每次得到處理器控制權時,顯示message
    mov ebx, message_kernel_task
    call sel_code_4gb_seg:show_string
    
    ; 這裏可以編寫回收已終止任務內存的代碼
    
    jmp .kernel_task
    

    







; -----------------------------------------------------------------
; Function: 加載並重定位用戶程序
; Input: PUSH app起始邏輯扇區號; PUSH app任務控制塊TCB線性地址
load_relocate_program:
    
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    ; 此時堆棧中,從下往上依次是:8個通用寄存器、EIP、TCB基地址、邏輯扇區號
    
    mov ebp, esp    ; 棧基址寄存器, 爲訪問通過堆棧傳遞的參數做準備

    ; 清空當前頁目錄的前半部分(對應低2GB的局部地址空間),頁目錄表的前512個目錄項
    ; 後半部分是由內核使用的,內核的虛擬地址空間倍映射在每個任務的高地址段,0x8000_0000之後(對應高2GB的全局地址空間)
    mov ebx, 0xffff_f000 ; 當前頁目錄表的起始地址
                         ; 線性地址高20位爲0xfffff時,訪問的就是頁目錄表自己
    xor esi, esi
 .clear_pdt_pre2gb:
    mov dword [ebx+esi*4], 0
    inc esi
    cmp esi, 512
    jl .clear_pdt_pre2gb
    
    ; 刷新快表TLB
    ; TLB是軟件不可直接訪問的。但,將CR3寄存器的內容讀出,再原樣寫入,就會使得TLB中的所有條目失效
    ; 本程序起碼創建了2個用戶任務,如果不刷新TLB,是不行的。
    mov eax, cr3
    mov cr3, eax
    
    ; 分配內存並加載用戶程序

    ; 開始加載用戶程序
    ; 先讀取一個扇區
    mov eax, [ebp+10*4]                 ; 從堆棧中取得用戶程序所在硬盤的起始邏輯扇區號 
    mov ebx, core_buf                   ; 自定義的一段內核緩衝區
                                        ; 在內核中開闢出一段固定的空間,有便於分析、加工和中轉數據
    call sel_code_4gb_seg:read_hard_disk_0  ; 先讀一個扇區
                                              ; 包含了頭部信息:程序大小、入口點、段重定位表
                                              
    ; 判斷需要加載的整個程序有多大
    mov eax, [core_buf]             ; 0x00, 應用程序的頭部包含了程序大小
    mov ebx, eax
    ; and ebx, 0xfffffe00             ; 能被512整除的數,其低9位都爲0
                                    ; 將低9位清零,等於是去掉那些不足512字節的零頭

    ; add ebx, 512                    ; 加上512,等於是將那些零頭湊整
    ; test eax, 0x000001ff            ; 判斷程序大小是否恰好爲512的倍數
    and ebx, 0xffff_f000    ; 使之4KB對齊,按頁進行內存分配
    add ebx, 0x1000
    test eax, 0x0000_0fff
    cmovnz eax, ebx                 ; 條件傳送指令,nz 不爲零則傳送
                                    ; 爲零,則不傳送,依然採用用戶程序原本的長度值eax

    ; 分配內存頁,並將用戶程序加載至內存
    ; 外循環每次分配一個4KB內存頁,內循環每次讀取8個扇區(8*512)
    ; 分頁機制下,內存是先登記,後使用的。
    mov ecx, eax    ; 需要申請的內存大小
    shr ecx, 12     ; 除以4096得到需要的4KB頁數,即循環分配的次數
    
    mov eax, [ebp+10*4]     ; 起始扇區號
    mov esi, [ebp+9*4]     ; 從堆棧中取得TCB的基地址
 .loop_allocate_install_memory_page:
    allocate_memory_user    ; 宏,在用戶空間中分配虛擬內存
    
    push ecx
    mov ecx, 8  ; 內循環每次讀取8個扇區(8*512)
 .loop_read_hard_disk:
    call sel_code_4gb_seg:read_hard_disk_0
    inc eax
    add ebx, 512
    loop .loop_read_hard_disk       ; 循環讀
    
    pop ecx
    loop .loop_allocate_install_memory_page


    ; 創建用戶任務的任務狀態段TSS
    ; 任務是由內核管理的,TSS必須在內核的虛擬地址空間中創建
    allocate_memory_kernel      ; 宏,在內核空間中分配虛擬內存
                                ; 用戶任務的TSS必須在全局空間上分配
    
    mov [esi+0x14], ebx          ; 在TCB中填寫TSS的線性地址
    mov word [esi+0x12], 103     ; 在TCB中填寫TSS的界限值
    ; TSS界限值必須至少是103,任何小於該值的TSS,在執行任務切換時,都會引發處理器異常中斷

    ; 創建用戶任務的局部描述符表LDT
    ; LDT是任務私有的,要在它自己的虛擬地址空間裏分配,其基地址要登記到TCB中
    allocate_memory_user
    mov [esi+0x0c], ebx  ; 在TCB中填寫LDT線性地址

    ; 創建用戶任務的代碼段描述符
    mov eax, 0              ; 段基地址
    mov ebx, 0x000f_ffff    ; 段界限值,粒度爲4KB
    mov ecx, 0x00c0_f800    ; 段屬性,特權級3
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi                    ; TCB基地址
    call setup_ldt_descriptor       ; 寫入ldt    
    or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3    
    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [ebx+76], cx   ; 填寫TSS的CS域

    ; 創建用戶任務的數據段描述符
    mov eax, 0              ; 段基地址
    mov ebx, 0x000f_ffff    ; 段界限值,粒度爲4KB
    mov ecx, 0x00c0_f200    ; 段屬性,特權級3
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi                    ; TCB基地址
    call setup_ldt_descriptor       ; 寫入ldt    
    or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3
    
    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [es:ebx+84], cx     ; 填寫TSS的DS域
    ; 平坦模型下,段寄存器DS ES FS GS都指向同一個4GB數據段
    mov [ebx+72], cx     ; 填寫TSS的ES域
    mov [ebx+88], cx     ; 填寫TSS的FS域
    mov [ebx+92], cx     ; 填寫TSS的GS域

    ; 創建用戶任務的堆棧段
    ; 將數據段作爲用戶任務的3特權級固有堆棧
    ; 分配4KB內存
    allocate_memory_user    ; 宏,在用戶空間中分配虛擬內存

    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [ebx+80], cx     ; 填寫TSS的SS域
    mov edx, [esi+0x06]  ; 從TCB中取得堆棧的高端線性地址
    ; 由於棧從內存的高端向低端推進,所以esp的內容被指定爲TCB中的下一個可分配的線性地址
    mov [ebx+56], edx    ; 填寫TSS的ESP域
    
    ; 創建用戶任務的0、1、2特權級棧
    ; 段基地址也是0,向上擴張的數據段,段界限爲0x000f_ffff,粒度4KB
    ; 這3個棧段其描述符的特權級不同,段選擇子也不一樣

    ; 創建用戶任務的0特權級棧
    allocate_memory_user    ; 宏,在用戶空間中分配虛擬內存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_9200    ; 4KB粒度的堆棧段描述符,特權級0
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0000B     ; 設置選擇子的請求特權級RPL爲0
    
    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [ebx+8], cx      ; 填寫TSS的SS0域
    mov edx, [esi+0x06]  ; 從TCB中取得堆棧的高端線性地址
    ; 由於棧從內存的高端向低端推進,所以esp的內容被指定爲TCB中的下一個可分配的線性地址
    mov [ebx+4], edx     ; 填寫TSS的ESP0域
    
    ; 創建用戶任務的1特權級棧
    allocate_memory_user    ; 宏,在用戶空間中分配虛擬內存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_b200    ; 4KB粒度的堆棧段描述符,特權級1
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0001B     ; 設置選擇子的請求特權級RPL爲1
    
    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [ebx+16], cx     ; 填寫TSS的SS1域
    mov edx, [esi+0x06]  ; 從TCB中取得堆棧的高端線性地址
    ; 由於棧從內存的高端向低端推進,所以esp的內容被指定爲TCB中的下一個可分配的線性地址
    mov [ebx+12], edx     ; 填寫TSS的ESP1域


    ; 創建用戶任務的2特權級棧
    allocate_memory_user    ; 宏,在用戶空間中分配虛擬內存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_d200    ; 4KB粒度的堆棧段描述符,特權級2
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0010B     ; 設置選擇子的請求特權級RPL爲2
    
    mov ebx, [esi+0x14]  ; 從TCB中取得TSS的基地址
    mov [ebx+24], cx     ; 填寫TSS的SS0域
    mov edx, [esi+0x06]  ; 從TCB中取得堆棧的高端線性地址
    ; 由於棧從內存的高端向低端推進,所以esp的內容被指定爲TCB中的下一個可分配的線性地址
    mov [ebx+20], edx     ; 填寫TSS的ESP0域


    ; 重定位用戶程序所調用的系統API
    ; 回填它們對應的入口地址
    ; 內外循環:外循環依次取出用戶程序需調用的系統api,內循環遍歷內核所有的系統api找到用戶需調用那個
    
    cld     ; 清標誌寄存器EFLAGS中的方向標誌位,使cmps指令正向比較
    
    mov ecx, [0x0c]  ; 0x0c, 應用程序的頭部包含了所需調用系統API個數
    mov edi, [0x08]  ; 0x08, 應用程序頭部中調用系統api列表的起始偏移地址
 .search_sys_api_external:
    push ecx
    push edi
    
    mov ecx, sys_api_items          ; 內循環次數
    mov esi, sys_api                ; 內核中系統api列表的起始偏移地址
 .search_sys_api_internal:
    push esi
    push edi
    push ecx
    
    mov ecx, 64             ; 檢索表中,每一條的比較次數
                            ; 每一項256字節,每次比較4字節,故64次
    repe cmpsd              ; cmpsd每次比較4字節,repe如果相同則繼續
    jnz .b4                 ; ZF=1, 即結果爲0,表示比較結果爲相同,ZF=0, 即結果爲1,不同
                            ; 不同,則開始下一條目的比較
                            
    ; 將系統api的入口地址寫回到用戶程序頭部中對應api條目的開始6字節
    mov eax, [esi]          ; 匹配成功時,esi指向每個條目後的入口地址
    mov [es:edi-256], eax   ; 回填入口地址
    mov ax, [esi+4]         ; 對應的段選擇子
    or ax, 0000_0000_0000_0011B     ; 在創建這些調用門時,選擇子的RPL爲0。即,這些調用門選擇子的請求特權級爲0
    mov [edi-252], ax            ; 回填調用門選擇子
 .b4:
    pop ecx
    pop edi
    pop esi
    add esi, sys_api_item_length    ; 內核中系統api列表的下一條目的偏移地址
    loop .search_sys_api_internal
    
    pop edi
    pop ecx
    add edi, 256                    ; 應用程序頭部中調用系統api列表的下一條目的偏移地址
    loop .search_sys_api_external


    ; 在GDT中登記LDT描述符, 處理器要求LDT描述符必須登記在GDT中
    mov esi, [ebp+9*4]         ; 從堆棧中取得TCB的基地址
    mov eax, [esi+0x0c]      ; LDT起始地址
    movzx ebx, word [esi+0x0a] ; LDT段界限,movzx先零擴展再傳送
    mov ecx, 0x0040_8200        ; LDT描述符屬性,特權級DPL爲0,TYPE爲2表示這是一個LDT描述符
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    mov [esi+0x10], cx       ; 登記LDT選擇子到TCB中
    
    mov ebx, [esi+0x14]      ; 從TCB中取得TSS的基地址
    mov [ebx+96], cx         ; 填寫TSS的LDT域
    
    
    ; 登記基本的TSS表格內容
    mov word [ebx+0], 0      ; 反向鏈=0,將指向前一個任務的指針(任務鏈接域)填寫爲0
                                ; 這表明這是唯一的任務
    
    ; 登記I/O許可位映射區的地址
    ; 在這裏填寫的是TSS段界限(103),表明不存在該區域
    mov dx, [esi+0x12]
    mov [ebx+102], dx
    
    mov word [ebx+100], 0     ; T=0
    
    mov eax, [0x04]     ; 從用戶程序頭部取得程序入口點,0x04    
    mov [ebx+32], eax   ; 填寫TSS的EIP域, 即用戶程序的入口點
    ; 從內核任務切換到用戶任務時,是用TSS中的內容恢復現場的

    ; 將標誌寄存器EFLAGS內容寫入TSS中的EFLAGS域
    pushfd      ; 將EFLAGS寄存器內容壓棧
    pop edx     ; 彈出到edx
    mov dword [ebx+36], edx  ; 將EFLAGS內容寫入TSS中EFLAGS域

    
    ; 登記TSS描述符到GDT中
    ; 和局部描述符表LDT一樣,也必須在GDT中安裝TSS的描述符
    ; 一方面是爲了對TSS進行段和特權級的檢查,另一方面也是執行任務切換的需要
    ; 當call far和jmp far指令的操作數是TSS描述符選擇子時,處理器執行任務切換操作
    mov eax, [esi+0x14]      ; 從TCB中取得TSS的基地址
    movzx ebx, word [esi+0x12] ; TSS的界限值
    mov ecx, 0x0040_8900        ; TSS的屬性,特權級DPL爲0,字節粒度
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    mov [esi+0x18], cx       ; 登記TSS描述符選擇子到TCB,RPL爲0
    
    ; 創建用戶任務的頁目錄
    ; 頁的分配和使用是由頁位圖決定的,可以不佔用線性地址空間 
    call sel_code_4gb_seg:create_user_pdt_by_copy
    mov ebx, [esi+0x14]      ; 從TCB中取得TSS的基地址
    mov dword [ebx+28], eax  ; 填寫TSS的CR3(PDBR)域
    
    popad
    
    ret 8       ; 丟棄調用本過程前壓入的參數
                ; 該指令執行時,除了將控制返回到過程的調用者之外,還會調整棧的指針esp=esp+8字節






; -----------------------------------------------------------------
; Function: 頻幕上顯示文本,並移動光標
; Input: ebx 字符串起始地址,以0結尾
show_string:
    push ebx
    push ecx
    
    ; 臨時關閉硬件中斷
    ; 否則,字符串顯示期間,隨時會被中斷而切換到另一個任務,將導致2個任務所顯示的內容在屏幕上交替出現
    cli     ; 用cli指令清零EFLAGS寄存器的IF位
    
 .loop_show_string:
    mov cl, [ebx]
    or cl, cl
    jz .exit                ; 以0結尾
    call show_char
    inc ebx
    jmp .loop_show_string
    
 .exit:
    ; 放開硬件中斷
    sti 
 
    pop ecx
    pop ebx
    retf                    ; 段間調用返回

; Function: 
; Input: cl 字符
show_char:

    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    
    ; 讀取當前光標位置
    ; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分別用於提供光標位置的高和低8位
    ; 數據端口0x3d5
    mov dx, 0x3d4   
    mov al, 0x0e   
    out dx, al
    mov dx, 0x3d5
    in al, dx
    mov ah, al
    
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x3d5
    in al, dx
    mov bx, ax      ; 此處用bx存放光標位置的16位數
    
    ; 因爲訪問顯示緩衝區時用的是32位尋址方式,故必須使用EBX寄存器
    and ebx, 0x0000_ffff ; 清除EBX寄存器高16位
    
 ; 判斷是否爲回車符0x0d
    cmp cl, 0x0d    ; 0x0d 爲回車符
    jnz .show_0a    ; 不是回車符0x0d,再判斷是否換行符0x0a
    mov ax, bx      ; 是回車符,則將光標置位到行首
    mov bl, 80
    div bl
    mul bl
    mov bx, ax
    jmp .set_cursor
    
    ; ; 將光標位置移到行首,可以直接減去當前行嗎??
    ; mov ax, bx
    ; mov dl, 80
    ; div dl
    ; sub bx, ah
    ; jmp .set_cursor
    
 
 ; 判斷是否爲換行符0x0a
 .show_0a:
    cmp cl, 0x0a    ; 0x0a 爲換行符    
    jnz .show_normal; 不是換行符,則正常顯示字符
    add bx, 80      ; 是換行符,再判斷是否需要滾屏
    jmp .roll_screen
 
 ; 正常顯示字符
 ; 在寫入其它內容之前,顯存裏全是黑底白字的空白字符0x0720,所以可以不重寫黑底白字的屬性
 .show_normal:
    shl bx, 1 ; 光標指示字符位置,顯存中一個字符佔2字節,光標位置乘2得到該字符在顯存中得偏移地址    
    mov [0x800b_8000+ebx], cl ; 物理內存的低端1MB已被完整地映射到從0x8000_0000開始的高端。
    ; 顯示緩衝區的線性基地址爲0x800b_8000,會被處理器的頁部件轉換成物理地址0x000b_8000
    
    shr bx, 1       ; 恢復bx
    inc bx          ; 將光標推進到下一個位置
    
 ; 判斷是否需要向上滾動一行屏幕
 .roll_screen:
    cmp bx, 2000    ; 25行x80列
    jl .set_cursor
    
    mov esi, 0x800b_80a0
    mov edi, 0x800b_8000
    mov ecx, 1920    ; rep次數 
    cld             ; 傳送方向cls std24行*每行80個字符*每個字符加顯示屬性佔2字節 / 一個字爲2字節
    rep movsd
    
    ; 清除屏幕最底一行,即寫入黑底白字的空白字符0x0720
    mov bx, 3840    ; 24行*每行80個字符*每個字符加顯示屬性佔2字節
    mov ecx, 80
 .cls:
    mov word [0x800b_8000+ebx], 0x0720
    add bx, 2
    loop .cls
    
    mov bx, 1920    ; 重置光標位置爲最底一行行首
 
 ; 根據bx重置光標位置
 ; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分別用於提供光標位置的高和低8位
 ; 數據端口0x3d5
 .set_cursor:
    mov dx, 0x3d4   
    mov al, 0x0e   
    out dx, al
    mov dx, 0x3d5
    mov al, bh      ; in和out 只能用al或者ax
    out dx, al
    
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x3d5
    mov al, bl
    out dx, al
    
    ; 依次pop EDI,ESI,EBP,EBX,EDX,ECX,EAX
    popad

    ret



; ===============================================================================    
; Function: 讀取主硬盤的1個邏輯扇區
; Input: 1) eax 起始邏輯扇區號 2) ds:ebx 目標緩衝區地址
read_hard_disk_0:
    
    ; 屏蔽硬件中斷
    ; 防止對同一個硬盤控制器端口的交叉修改
    ; 多任務環境下,當一個任務正在讀硬盤時,會被另一個任務打斷。如果另一個任務也訪問硬盤,將破壞前一個任務對硬盤的操作狀態
    cli

    push eax
    push ebx
    push ecx
    push edx

    push eax
    ; 1) 設置要讀取的扇區數
    ; ==========================
    ; 向0x1f2端口寫入要讀取的扇區數。每讀取一個扇區,數值會減1;
    ; 若讀寫過程中發生錯誤,該端口包含着尚未讀取的扇區數
    mov dx, 0x1f2           ; 0x1f2爲8位端口
    mov al, 1               ; 1個扇區
    out dx, al
    
    ; 2) 設置起始扇區號
    ; ===========================
    ; 扇區的讀寫是連續的。這裏採用早期的LBA28邏輯扇區編址方法,
    ; 28個比特表示邏輯扇區號,每個扇區512字節,所以LBA25可管理128G的硬盤
    ; 28位的扇區號分成4段,分別寫入端口0x1f3 0x1f4 0x1f5 0x1f6,都是8位端口
    inc dx                  ; 0x1f3
    pop eax
    out dx, al              ; LBA地址7~0
    
    inc dx                  ; 0x1f4
    mov cl, 8
    shr eax, cl
    out dx, al              ; in和out 操作寄存器只能是al或者ax
                            ; LBA地址15~8
                            
    inc dx                  ; 0x1f5
    shr eax, cl
    out dx, al              ; LBA地址23~16

    ; 8bits端口0x1f6,低4位存放28位邏輯扇區號的24~27位;
    ; 第4位指示硬盤號,0爲主盤,1爲從盤;高3位,111表示LBA模式
    inc dx                  ; 0x1f6
    shr eax, cl             
    or al, 0xe0             ; al 高4位設爲 1110
                            ; al 低4位設爲 LBA的的高4位
    out dx, al

    ; 3) 請求讀硬盤
    ; ==========================
    ; 向端口寫入0x20,請求硬盤讀
    inc dx                  ; 0x1f7
    mov al, 0x20
    out dx, al
    
 .wait:
    ; 4) 等待硬盤讀寫操作完成
    ; ===========================
    ; 端口0x1f7既是命令端口,又是狀態端口
    ; 通過這個端口發送讀寫命令之後,硬盤就忙乎開了。
    ; 0x1f7端口第7位,1爲忙,0忙完了同時將第3位置1表示準備好了,
    ; 即0x08時,主機可以發送或接收數據
    in al, dx               ; 0x1f7
    and al, 0x88            ; 取第8位和第3位
    cmp al, 0x08            
    jnz .wait
    
    ; 5) 連續取出數據
    ; ============================
    ; 0x1f0是硬盤接口的數據端口,16bits
    mov ecx, 256             ; loop循環次數,每次讀取2bytes
    mov dx, 0x1f0           ; 0x1f0
 .readw:
    in ax, dx
    mov [ebx], ax
    add ebx, 2
    loop .readw
    
    pop edx
    pop ecx
    pop ebx
    pop eax

    ; 放開硬件中斷
    sti
    
    retf        ; 段間返回



; ===============================================================================    
; Function: 申請一個4KB物理頁
; Input: 
; Output: eax, 頁的物理地址 
allocate_memory_page:
; 搜索頁映射位串查找空閒的頁,並分配頁
    push ebx
    push ecx ; ???
    push edx ; ???
    
    ; 從頁映射位串的第0個比特開始搜索
    xor eax, eax
 .search_freepage:
    bts [page_bitmap], eax ; bit test and set, 將指定位置的比特傳送到CF標誌位,然後將其置位
    jnc .done   ; 判斷位串中指定的位是否原本爲0
    inc eax
    cmp eax, page_bitmap_len * 8 ; 判斷是否已經測試了位串中的所有比特
    jl .search_freepage

    ; 沒有可用於分配的空閒頁,顯示一條錯誤信息,並停機
    ; 但這樣是不對的。正確的做法是:看哪些已分配的頁較少使用,然後將它換出到磁盤,
    ; 騰出空間給當前需要的程序,當需要的時候再換回來
    mov ebx, message_page_notenough
    call sel_code_4gb_seg:show_string
    hlt
 
 .done:
    shl eax, 12 ; 將該比特在位串中的位置數值乘以每個頁的大小4KB,就是該比特對應的那個頁的物理地址
    
    pop edx
    pop ecx
    pop ebx
    
    ret ; 這是段內的內部過程,僅供同一段內的其他過程使用



; ===============================================================================    
; Function: 申請一個4KB物理頁,並寫入分頁結構中(頁目錄表和頁表)
; Input: ebx, 線性地址
; Output: 
allocate_install_memory_page:

; 在可用的物理內存中搜索空閒的頁,然後根據線性地址來創建頁目錄項和頁表項,並將頁的地址填寫在頁表項中
    push eax
    push ebx
    push esi
    
    ; 訪問頁目錄表,檢查該線性地址對應的頁目錄項是否存在
    ; 分頁機制下,訪問內存需要通過頁目錄表和頁表,而這裏卻要訪問頁目錄表
    ; 要先得到要修改的那個頁目錄項的線性地址,把頁目錄當作普通頁來訪問
    mov esi, ebx
    and esi, 0xffc0_0000    ; 線性地址的高10位是頁目錄表的索引
    shr esi, 22
    shl esi, 2              ; 乘4,該目錄項在當前頁目錄的偏移地址
    or esi, 0xffff_f000 ; 頁目錄自身的線性地址+目錄項的表內偏移=目錄項的線性地址
                        ; 線性地址高20位爲0xfffff時,訪問的就是頁目錄表自己。創建時已將頁目錄的最後一個目錄項指向了頁目錄本身
    test dword [esi], 0x0000_0001 ; P位是否爲1,即該線性地址是否已經有對應的頁表
    ; 處理器的段管理機制是始終存在的。前面已令ds指向4GB內存段,段基地址爲0。
    ; 這樣,用我們給出的線性地址作爲段內偏移訪問內存,段部件纔會輸出真正的線性地址,儘管兩者是相同的
    jnz .test_pagetable_item
    
    ; 創建該線性地址所對應的頁表
    call allocate_memory_page ; 分配一個4KB物理頁作爲頁表
    or eax, 0x0000_0007 ; 頁表地址高20位對應着頁表物理地址高20位,頁表地址低12位爲頁表屬性
                        ; RW=1頁可讀可寫 P=1頁已經位於內存中 US=1特權級爲3的程序也可訪問
    ; 內核的頁表原則上是不允許特權級爲3的程序訪問,但這個例程既要爲內核分配頁面,也要爲用戶任務分配頁面
    mov [esi], eax      ; 在頁目錄中登記該頁表
    
 .test_pagetable_item:
    ; 訪問頁表,檢查該線性地址對應的頁是否存在
    ; 分頁機制下,訪問內存需要通過頁目錄表和頁表,而這裏卻要訪問頁表
    ; 要先得到要修改的那個頁表項的線性地址,把頁表當作普通頁來訪問
    mov esi, ebx
    shr esi, 10
    and esi, 0x003f_f000    ; 高10位移到中間,再清除2邊
    or esi, 0xffc0_0000     ; 構造的高10位0x3ff指向頁目錄的最後一項。最後一項已在初始化時指向頁目錄本身
                            ; 得到頁表的線性地址
    
    and ebx, 0x003f_f000
    shr ebx, 12
    shl ebx, 2      ; 中間10位移到右邊,再乘以4,得到偏移量
    or esi, ebx     ; 得到頁表項的線性地址    
    
    call allocate_memory_page ; 分配一個物理頁,這纔是要安裝的頁
    or eax, 0x0000_0007     ; 添加屬性值0x007    
    mov [esi], eax          ; 將頁的物理地址寫入頁表項
    
    pop esi
    pop ebx
    pop eax
    
    retf



; ===============================================================================    
; Function: 構造段描述符
; Input: 1) eax 線性基地址 2) ebx 段界限 3) ecx 屬性(無關位則置0)
; Output: edx:eax 完整的8字節(64位)段描述符
make_gdt_descriptor:
    ; 構造段描述符的低32位
    ; 低16位,爲段界限的低16位; 高16位,爲段基址的低16位
    mov edx, eax
    shl eax, 16
    or ax, bx           ; 段描述符低32位(eax)構造完畢
    
    ; 段基地址在描述符高32位edx兩邊就位
    and edx, 0xffff0000 ; 清除基地址的低32位(低32位前面已處理完成)    
    rol edx, 8          ; rol循環左移
    bswap edx           ; bswap, byte swap 字節交換

    ; 段界限的高4位在描述符高32位中就位
    and ebx, 0x000f0000 ; 20位的段界限只保留高4位(低16位前面已處理完成)
    or edx, ebx

    ; 段屬性在描述符高32位中就位
    or edx, ecx         ; 入參的段界限ecx無關位需先置0
    
    retf
    
    
    
; ===============================================================================    
; Function: 在gdt中安裝一個新的段描述符
; Input: edx:eax 段描述符
; Output: cx 段描述符的選擇子
setup_gdt_descriptor:
    
    push eax
    push ebx
    push edx

    ; sgdt, Store Global Descriptor Table Register
    ; 將gdtr寄存器的基地址和邊界信息保存到指定的內存位置
    ; 低2字節爲gdt界限(大小),高4字節爲gdt的32位物理地址
    sgdt [gdt_size]
    
    ; movzx, Move with Zero-Extend, 左邊添加0擴展
    ; 或使用這2條指令替換movzx指令 xor ebx, ebx; mov bx, [gdt_size]
    movzx ebx, word [gdt_size]  ; gdt界限
    inc bx                      ; gdt總字節數,也是gdt中下一個描述符的偏移
                                ; 若使用inc ebx, 如果是啓動計算機以來第一次在gdt中安裝描述符就會有問題
    add ebx, [gdt_base]         ; 下一個描述符的線性地址
    
    mov [ebx], eax
    mov [ebx+4], edx
    
    add word [gdt_size], 8      ; 將gdt的界限值加8,每個描述符8字節

    ; lgdt指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
    ; GDTR, 全局描述符表寄存器
    lgdt [gdt_size]             ; 對gdt的更改生效
    
    ; 生成相應的段選擇子
    ; 段選擇子:15~3位,描述符索引;2, TI(0爲GDT,1爲LDT); 1~0位,RPL(特權級)
    mov ax, [gdt_size]
    xor dx, dx
    mov bx, 8                   ; 界限值總是比gdt總字節數小1。除以8,餘7(丟棄不用)   
    div bx                      ; 商就是所需要的描述符索引號
    mov cx, ax
    shl cx, 3                   ; 將索引號移到正確位置,即左移3位,留出TI位和RPL位
                                ; 這裏 TI=0, 指向gdt RPL=000
                                ; 於是生成了相應的段選擇子

    pop edx
    pop ebx
    pop eax
    
    retf
    
    
    
; =============================================================================== 
; Function: 在ldt中安裝一個新的段描述符
; Input: edx:eax 段描述符; ebx 任務控制塊TCB基地址
; Output: cx 段描述符的選擇子
setup_ldt_descriptor:
    push eax
    push edx
    push edi
    
    mov edi, [ebx+0x0c]     ; 從用戶程序的TCB中取得程序LDT基地址
    
    xor ecx, ecx
    mov cx, [ebx+0x0a]      ; 從用戶程序的TCB中取得程序LDT界限
    inc cx                  ; LDT的總字節數,即新描述符偏移地址
    
    mov [edi+ecx+0x00], eax
    mov [edi+ecx+0x04], edx ; 安裝描述符

    add cx, 8               ; 每個描述符8字節
    dec cx                  ; 更新LDT界限值
    mov [ebx+0x0a], cx      ; 更新LDT界限值到用戶程序的TCB中

    ; 生成相應的段選擇子
    ; 段選擇子:15~3位,描述符索引;2, TI(0爲GDT,1爲LDT); 1~0位,RPL(特權級)
    mov ax, cx
    xor dx, dx
    mov cx, 8                   ; 界限值總是比gdt總字節數小1。除以8,餘7(丟棄不用)   
    div cx                      ; 商就是所需要的描述符索引號
    mov cx, ax
    shl cx, 3                   ; 將索引號移到正確位置,即左移3位,留出TI位和RPL位
    or cx, 0000_0000_0000_0100B ; 這裏 TI=1, 指向ldt; RPL=000
                                ; 於是生成了相應的段選擇子    
    pop edi
    pop edx
    pop eax

    ret

    
    
; ===============================================================================    
; Function: 構造調用門的門描述符
; Input: eax 門代碼在段內的偏移地址; bx 門代碼所在段的段選擇子; cx 門屬性
; Output: edx:eax 門描述符
make_gate_descriptor:    
    push ebx
    push ecx
    
    mov edx, eax
    and edx, 0xffff_0000    ; 得到偏移地址高16位    
    or dx, cx               ; 組裝屬性部分到edx
    
    and eax, 0x0000_ffff    ; 得到偏移地址低16位
    shl ebx, 16
    or eax, ebx             ; 組裝段選擇子到eax
    
    pop ecx
    pop ebx
    retf                ; retf 說明該過程必須以遠調用的方式使用



; -----------------------------------------------------------------
; Function: 在TCB鏈上追加任務控制塊
; Input: ecx 需要追加的那項TCB線性基地址
append_to_tcb_link:
    cli     ; 屏蔽硬件中斷

    push eax
    push ebx
    
    mov eax, tcb_chain_head
 .totailTCB:
    mov ebx, [eax]
    or ebx, ebx
    jz .appendTCB                ; 鏈表爲空,或已到末尾    
    mov eax, ebx
    jmp .totailTCB
    
 .appendTCB:
    cli
    mov [eax], ecx
    mov dword [ecx], 0 ; 將當前TCB指針域清零,表示這是鏈表中最後一個TCB

    pop ebx
    pop eax
    
    sti     ; 放開硬件中斷
    ret



; ===============================================================================    
; Function: 創建用戶任務的頁目錄表
; Input:
; Output: eax, 新頁目錄的物理地址
create_user_pdt_by_copy:

; 創建新的頁目錄,並複製當前頁目錄內容
    push ebx
    push ecx
    push esi
    push edi
    
    call allocate_memory_page
    mov ebx, eax
    or ebx, 0x0000_0007     ; 屬性值0x007,US=1 允許特權級爲3的用戶程序訪問,RW=1可讀可寫,P=1位於物理內存中
    mov [0xffff_fff8], ebx  ; 爲了訪問該頁,將其物理地址登記到當前頁目錄表的倒數第2個目錄項
    ; 當前頁目錄表的線性地址0xffff_f000,倒數第2個目錄項的偏移量爲0xff8
    ; 可倒推出該新目錄表的線性地址爲0xffff_e000

    ; 刷新TLB中的單個條目:
    ; invalidate TLB entry, 是特權指令,當前特權級CPL必須爲0
    invlpg [0xffff_fff8]    ; 0xfffffff8是內核頁目錄表內倒數第2個目錄項,每次都用它來指向新任務的頁目錄表
    ; 雖然在上一條指令中改寫了它,使它指向新任務的頁目錄表,但這個更改只在內存中有效,沒有反映到TLB中
    
    mov esi, 0xffff_f000    ; 當前頁目錄的線性地址
    mov edi, 0xffff_e000    ; 新頁目錄的線性地址
    mov ecx, 1024           ; 傳送次數
    cld                     ; 傳送方向爲正向    
    repe movsd
    
    pop edi
    pop esi
    pop ecx
    pop ebx
    
    retf
    
    
; ===============================================================================    
; Function: 將ds的值以十六進制的形式在屏幕上顯示
; Input: 
; Output: 
show_hex_dword:
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad

    mov ebx, bin_hex
    mov ecx, 8              ; 循環8次
 .hex2word:
    rol edx, 4              ; 循環左移
    mov eax, edx
    and eax, 0x0000_000f
    ; xlat, 處理器的查表指令
    ; 用al作爲偏移量,從ds:ebx指向的內存空間中取出一個字節,傳回al
    xlat
    
    push ecx
    mov cl, al
    call show_char          ; 顯示
    pop ecx
    
    loop .hex2word
    
    popad
    retf



; ===============================================================================    
; Function: 終止當前任務,並轉換到其他任務
; Input: 
; Output: 
terminate_current_task:
; 現在仍處在用戶任務中,要結束當前的用戶任務,可以先切換到程序管理器任務,然後回收用戶程序所佔用的內存空間

    ; 找到當前任務(狀態爲忙的任務)在鏈表中的位置,即狀態值爲0xFFFF的節點
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    cmp word [ebx+0x04], 0xffff     ; 判斷狀態是否爲忙,即是否爲當前任務
    je .modify_state_to_exit
    mov eax, ebx ; TCB內偏移爲0x00處,是下一個TCB的線性地址
    jmp .search_current_task
    
 .modify_state_to_exit:
    ; 修改當前任務的狀態爲“退出”
    mov word [ebx+0x04], 0x3333
    
 .halt:
    hlt         ; 停機,等待程序管理器恢復運行時,將其回收
    jmp .halt
    
    
; -----------------------------------------------------------------
; Function: 通用的異常處理過程
; Input: 
handler_exception_general:
    mov ebx, message_exception_general
    call sel_code_4gb_seg:show_string
    ; show_string函數爲方便用戶任務調用,被包裝成調用門,通過retf返回
    ; 通過調用門的控制轉移屬於遠過程調用,儘管show_string屬於內核
    
    hlt
    
    
    
; -----------------------------------------------------------------
; Function: 通用的中斷處理過程
; Input: 
handler_interrupt_general:
    push eax
    
    ; 向8259A芯片發送中斷結束命令EOI(End of Interrupt)    
    mov al, 0x20    
    out 0xa0, al    ; 向8259A從片發送
    out 0x20, al    ; 向8259A主片發送
    
    pop eax
    iretd    
    
    
    
; -----------------------------------------------------------------
; Function: 實時時鐘中斷處理過程,實現任務切換
;           利用硬件中斷實現任務切換
; Input: 
handler_interrupt_rtm_0x70:
; 計算機主板上有實時時鐘芯片RTC,可以設置RTC芯片,使得它每次更新CMOS中的時間信息後,發出更新週期結束的中斷信號,從而進行任務切換
; 其實,用實時時鐘更新週期結束中斷來實施任務切換並不是一個好主意,和別的中斷相比,它更囉嗦

    pushad
    
    ; 向8259A芯片發送中斷結束命令EOI(End of Interrupt)
    ; 否則,它不會再向處理器發送另一箇中斷通知
    mov al, 0x20    
    out 0xa0, al    ; 向8259A從片發送
    out 0x20, al    ; 向8259A主片發送
    
    ; 必須讀一下CMOS芯片內的寄存器C,使它復位一下, 才能使RTC產生下一個中斷信號。否則,它只產生一次中斷信號
    mov al, 0x0c    ; 寄存器C的索引,且放開NMI
    out 0x70, al
    in al, 0x71     ; 讀一下RTC的寄存器C
    
    ; 找到當前任務(狀態爲忙的任務)在鏈表中的位置,即狀態值爲0xFFFF的節點
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    or ebx, ebx
    jz .irtn    ; 鏈表爲空,或已到末尾,從中斷返回
    cmp word [ebx+0x04], 0xffff     ; 判斷狀態是否爲忙,即是否爲當前任務
    je .move_current_to_chaintail
    mov eax, ebx ; TCB內偏移爲0x00處,是下一個TCB的線性地址
    jmp .search_current_task
    
    ; 把當前任務(狀態爲忙的任務)移到鏈尾
 .move_current_to_chaintail:
    ; 從鏈表中刪除該節點
    mov ecx, [ebx]  ; 下一個TCB節點的線性地址
    mov [eax], ecx  ; 將當前任務從TCB鏈表中刪除
 .goto_chaintail:
    ; 遍歷至鏈表尾部
    mov edx, [eax]
    or edx, edx     ; 判斷是否已到鏈表尾部
    jz .add_current_to_chaintail
    mov eax, edx
    jmp .goto_chaintail
 .add_current_to_chaintail:
    ; 將該節點添加至鏈表尾部
    mov [eax], ebx
    mov dword [ebx], 0 ; 將當前任務標記爲鏈尾。TCB內偏移爲0處,是下一個TCB的線性地址
    
    ; 從TCB鏈表中搜索第一個空閒任務
    mov eax, tcb_chain_head
 .search_next_task:
    mov eax, [eax]
    or eax, eax     ; 已到鏈尾,即未發現空閒任務
    jz .irtn        ; 從中端返回
    cmp word [eax+0x04], 0x0000     ; 是空閒任務。空閒爲0,忙爲0xffff
    jnz .search_next_task
    
    ; 將空閒任務和當前任務的狀態都取反
    not word [eax+0x04]     ; 設置空閒任務的狀態爲忙
    not word [ebx+0x04]     ; 設置當前任務的狀態爲空閒
    
    ; 任務切換
    jmp far [eax+0x14] ; TCB內偏移爲0x14處,依次是該任務TSS的線性地址和TSS描述符選擇子

 .irtn:
    popad   ; 當前任務再次獲得執行權時的返回點
    iretd   ; 從中斷返回,正常執行任務的其他代碼
    
    
    


; -----------------------------------------------------------------
; 用於設置和修改GDT
; lgdt指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0

; IDT
idt_size dw 0   ; 界限值,描述符個數*8-1
idt_base dd 0   ; 基地址

; 任務控制塊鏈表的頭部
tcb_chain_head  dd 0

; 內核(程序管理器)的TCB
kernel_tcb times 32 db 0 

; 頁面的位映射串
; 這裏沒有去檢測實際可用內存,僅僅假定只有2MB的物理內存可用
; 2MB物理內存,即512個4KB頁,需要512個比特的位串
; 這裏前32字節的值基本都是0xff。因爲它們對應着最低端1MB內存的那些頁(256個頁),它們已經整體上劃歸內核使用了,
; 沒有被內核佔用的部分多數也被外圍硬件佔用了,比如ROM-BIOS
; 這裏0x55, 即0101_0101, 是有意將空閒的頁在物理上分開,用於說明連續的地址空間不必對應着連續的頁
page_bitmap   db  0xff,0xff,0xff,0xff,0xff,0xff,0x55,0x55
              db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
              db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
              db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
              db  0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
              db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
              db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
              db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
page_bitmap_len equ $-page_bitmap

; 系統API的符號-地址檢索表
; 自命名 Symbol-Address Lookup Table, SALT
sys_api:
 sys_api_1  db '@ShowString'
            times 256-($-sys_api_1) db 0
            dd show_string
            dw sel_code_4gb_seg
            
 sys_api_2  db '@ReadDiskData'
            times 256-($-sys_api_2) db 0
            dd read_hard_disk_0
            dw sel_code_4gb_seg

 sys_api_3  db '@ShowDwordAsHexString'
            times 256-($-sys_api_3) db 0
            dd show_hex_dword
            dw sel_code_4gb_seg            

 sys_api_4  db '@TerminateProgram'
            times 256-($-sys_api_4) db 0
            dd terminate_current_task
            dw sel_code_4gb_seg
            
sys_api_item_length     equ $-sys_api_4
sys_api_items           equ ($-sys_api)/sys_api_item_length    

; 處理器品牌信息    
cpu_brand0  db 0x0d, 0x0a, '  ', 0      ; 空行    
cpu_brand   times 52 db 0
cpu_brand1  db 0x0d, 0x0a, 0x0d, 0x0a, 0; 空行

core_buf    times 512 db 0              ; 自定義的內核緩衝區
    
bin_hex     db '0123456789ABCDEF' ; show_hex_dword過程需要的查找表

; 提示信息,捕獲到異常
message_exception_general db  '********Exception encounted********',0

; 提示信息,內核已工作在保護和分頁模式
message_kernel_at_protect_page_mode db  '  Working in system core with protection '
    db  'and paging are all enabled.System core is mapped '
    db  'to address 0x80000000.',0x0d,0x0a,0

; 提示信息,系統api的調用門安裝完成
message_callgate_mount_succ db  '  System wide CALL-GATE mounted.',0x0d,0x0a,0

; 提示信息, 沒有可用於分配的空閒頁 
message_page_notenough db  '********No more pages********',0

; 提示信息,正執行內核任務
message_kernel_task db  '  System core task running!',0x0d,0x0a,0

    




core_code_end:



; ===============================================================================    
SECTION tail        ; 這裏用於計算程序大小,不需要vstart=0
core_end:    

# file_03: c17_1.asm    用戶程序1代碼

; FILE: c17_1.asm
; TITLE: 用戶程序
; DATE: 20200209

; ===============================================================================
; SECTION head vstart=0                       ; 定義用戶程序頭部段
    ; 用戶程序可能很大,16位可能不夠
    program_length  dd program_end      ; 程序總長度[0x00]
    
    ; 程序入口點(Entry Point), 編譯階段確定的起始彙編地址
    program_entry   dd beginning        ; 偏移地址[0x04]

    ; 所需調用的系統API
    ; 自定義規則:用戶程序在頭部偏移量爲0x30處構造一個表格,並列出所有要用到的符號名
    ; 每個符號名的長度是256字節,不足部分用0x00填充
    ; 內核加載用戶程序時,會將每一個符號名替換成相應的內存地址,即重定位
    ; 符號-地址檢索表,Symbol-Address Lookup Table, SALT
    salt_position   dd salt             ; salt表偏移量[0x08]        
    salt_itmes      dd (salt_end-salt)/256  ; salt條目數[0x0c]
    
salt:                                     ; [0x2c]
    ShowString      db '@ShowString'
                    times 256-($-ShowString) db 0
    
    TerminateProgram db '@TerminateProgram'
                    times 256-($-TerminateProgram) db 0

    ReadDiskData    db '@ReadDiskData'
                    times 256-($-ReadDiskData) db 0
                    
    ShowDwordAsHexString db '@ShowDwordAsHexString'
                    times 256-($-ShowDwordAsHexString) db 0
                    
salt_end:


; ===============================================================================
; SECTION data vstart=0                       ; 定義用戶程序數據段

; 自定義的數據緩衝區
; buffer  times 1024 db 0

; 提示信息,正在運行用戶程序
message_usermode    db  '  User task A->;;;;;;;;;;;;;;;;;;;;;;;;;;'
                    db 0x0d,0x0a, 0
                    

; ===============================================================================
[bits 32]


; ===============================================================================
; SECTION code vstart=0                       ; 定義用戶程序代碼段
beginning:   
    ; 調用系統API
    ; 顯示提示信息,正在運行的用戶程序的當前特權級CPL
    mov ebx, message_usermode
    call far [ShowString]   

    jmp beginning

    ; 調用系統API, 退出,並將控制權返回給內核   
    call far [TerminateProgram]

; code_end:

    
; ===============================================================================    
; SECTION tail align=16       ; 這裏用於計算程序大小,不需要vstart=0
program_end:    
    
    

# file_04: c17_2.asm    用戶程序1代碼

; FILE: c17_1.asm
; TITLE: 用戶程序
; DATE: 20200209

; ===============================================================================
; SECTION head vstart=0                       ; 定義用戶程序頭部段
    ; 用戶程序可能很大,16位可能不夠
    program_length  dd program_end      ; 程序總長度[0x00]
    
    ; 程序入口點(Entry Point), 編譯階段確定的起始彙編地址
    program_entry   dd beginning        ; 偏移地址[0x04]

    ; 所需調用的系統API
    ; 自定義規則:用戶程序在頭部偏移量爲0x30處構造一個表格,並列出所有要用到的符號名
    ; 每個符號名的長度是256字節,不足部分用0x00填充
    ; 內核加載用戶程序時,會將每一個符號名替換成相應的內存地址,即重定位
    ; 符號-地址檢索表,Symbol-Address Lookup Table, SALT
    salt_position   dd salt             ; salt表偏移量[0x08]        
    salt_itmes      dd (salt_end-salt)/256  ; salt條目數[0x0c]
    
salt:                                     ; [0x2c]
    ShowString      db '@ShowString'
                    times 256-($-ShowString) db 0
    
    TerminateProgram db '@TerminateProgram'
                    times 256-($-TerminateProgram) db 0

    ReadDiskData    db '@ReadDiskData'
                    times 256-($-ReadDiskData) db 0
                    
    ShowDwordAsHexString db '@ShowDwordAsHexString'
                    times 256-($-ShowDwordAsHexString) db 0
                    
salt_end:


; ===============================================================================
; SECTION data vstart=0                       ; 定義用戶程序數據段

; 自定義的數據緩衝區
; buffer  times 1024 db 0

; 提示信息,正在運行用戶程序
message_usermode    db  '  User task B->********************'
                    db 0x0d,0x0a, 0
                    

; ===============================================================================
[bits 32]


; ===============================================================================
; SECTION code vstart=0                       ; 定義用戶程序代碼段
beginning:   
    ; 調用系統API
    ; 顯示提示信息,正在運行的用戶程序的當前特權級CPL
    mov ebx, message_usermode
    call far [ShowString]   

    jmp beginning

    ; 調用系統API, 退出,並將控制權返回給內核   
    call far [TerminateProgram]

; code_end:

    
; ===============================================================================    
; SECTION tail align=16       ; 這裏用於計算程序大小,不需要vstart=0
program_end:    
    
    

 

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