[書]x86彙編語言:從實模式到保護模式 -- 第16章 分頁機制、平坦模型

# 分頁機制

    二級頁表:頁目錄、頁表 ==> 4KB物理頁

    32位線性地址中:高10位爲頁目錄中的索引號(乘4得偏移量),該目錄項指向頁表的基地址;中間10位爲頁表中的索引號,該頁表項指向4KB物理頁的基地址;低12位爲物理頁中的偏移量。

    爲了方便能修改頁目錄或者頁表中的內容,將創建並初始化頁目錄時,將頁目錄的最後一個目錄項指向頁目錄本身的物理地址。

    4GB線性地址空間中,低2GB爲局部空間,高2GB爲全局空間指向內核。

    開啓分頁機制:

    ; 令CR3寄存器指向頁目錄
    ; CR3寄存器的低12位除了用於控制高速緩存的PCD和PWT位,都沒有使用
    mov eax, 0x2_0000       ; PCD=PWT=0    
    mov cr3, eax
    
    ; 開啓分頁機制
    ; 從此,段部件產生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址
    mov eax, cr0
    or eax, 0x8000_0000
    mov cr0, eax

# 平坦模型

    不分段的內存管理模型稱爲平坦模型(Flat model)。在這種模型下,所有段都是4GB,每個段的描述符都指向4GB的段,段的基地址都是0,段界限都是0xf_ffff,粒度爲4KB。

 

# 執行結果

# file_01: c16_core.asm

; FILE: c13_core.asm
; DATE: 20200104
; TITLE: mini內核

; 常量
; 僞指令equ僅僅是允許用符號代替具體的數值,但聲明的數值並不佔用空間
; 這些選擇子對應的gdt描述符會在mbr中的內核初始化階段創建
; 段選擇子:15~3位,描述符索引;2, TI(0爲GDT,1爲LDT); 1~0位,RPL(特權級)
sel_core_code_seg   equ 0x38    ; gdt第7號描述符,內核代碼段選擇子
sel_core_data_seg   equ 0x30    ; gdt第6號描述符,內核數據段選擇子
sel_sys_routine_seg equ 0x28    ; gdt第5號描述符,系統API代碼段的選擇子
sel_video_ram_seg   equ 0x20    ; gdt第4號描述符,視頻顯示緩衝區的段選擇子
sel_core_stack_seg  equ 0x18    ; gdt第3號描述符,內核堆棧段選擇子
sel_mem_0_4gb_seg   equ 0x08    ; gdt第1號描述符,整個0~4GB內存的段選擇子

app_lba_begin       equ 50      ; 將配套的的用戶程序從磁盤lba邏輯扇區50開始寫入

; ===============================================================================
SECTION head vstart=0               ; mini內核的頭部,用於mbr加載mini內核

core_length         dd core_end                     ; mini內核總長度, 0x00

segment_sys_routine dd section.sys_routine.start    ; 系統API代碼段起始彙編地址,0x04
sys_routine_length  dd sys_routine_end              ; 0x08

segment_core_data   dd section.core_data.start      ; mini內核數據段起始彙編地址,0x0c
core_data_length    dd core_data_end                ; 0x10
 
segment_core_code   dd section.core_code.start      ; mini內核代碼段起始彙編地址,0x14
core_code_length    dd core_code_end                ; 0x18

core_entry          dd beginning                    ; mini內核入口點(32位的段內偏移地址),0x1c
                    dw sel_core_code_seg            ; 16位的段選擇子


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


; ===============================================================================
SECTION core_code vstart=0               ; mini內核代碼
beginning:
    ; 執行到這裏的時候,主引導程序已經創建了內核的大部分要素:
    ; 全局描述符表GDT、公共例程段、內核數據段/代碼段、內核棧、用於訪問4GB內存空間的段
    mov ecx, sel_core_data_seg
    mov ds, ecx                 ; 使ds指向mini內核數據段
    
    mov ecx, sel_mem_0_4gb_seg
    mov es, ecx                 ; 使es指向4GB內存段

    ; 顯示提示信息,內核已加載成功並開始執行
    mov ebx, message_kernel_load_succ
    call sel_sys_routine_seg:show_string ; 調用系統api,顯示一段文字
                                         ; call 段選擇子:段內偏移
    
    ; 獲取處理器品牌信息
    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_sys_routine_seg:show_string
    mov ebx, cpu_brand          ; 處理器品牌信息
    call sel_sys_routine_seg:show_string
    mov ebx, cpu_brand1         ; 空行
    call sel_sys_routine_seg:show_string
    
    ; 準備開啓分頁機制
    
    ; 每個任務都有自己的頁目錄和頁表,內核也不例外
    ; 理想的分頁系統中,要加載程序,必須先搜索可用的頁,並將它們與段對應起來
    ; 但,內核是在開啓頁功能之前加載的,段在內存中的位置已固定。這種情況下,要保證即使開啓了頁功能,線性地址也必須和物理地址相同纔行
    ; 這裏自定義的內存佈局:內核的頁目錄表放在物理地址0x2_0000處,
    ;   內核的第一個頁表放在物理地址0x2_1000處
    
    ; 頁目錄的所有頁目錄項清零
    ; 主要是使所有目錄項的P位爲0,表明該頁表不在內存中,地址變換時將引發處理器異常中斷
    mov ecx, 1024       ; 頁目錄含1024個頁目錄項
    mov ebx, 0x2_0000   ; 自定義的頁目錄物理地址
    xor esi, esi
 .pdt2zero:
    mov dword [es:ebx+esi], 0   ; 頁目錄項清零
    add esi, 4
    loop .pdt2zero
    
    ; 頁目錄中最後一個頁目錄項,即第1023個
    ; 將頁目錄表的物理地址0x20000登記在它自己的最後一個目錄項內
    ; 主要是爲了方便用線性地址訪問頁目錄表自身
    ; 這裏浪費了一個頁目錄項
    ; 0x0002_0003, 前20位是物理地址的高20位;P=1,頁位於內存中;RW=1,該目錄項指向的頁表可讀可寫;
    ;   US位爲1,此目錄項指向的頁表不允許特權級爲3的程序和任務訪問
    mov dword [es:ebx+4092], 0x2_0003   ; 1023*4 = 4092
    
    ; 頁目錄中第0個頁目錄項,使其指向頁表0x21000
    ; 該頁位於內存中,可讀可寫,不允許特權級爲3的程序和任務訪問
    mov dword [es:ebx+0], 0x2_1003
    
    ; 創建與上面那個目錄項對應的頁表,初始化頁表項
    ; 將內存低端1MB所包含的那些頁的物理地址按順序一個一個地填寫到頁表中
    ; 這裏的mini內核佔用着內存的低端1MB,即256個頁表項
    ; 這裏選擇用頁目表的第1個目錄項,以及該目錄項所指向頁表的前256個頁表項
    mov ebx, 0x2_1000       ; 頁表的物理地址0x21000
    xor eax, eax            ; 起始頁的物理地址0x0
    xor esi, esi
 .pdt_1_pre256:
    mov edx, eax
    or edx, 0x0000_0003     ; 低12位爲頁屬性
                            ; 屬性值3,P=1, RW=1, US=0
    
    mov [es:ebx+esi*4], edx ; 在頁表中登記頁的物理地址
    add eax, 0x1000         ; 下一個相鄰頁的物理地址,每個頁4KB
    inc esi                 ; 頁表的下一個頁表項
    cmp esi, 256
    jl .pdt_1_pre256
    
    ; 將上面那個頁表的其餘頁表項置爲無效
 .pdt_1_others:
    mov dword [es: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
    
    ; 開啓分頁機制
    ; 從此,段部件產生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址
    mov eax, cr0
    or eax, 0x8000_0000
    mov cr0, eax
    
    ; 分頁機制下,程序只能使用線性地址,訪問內存必須先訪問頁目錄和頁表,通過它們轉換之後的地址纔是能夠發送到內存芯片的物理地址
    ; 所以,就算知道頁目錄表的物理地址,也沒有用
    ; 除非,頁目錄表中有一個目錄項能指向頁目錄表自己
    ; 否則,訪問一個並未在頁目錄表和頁表內登記的頁,會引發處理器異常中斷
    
    ; 任務的4GB地址空間分2部分:局部地址空間和全局地址空間,各2GB
    ; 低2GB爲局部空間,線性地址爲 0x0000_0000 ~ 0x7FFF_FFFF; 高2GB爲全局空間,線性地址爲 0x8000_0000 ~ 0xFFFF_FFFF
    ; 地址空間的分配必須在每個任務的頁目錄中體現。頁目錄的前半部分指向任務自己的頁表,後半部分指向內核的頁表
    
    ; 在內核的頁目錄內創建與線性地址0x8000_0000對應的目錄項,並使它指向同一個頁表
    ; 需發推來構造線性地址,再通過頁機制映射爲物理地址
    mov ebx, 0xffff_f000    ; 線性地址高20位爲0xfffff時,訪問的就是頁目錄表自己
                            ; 前面已將頁目錄的最後一個目錄項指向了頁目錄本身。頁目錄和頁表本質上都是一個頁
    mov esi, 0x8000_0000    ; 保留高12位作爲頁目錄表內的偏移地址
    shr esi, 20             ; 偏移量0x800=2^11,每個目錄項4字節,除4,第2^9個目錄項,每個目錄項對應4MB內存,乘2^22,得2^31,即2GB。高2GB全局空間
    mov dword [es:ebx+esi], 0x2_1003 ; 寫入目錄項(頁表的物理地址和屬性)
                                     ; 目標單元線性地址爲[0xffff_f800]
    
    ; 修改內核的段描述符,將基地址部分加上0x8000_0000即可
    ; 段描述符中的基地址處於高低2個雙字中的不同位置,重新計算比較麻煩;但0x8000_0000這個數值比較特殊,只需將描述符的最高位置1
    ; 將GDT中的段描述符映射到線性地址0x8000_0000
    sgdt [gdt_size]
    mov ebx, [gdt_base]
    or dword [es:ebx+0x10+4], 0x8000_0000 ; mbr代碼段描述符的高4字節
    or dword [es:ebx+0x18+4], 0x8000_0000 ; 內核堆棧段
    or dword [es:ebx+0x20+4], 0x8000_0000 ; 顯示緩衝區
    or dword [es:ebx+0x28+4], 0x8000_0000 ; 系統API代碼段
    or dword [es:ebx+0x30+4], 0x8000_0000 ; 內核數據段
    or dword [es:ebx+0x38+4], 0x8000_0000 ; 內核代碼段
    ; 這裏0~4GB內存段的描述符沒有修改,因爲它本身就是爲了訪問整個內存空間而存在的
    ; 內核需要有訪問整個內存空間的能力
    
    add dword [gdt_base], 0x8000_0000 ; 全局描述符表寄存器GDTR也用的是線性地址
    lgdt [gdt_size] ; 將修改後的GDT基地址和界限值加載到GDTR
    
    ; 顯示刷新段寄存器,使用高端線性地址
    ; 遇到jmp或call指令,處理器一般會清空流水線,另一方面,還會重新加載段選擇器,並刷新描述符高速緩存器中的內容
    ; jmp dword 0x0010:flush
    jmp sel_core_code_seg:flush
    
flush:
    ; 重新加載段寄存器ss和ds的描述符高速緩存器
    mov eax, sel_core_stack_seg
    mov ss, eax
    
    mov eax, sel_core_data_seg
    mov ds, eax
    
    ; 顯示提示信息,分頁功能已開啓,
    ; 而且內核已被映射到線性地址0x8000_0000以上
    ; mov ebx, message_pagemode_load_succ
    ; call sel_sys_routine_seg:show_string
    
    ; 安裝整個系統服務的調用門。特權級之間的控制轉移必須使用門
    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_sys_routine_seg:make_gate_descriptor   ; 創建調用門描述符
    call sel_sys_routine_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_sys_routine_seg:show_string


    ; 爲程序管理器的TSS分配內存
    ; 0特權級的內核任務
    ; mov ecx, 104
    ; call sel_sys_routine_seg:allocate_memory
    ; mov [prgman_tss+0x00], ecx      ; 保存TSS基地址
    ; 分頁機制下,內存的分配既要在虛擬內存空間中進行,還要在頁目錄表和頁表中進行
    mov ebx, [core_next_laddr]
    call sel_sys_routine_seg:allocate_install_memory_page
    add dword [core_next_laddr], 4096  ; 4KB
    
    ; 在程序管理器的TSS中設置必要的項目
    mov eax, cr3
    mov dword [es:ebx+28], eax  ; 登記CR3(PDBR)
    
    ; 程序管理器TSS的基本設置
    mov word [es:ebx+96], 0         ; 沒有LDT。這裏是將所有的段描述符安裝在GDT中
    ; 登記I/O許可位映射區的地址
    ; 在這裏填寫的是TSS段界限(103),表明不存在該區域
    mov word [es:ebx+102], 103      ; 沒有I/O位圖。事實上0特權級不需要
    mov word [es:ebx+0], 0          ; 反向鏈=0
    ; mov dword [es:ecx+28], 0        ; 登記CR3(PDBR)
    mov word [es:ebx+100], 0        ; T=0。不需要0 1 2特權級堆棧,0特權級不會向低特權級轉移控制
    
    ; 創建TSS描述符,並安裝到GDT中
    mov eax, ebx        ; 起始地址
    mov ebx, 103        ; 段界限
    mov ecx, 0x0040_8900; 段屬性,特權級0
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [prgman_tss+0x04], cx       ; 保存TSS描述符選擇子
    
    ; 說明表明當前任務是誰,表明當前任務正在執行中
    ; 將當前任務的TSS選擇子傳送到任務寄存器TR
    ; 執行這條指令後,處理器用該選擇子訪問GDT,找到相對應地TSS,將其B位置1,表示該任務正在執行中
    ; 同時,還將該描述符傳送到TR寄存器的描述符高速緩存器中
    ltr cx  ; 任務寄存器TR

    ; 現在可認爲"程序管理器"任務正在執行中
    ; 顯示提示信息,任務管理器正在執行中
    ; mov ebx, prgman_msg1
    ; call sel_sys_routine_seg:show_string
    
    
    ; 創建用戶任務的任務控制塊TCB
    ; 任務都是由內核負責管理和調度的,所有任務的TCB都應在內核的地址空間裏分配
    mov ebx, [core_next_laddr]
    call sel_sys_routine_seg:allocate_install_memory_page
    add dword [core_next_laddr], 4096
    
    ; 初始化TCB
    ; TCB中有2項內容需要在創建用戶任務之前初始化:LDT當前界限值、下一個可用的線性地址
    ; LDT當前界限值,應初始化爲0xffff。這是計算機啓動時,LDTR寄存器中的默認界限值,LDTR中的界限部分只有16位。
    ;   LDT的界限是LDT的長度減1,LDT的初識長度爲0
    ; 下一個可用的線性地址。每個任務都有自己的4GB虛擬內存空間,實際可使用的是前2GB局部空間,後2GB爲全局空間,映射並指向內核的頁表
    ;   一般來說,第一個可分配的線性地址是0
    mov dword [es:ebx+0x06], 0  ; 0x06, 下一個可用的線性地址
                                ; 用戶任務局部空間的分配從0開始
    mov word [es:ebx+0x0a], 0xffff ; 0x0a, LDT當前界限值
    
    ; 將TCB添加到TCB鏈中
    mov ecx, ebx
    call append_to_tcb_link
    
    ; 這裏自定義的TCB結構需要0x46字節的內存空間
    ; mov ecx, 0x46
    ; call sel_sys_routine_seg:allocate_memory
    ; call append_to_tcb_link     ; 將此TCB添加到TCB鏈中 
    
    ; 加載並重定位用戶程序
    ; 通過棧傳入參數
    push dword app_lba_begin    ; 用戶程序在硬盤中邏輯扇區號
    push ecx                    ; 用戶程序的任務控制塊TCB地址
    
    call load_relocate_program  ; call指令相對近調用時自動執行push eip

    ; 執行任務切換
    ; 這裏操作數是一個內存地址,指向任務控制塊TCB內的0x14單元,存放着任務的TSS基地址,接着是TSS選擇子
    ; 處理器用得到的選擇子訪問GDT,當發現得到的是一個TSS描述符,就執行任務切換
    ; 首先,會把每個寄存器的快照保存到由TR指向的TSS中;然後,從新任務的TSS描述符中恢復各個寄存器的內容;最後,任務寄存器TR指向新任務的TSS,處理器開始執行新的任務
    ; 任務切換時要恢復TSS內容,所以在創建任務時TSS要填寫完整

    
    call far [es:ecx+0x14]  ; call指令的參數是TCB中的TSS選擇子

    ; 理論上這裏還需要回收舊任務所佔用的內存空間,並從任務控制塊TCB鏈上去掉,以確保不會再切換到該任務執行
    ; 但,在這裏並沒有實現這個功能
    
    ; 顯示提示信息,重新回到了任務管理器任務
    mov ebx, prgman_msg2
    call sel_sys_routine_seg:show_string
    
    
    ; 這裏自定義的TCB結構需要0x46字節的內存空間
    ; mov ecx, 0x46
    ; call sel_sys_routine_seg:allocate_memory
    ; call append_to_tcb_link     ; 將此TCB添加到TCB鏈中    


    ; 加載並重定位用戶程序
    ; 和上一個用戶任務來自同一個程序,一個程序可以對應着多個運行中的副本,或者說多個任務。但,它們卻沒有任何關係,在內存中的位置不同,運行狀態也不一樣
    ; 通過棧傳入參數
    ; push dword app_lba_begin    ; 用戶程序在硬盤中邏輯扇區號
    ; push ecx                    ; 用戶程序的任務控制塊TCB地址    

    ; call load_relocate_program  ; call指令相對近調用時自動執行push eip
    
    ; 執行任務切換
    ; 這裏操作數是一個內存地址,指向任務控制塊TCB內的0x14單元,存放着任務的TSS基地址,接着是TSS選擇子
    ; 用jmp指令發起的任務切換,新任務不會嵌套於舊任務中
    ; jmp far [es:ecx+0x14]
    
    ; 顯示提示信息,重新回到了任務管理器任務
    ; mov ebx, prgman_msg3
    ; call sel_sys_routine_seg:show_string

    hlt



    ; 顯示提示信息,用戶程序加載完成
    ; mov ebx, message_app_load_succ
    ; call sel_sys_routine_seg:show_string

    ; 將控制轉移到用戶程序
    ; 即,從0特權級轉到3特權級,從0特權級全局空間轉移到3特權級局部空間執行
    ; 通常情況,這既不允許,也不太可能
    
    ; 假裝從調用門返回
    ; 先確立身份,使TR和LDTR寄存器指向這個任務,然後假裝從調用門返回
    ; mov eax, sel_mem_0_4gb_seg
    ; mov ds, eax
    
    ; ltr [ecx+0x18]      ; load task register,TR指向TSS。加載任務狀態段
    ; lldt [ecx+0x10]     ; load local descriptor table,LDTR指向LDT。加載LDT
                        ; 這裏ecx是前面調用allocate_memory的返回值
    
    ; mov eax, [ecx+0x44]
    ; mov ds, eax         ; 切換到用戶程序頭部段
                        ; 局部描述符表LDT已經生效,可以通過它訪問用戶程序的私有內存段了
                        ; 此處該選擇子RPL請求特權級爲3,TI位爲1即指向任務自己的LDT
    
    ; 模仿處理器壓入返回參數,假裝從調用門返回
    ; push dword [0x08]   ; 從用戶程序頭部取出堆棧段選擇子ss
    ; push dword 0        ; 棧指針esp
    
    ; push dword [0x14]   ; 代碼段選擇子cs
    ; push dword [0x10]   ; 指令指針eip

    ; retf                ; 假裝從調用門返回
                        ; 於是控制轉移到用戶程序的3特權級代碼開始執行

    
    ; mov [kernel_esp_pointer], esp   ; 臨時保存內核的堆棧指針
                                    ; 進入用戶程序後,會切換到用戶的堆棧
                                    ; 從用戶程序返回時,可通過這裏還原內核棧指針

    ; mov ds, ax      ; 使ds指向用戶程序頭部段
                    ; 此處的ax值是load_relocate_program的返回值

    ; jmp far [0x10]  ; 跳轉到用戶程序執行,控制權交給用戶程序
                    ; 0x10, 應用程序的頭部包含了用戶程序的入口點
    
    

    
    
; Function: 加載並重定位用戶程序
; Input: PUSH app起始邏輯扇區號; PUSH app任務控制塊TCB線性地址
load_relocate_program:
    
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    
    push ds
    push es
    
    mov ebp, esp    ; 棧基址寄存器, 爲訪問通過堆棧傳遞的參數做準備
    
    mov ecx, sel_mem_0_4gb_seg
    mov es, ecx                         ; 切換es到0~4GB的段

    ; 清空當前頁目錄的前半部分(對應低2GB的局部地址空間),頁目錄表的前512個目錄項
    ; 後半部分是由內核使用的,內核的虛擬地址空間倍映射在每個任務的高地址段,0x8000_0000之後(對應高2GB的全局地址空間)
    mov ebx, 0xffff_f000 ; 當前頁目錄表的起始地址
                         ; 線性地址高20位爲0xfffff時,訪問的就是頁目錄表自己
    xor esi, esi
 .clear_pdt_pre2gb:
    mov dword [es:ebx+esi*4], 0
    inc esi
    cmp esi, 512
    jl .clear_pdt_pre2gb
    
    ; 分配內存並加載用戶程序
    
    ; mov esi, [ebp+11*4]                 ; 從堆棧中取得用戶程序的TCB基地址
    
    ; 申請創建LDT所需的內存
    ; mov ecx, 160                        ; 160字節,允許安裝20個LDT描述符
    ; call sel_sys_routine_seg:allocate_memory
    ; mov [es:esi+0x0c], ecx              ; 登記LDT基地址到TCB中
    ; mov word [es:esi+0x0a], 0xffff      ; 登記LDT界限值到TCB中
                                        ; 和GDT一樣,LDT的界限值等於總字節數減1。初始時,0-1=0xFFFFD

    ; 開始加載用戶程序
    ; 先讀取一個扇區
    mov eax, sel_core_data_seg          ; 切換ds到內核數據段
    mov ds, eax
    
    mov eax, [ebp+12*4]                 ; 從堆棧中取得用戶程序所在硬盤的起始邏輯扇區號 
    mov ebx, core_buf                   ; 自定義的一段內核緩衝區
                                        ; 在內核中開闢出一段固定的空間,有便於分析、加工和中轉數據
    call sel_sys_routine_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
                                    
    ; mov ecx, eax                    ; 需要申請的內存大小
    ; call sel_sys_routine_seg:allocate_memory
    ; mov [es:esi+0x06], ecx          ; 登記用戶程序加載到內存的基地址到TCB中
    
    ; mov ebx, ecx                    ; 申請到的內存首地址
                                    ; 作爲起始地址,從硬盤上加載整個用戶程序
                                    
    ; push ebx                        ; 用於後面訪問用戶程序頭部

    ; 分配內存頁,並將用戶程序加載至內存
    ; 外循環每次分配一個4KB內存頁,內循環每次讀取8個扇區(8*512)
    ; 分頁機制下,內存是先登記,後使用的。
    mov ecx, eax
    shr ecx, 12     ; 除以4096得到需要的4KB頁數,即循環分配的次數
    
    mov eax, sel_mem_0_4gb_seg
    mov ds, eax ; ds需要作爲入參傳給函數read_hard_disk_0
    
    mov eax, [ebp+12*4]     ; 起始扇區號
    mov esi, [ebp+11*4]     ; 從堆棧中取得TCB的基地址
 .loop_allocate_install_memory_page:
    mov ebx, [es:esi+0x06]  ; 從TCB中取得可用的線性地址
    add dword [es:esi+0x06], 0x1000 ; 加4KB即下一個可用的線性地址,寫回至TCB
    call sel_sys_routine_seg:allocate_install_memory_page
    
    push ecx
    mov ecx, 8  ; 內循環每次讀取8個扇區(8*512)
 .loop_read_hard_disk:
    call sel_sys_routine_seg:read_hard_disk_0
    inc eax
    add ebx, 512
    loop .loop_read_hard_disk       ; 循環讀
    
    pop ecx
    loop .loop_allocate_install_memory_page


    ; 從硬盤上加載整個用戶程序到已分配的物理內存中
    ; xor edx, edx
    ; mov ecx, 512
    ; div ecx                         ; 用戶程序佔硬盤的邏輯扇區個數
    ; mov ecx, eax                    ; 循環讀取的次數

    ; mov eax, sel_mem_0_4gb_seg
    ; mov ds, eax                     ; 切換ds到0~4GB的段
    
    ; mov eax, [ebp+12*4]             ; 起始扇區號
 ; .loop_read_hard_disk:
    ; call sel_sys_routine_seg:read_hard_disk_0
                                    ; ; Input: 1) eax 起始邏輯扇區號 2) ds:ebx 目標緩衝區地址
    ; inc eax
    ; add ebx, 512
    ; loop .loop_read_hard_disk       ; 循環讀    


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

    ; 創建用戶任務的局部描述符表LDT
    ; LDT是任務私有的,要在它自己的虛擬地址空間裏分配,其基地址要登記到TCB中
    mov ebx, [es:esi+0x06]  ; 從TCB中取得可用的線性地址
    add dword [es:esi+0x06], 0x1000
    call sel_sys_routine_seg:allocate_install_memory_page
    mov [es:esi+0x0c], ebx  ; 在TCB中填寫LDT線性地址

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

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

    ; 創建用戶任務的堆棧段
    ; 將數據段作爲用戶任務的3特權級固有堆棧
    ; 分配4KB內存
    mov ebx, [es:esi+0x06]  ; 從TCB中取得可用的線性地址
    add dword [es:esi+0x06], 0x1000 ; 下一個可用的線性地址,每頁4KB
    call sel_sys_routine_seg:allocate_install_memory_page

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

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


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

   
    ; 根據頭部信息創建段描述符
    ; pop edi                         ; 彈出ebx,恢復程序裝載的首地址
    ; mov edi, [es:esi+0x06]          ; 從用戶程序的TCB中取得程序裝載的首地址
    
    ; 創建ldt第#0號描述符
    ; 建立用戶程序頭部段描述符
    ; mov eax, edi                    ; 基地址
    ; mov ebx, [edi+0x04]             ; 0x04, 應用程序的頭部包含了用戶程序頭部段的長度
    ; dec ebx                         ; 粒度爲字節的段,段界限在數值上等於段長度減1
    ; mov ecx, 0x0040_f200            ; 字節粒度的數據段屬性值(無關位則置0)
                                    ; DPL 爲3,即最低的特權級
    ; call sel_sys_routine_seg:make_gdt_descriptor    ; 構建段描述符
    ; mov ebx, esi                    ; 用戶程序的任務控制塊TCB地址
    ; call setup_ldt_descriptor       ; 寫入ldt
    
    ; or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3
    ; mov [es:esi+0x44], cx           ; 登記該段的段選擇子到TCB中    
    ; mov [edi+0x04], cx              ; 0x04, 將該段的段選擇子寫回到用戶程序頭部
    
    ; 創建ldt第#1號描述符
    ; 建立用戶程序代碼段描述符
    ; mov eax, edi
    ; add eax, [edi+0x18]             ; 0x18, 應用程序的頭部包含了用戶程序代碼段的起始彙編地址
                                    ; 內核加載用戶程序的首地址,加上代碼段的起始彙編地址,得到代碼段在物理內存中的基地址
    ; mov ebx, [edi+0x1c]             ; 0x1c, 應用程序的頭部包含了用戶程序代碼段長度
    ; dec ebx                         ; 段界限
    ; mov ecx, 0x0040_f800            ; 字節粒度的代碼段屬性值(無關位則置0)
                                    ; DPL 爲3,即最低的特權級
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                    ; 用戶程序的任務控制塊TCB地址
    ; call setup_ldt_descriptor       ; 寫入ldt    
    ; or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3    
    ; mov [edi+0x18], cx              ; 0x18, 將該段的段選擇子寫回到用戶程序頭部
    ; mov [edi+0x14], cx              ; 0x14, 將該段的段選擇子寫回到用戶程序頭部
                                    ; 應用程序頭部中,和0x10處的雙字一起,共同組成一個6字節的入口點,內核從這裏轉移控制給用戶程序
    
    ; 創建ldt第#2號描述符
    ; 建立用戶程序數據段描述符
    ; mov eax, edi
    ; add eax, [edi+0x20]             ; 0x20, 應用程序的頭部包含了用戶程序數據段的起始彙編地址
                                    ; 內核加載用戶程序的首地址,加上數據段的起始彙編地址,得到數據段在物理內存中的基地址
    ; mov ebx, [edi+0x24]             ; 0x24, 應用程序的頭部包含了用戶程序數據段長度
    ; dec ebx                         ; 段界限
    ; mov ecx, 0x0040_f200            ; 字節粒度的數據段屬性值(無關位則置0)
                                    ; ; DPL 爲3,即最低的特權級
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                    ; 用戶程序的任務控制塊TCB地址
    ; call setup_ldt_descriptor       ; 寫入ldt    
    ; or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3      
    ; mov [edi+0x20], cx     
    
    ; 創建ldt第#3號描述符
    ; 建立用戶程序堆棧段描述符
    ; mov ecx, [edi+0x0c]             ; 0x0c, 應用程序的頭部包含了用戶程序棧段大小,以4KB爲單位
    ; 計算棧段的界限
    ; 粒度爲4KB,棧段界限值=0xFFFFF - 棧段大小(4KB個數), 例如 0xFFFFF-2=0xFFFFD
    ; 當處理器訪問該棧段時,實際使用的段界限爲 (0xFFFFD+1)*0x1000 - 1 = 0xFFFFDFFF
    ; 即,ESP的值只允許在0xFFFF DFFF和0xFFFF FFFF之間變化,共8KB
    ; 4KB, 即2^12=0x1000; 4GB, 即2^32; 4GB/4KB=2^20=0x10_0000, 段界限=段長-1=0xF_FFFF
    ; mov ebx, 0x000f_ffff
    ; sub ebx, ecx                    ; 段界限
    ; mov eax, 0x0000_1000            ; 粒度爲4KB
    ; 32位eax乘另一個32位,結果爲edx:eax
    ; mul dword [edi+0x0c]            ; 棧大小
    ; mov ecx, eax                    ; 準備爲堆棧分配內存, eax爲上面乘的結果,即棧大小
    ; call sel_sys_routine_seg:allocate_memory
    ; add eax, ecx                    ; 和數據段不同,棧描述符的基地址是棧空間的高端地址
    ; mov ecx, 0x00c0_f600            ; 4KB粒度的堆棧段屬性值(無關位則置0)
                                    ; DPL 爲3,即最低的特權級
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                    ; 用戶程序的任務控制塊TCB地址
    ; call setup_ldt_descriptor       ; 寫入ldt    
    ; or cx, 0000_0000_0000_0011B     ; 設置選擇子的請求特權級RPL爲3 
    ; mov [edi+0x08], cx              ; 0x08, 寫回到應用程序的頭部
    


    ; 重定位用戶程序所調用的系統API
    ; 回填它們對應的入口地址
    ; 內外循環:外循環依次取出用戶程序需調用的系統api,內循環遍歷內核所有的系統api找到用戶需調用那個
    mov eax, sel_mem_0_4gb_seg      ; 頭部段描述符已安裝,但還沒有生效,故只能通過4GB內存段訪問用戶程序頭部
    mov es, eax                     
    mov eax, sel_core_data_seg
    mov ds, eax                     ; 使ds指向mini內核數據段
    
    cld     ; 清標誌寄存器EFLAGS中的方向標誌位,使cmps指令正向比較
    ; mov ecx, [es:edi+0x28]          ; 0x28, 應用程序的頭部包含了所需調用系統API個數
                                    ; edi 前面已將其賦值爲用戶程序的起始裝載地址
                                    ; 外循環次數
    ; add edi, 0x2c                   ; 0x2c, 應用程序頭部中調用系統api列表的起始偏移地址
    
    mov ecx, [es:0x0c]  ; 0x0c, 應用程序的頭部包含了所需調用系統API個數
    mov edi, [es: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 [es: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


    ; 創建0 1 2特權級的棧
    ; 通過調用門的控制轉移通常會改變當前特權級CPL,同時還要切換到與目標代碼段特權級相同的棧。
    ; 爲此,必須爲每個任務定義額外的棧。
    ; 這些額外的棧需要登記在任務狀態段TSS中,以便處理器能夠自動訪問到。
    ; 但目前還沒有創建TSS,所以先將這些棧信息登記在任務控制塊TCB中暫存
    
    ; mov esi, [ebp+11*4]             ; 從堆棧中取得用戶程序的TCB基地址

    ; 創建0特權級堆棧
    ; mov ecx, 0x1000             ; 申請創建0特權級堆棧所需的4KB內存
    ; mov eax, ecx                ; 用於後面生成堆棧頂地址(即棧基址)
    ; mov [es:esi+0x1a], ecx      ; 登記0特權級堆棧尺寸到TCB
    ; shr dword [es:esi+0x1a], 12 ; 登記到TCB中的尺寸要求是以4KB爲單位,所以這裏需除以4KB
    
    ; call sel_sys_routine_seg:allocate_memory
    ; add eax, ecx                ; 棧頂地址(即棧基址)
    ; mov [es:esi+0x1e], eax      ; 登記0特權級堆棧基地址到TCB
    
    ; mov ebx, 0xf_fffe           ; 段界限
    ; mov ecx, 0x00c0_9600        ; 段屬性,4KB粒度 讀寫 特權級DPL爲0
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                ; TCB基地址
    ; call setup_ldt_descriptor
    ; or cx, 0000_0000_0000_0000B ; 設置選擇子的請求特權級RPL爲0
    ; mov [es:esi+0x22], cx       ; 登記0特權級堆棧選擇子到TCB
    ; mov dword [es:esi+0x24], 0  ; 登記0特權級堆棧初始esp到TCB
    
    ; 創建1特權級堆棧
    ; mov ecx, 0x1000             ; 申請創建0特權級堆棧所需的4KB內存
    ; mov eax, ecx                ; 用於後面生成堆棧頂地址(即棧基址)
    ; mov [es:esi+0x28], ecx      ; 登記0特權級堆棧尺寸到TCB
    ; shr dword [es:esi+0x28], 12 ; 登記到TCB中的尺寸要求是以4KB爲單位,所以這裏需除以4KB
    
    ; call sel_sys_routine_seg:allocate_memory
    ; add eax, ecx                ; 棧頂地址(即棧基址)
    ; mov [es:esi+0x2c], eax      ; 登記0特權級堆棧基地址到TCB
    
    ; mov ebx, 0xf_fffe           ; 段界限
    ; mov ecx, 0x00c0_b600        ; 段屬性,4KB粒度 讀寫 特權級DPL爲1
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                ; TCB基地址
    ; call setup_ldt_descriptor
    ; or cx, 0000_0000_0000_0001B ; 設置選擇子的請求特權級RPL爲1
    ; mov [es:esi+0x30], cx       ; 登記1特權級堆棧選擇子到TCB
    ; mov dword [es:esi+0x32], 0  ; 登記1特權級堆棧初始esp到TCB

    ; 創建2特權級堆棧
    ; mov ecx, 0x1000             ; 申請創建0特權級堆棧所需的4KB內存
    ; mov eax, ecx                ; 用於後面生成堆棧頂地址(即棧基址)
    ; mov [es:esi+0x36], ecx      ; 登記0特權級堆棧尺寸到TCB
    ; shr dword [es:esi+0x36], 12 ; 登記到TCB中的尺寸要求是以4KB爲單位,所以這裏需除以4KB
    
    ; call sel_sys_routine_seg:allocate_memory
    ; add eax, ecx                ; 棧頂地址(即棧基址)
    ; mov [es:esi+0x3a], eax      ; 登記0特權級堆棧基地址到TCB
    
    ; mov ebx, 0xf_fffe           ; 段界限
    ; mov ecx, 0x00c0_d600        ; 段屬性,4KB粒度 讀寫 特權級DPL爲2
    ; call sel_sys_routine_seg:make_gdt_descriptor
    ; mov ebx, esi                ; TCB基地址
    ; call setup_ldt_descriptor
    ; or cx, 0000_0000_0000_0010B ; 設置選擇子的請求特權級RPL爲2
    ; mov [es:esi+0x3e], cx       ; 登記0特權級堆棧選擇子到TCB
    ; mov dword [es:esi+0x40], 0  ; 登記0特權級堆棧初始esp到TCB
    
    ; 在GDT中登記LDT描述符, 處理器要求LDT描述符必須登記在GDT中
    mov esi, [ebp+11*4]         ; 從堆棧中取得TCB的基地址
    mov eax, [es:esi+0x0c]      ; LDT起始地址
    movzx ebx, word [es:esi+0x0a] ; LDT段界限,movzx先零擴展再傳送
    mov ecx, 0x0040_8200        ; LDT描述符屬性,特權級DPL爲0,TYPE爲2表示這是一個LDT描述符
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [es:esi+0x10], cx       ; 登記LDT選擇子到TCB中
    
    mov ebx, [es:esi+0x14]      ; 從TCB中取得TSS的基地址
    mov [es:ebx+96], cx         ; 填寫TSS的LDT域
    
    ; 創建用戶程序的TSS(Task State Segment)
    ; mov ecx, 104                ; TSS的標準大小
    ; mov [es:esi+0x12], cx       
    ; dec word [es:esi+0x12]      ; 登記TSS界限值到TCB
                                ; TSS界限值必須至少是103,任何小於該值的TSS,在執行任務切換時,都會引發處理器異常中斷
    ; call sel_sys_routine_seg:allocate_memory    ; 申請創建TSS所需的內存
    ; mov [es:esi+0x14], ecx      ; 登記TSS基地址到TCB
    
    ; 登記基本的TSS表格內容
    mov word [es:ebx+0], 0      ; 將指向前一個任務的指針(任務鏈接域)填寫爲0
                                ; 這表明這是唯一的任務
                                
    ; 登記0/1/2特權級棧的段選擇子,以及它們的初識棧指針
    ; 所有的棧信息都在TCB中,先從TCB中取出,然後填寫到TSS中的相應位置
    ; mov edx,[es:esi+0x24]       ; 登記0特權級堆棧初始ESP到TSS中
    ; mov [es:ecx+4], edx                 

    ; mov dx,[es:esi+0x22]        ; 登記0特權級堆棧段選擇子到TSS中
    ; mov [es:ecx+8], dx                  

    ; mov edx,[es:esi+0x32]       ; 登記1特權級堆棧初始ESP到TSS中
    ; mov [es:ecx+12], edx                

    ; mov dx,[es:esi+0x30]        ; 登記1特權級堆棧段選擇子到TSS中
    ; mov [es:ecx+16], dx                 

    ; mov edx,[es:esi+0x40]       ; 登記2特權級堆棧初始ESP到TSS中
    ; mov [es:ecx+20], edx                

    ; mov dx,[es:esi+0x3e]        ; 登記2特權級堆棧段選擇子到TSS中
    ; mov [es:ecx+24], dx                     
    
    ; mov dx, [es:esi+0x10]       ; 登記當前任務的LDT描述符選擇子到TSS中
    ; mov [es:ecx+96], dx         ; 任務切換時,處理器需要用這裏的信息找到當前任務的LDT
    
    ; 登記I/O許可位映射區的地址
    ; 在這裏填寫的是TSS段界限(103),表明不存在該區域
    mov dx, [es:esi+0x12]
    mov [es:ebx+102], dx
    
    mov word [es:ebx+100], 0     ; T=0
    
    ; mov dword [es:ecx+28], 0     ; 登記CR3(PDBR)

    ; 訪問用戶程序頭部,獲取數據並填充到TSS
    ; mov eax, [ebp+11*4]             ; 從堆棧中取得TCB的基地址
    ; mov edi, [es:ebx+0x06]          ; 從TCB中取得用戶程序加載的基地址
    
    ; mov edx, [es:edi+0x10]          ; 從用戶程序頭部取得程序入口點(EIP)
    ; mov [es:ecx+32], edx            ; 登記到TSS中
    
    ; mov dx, [es:edi+0x14]           ; 從用戶程序頭部取得程序代碼段(CS)選擇子
    ; mov [es:ecx+76], dx             ; 登記到TSS中

    ; mov dx, [es:edi+0x08]           ; 從用戶程序頭部取得程序堆棧段(SS)選擇子
    ; mov [es:ecx+80], dx             ; 登記到TSS中
    
    ; mov dx, [es:edi+0x04]           ; 從用戶程序頭部取得程序數據段(DS)選擇子
    ; mov word [es:ecx+84], dx        ; 登記到TSS中
    
    ; mov word [es:ecx+72],0             ;TSS中的ES=0

    ; mov word [es:ecx+88],0             ;TSS中的FS=0

    ; mov word [es:ecx+92],0             ;TSS中的GS=0
    
    mov eax, [es:0x04]  ; 從用戶程序頭部取得程序入口點,0x04    
    mov [es:ebx+32], eax    ; 填寫TSS的EIP域, 即用戶程序的入口點
    ; 從內核任務切換到用戶任務時,是用TSS中的內容恢復現場的

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


    
    ; 登記TSS描述符到GDT中
    ; 和局部描述符表LDT一樣,也必須在GDT中安裝TSS的描述符
    ; 一方面是爲了對TSS進行段和特權級的檢查,另一方面也是執行任務切換的需要
    ; 當call far和jmp far指令的操作數是TSS描述符選擇子時,處理器執行任務切換操作
    mov eax, [es:esi+0x14]      ; 從TCB中取得TSS的基地址
    movzx ebx, word [es:esi+0x12] ; TSS的界限值
    mov ecx, 0x0040_8900        ; TSS的屬性,特權級DPL爲0,字節粒度
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [es:esi+0x18], cx       ; 登記TSS描述符選擇子到TCB,RPL爲0
    
    ; 創建用戶任務的頁目錄
    ; 頁的分配和使用是由頁位圖決定的,可以不佔用線性地址空間 
    call sel_sys_routine_seg:create_uesr_pdt_by_copy
    mov ebx, [es:esi+0x14]      ; 從TCB中取得TSS的基地址
    mov dword [es:ebx+28], eax  ; 填寫TSS的CR3(PDBR)域
    
    pop es      
    pop ds
    popad
    
    ret 8       ; 丟棄調用本過程前壓入的參數
                ; 該指令執行時,除了將控制返回到過程的調用者之外,還會調整棧的指針esp=esp+8字節
    
    
    
; 內核重新接管處理器的控制權    
return_kernel:    
    mov eax, sel_core_data_seg
    mov ds, eax                 ; 使ds指向mini內核數據段
                                ; 該選擇子的請求特權級RPL爲0,目標代碼段的特權級DPL爲0
                                ; 如果當前特權級CPL爲3,低於目標代碼段DPL,將引發處理器異常中斷,也不可能通過特權級檢查
    
    ; mov eax, sel_core_stack_seg
    ; mov ss, eax                 ; 使ss指向mini內核堆棧段
    ; mov esp, [kernel_esp_pointer]
    
    mov ebx, message_kernelmode ; 顯示提示信息,已返回內核態
    call sel_sys_routine_seg:show_string
    
    ; 對於一個操作系統來說,此刻應該回收前一個用戶程序所佔用的內存,並啓動下一個用戶程序
    
    hlt     ; 進入保護模式之前,用cli指令關閉了中斷,所以,
            ; 這裏除非有NMI產生,否則處理器將一直處於停機狀態


; Function: 在TCB鏈上追加任務控制塊
; Input: ecx 需要追加的那項TCB線性基地址
append_to_tcb_link:
    push eax
    push edx
    push ds
    push es
    
    mov eax, sel_core_data_seg  ; ds 指向內核數據段, 用於定位內核數據段中定義的TCB鏈表首地址tcb_chain_head
    mov ds, eax
    mov eax, sel_mem_0_4gb_seg  ; es 指向4G內存段, 用於定位當前TCB的線性基地址
    mov es, eax
    
    mov dword [es:ecx+0x00], 0  ; 將當前TCB指針域清零,表示這是鏈表中最後一個TCB

    mov eax, [tcb_chain_head]
    or eax, eax                 ; 判斷鏈表是否爲空
    jz .emptyTCB
    
 .totailTCB:
    mov edx, eax
    mov eax, [es:edx+0x00]      ; 鏈表下一項TCB的指針域
    or eax, eax
    jnz .totailTCB
    
    mov [es:edx+0x00], ecx      ; 插入至鏈表尾部
    jmp .appendTCBsucc
    
    
 .emptyTCB:
    mov [tcb_chain_head], ecx   ; 鏈表頭部

 .appendTCBsucc:
    pop es
    pop ds
    pop edx
    pop eax

    ret



; Function: 在ldt中安裝一個新的段描述符
; Input: edx:eax 段描述符; ebx 任務控制塊TCB基地址
; Output: cx 段描述符的選擇子
setup_ldt_descriptor:
    push eax
    push edx
    push edi
    push ds
    
    mov ecx, sel_mem_0_4gb_seg
    mov ds, ecx
    
    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 ds
    pop edi
    pop edx
    pop eax

    ret

    


    
core_code_end:

    
    
; ===============================================================================
SECTION core_data vstart=0               ; mini內核數據段

; sgdt, Store Global Descriptor Table Register
; 將gdtr寄存器的基地址和邊界信息保存到指定的內存位置
; 低2字節爲gdt界限(大小),高4字節爲gdt的32位物理地址
; lgdt, load gdt, 指令的操作數是一個48位(6字節)的內存區域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0

; 內存分配時的起始地址
; 每次請求分配內存時,返回這個值,作爲所分配內存的起始地址;
; 同時,將這個值加上所分配的長度,作爲下次分配的起始地址寫回該內存單元
ram_allocate_base dd 0x0010_0000

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

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

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



; 提示信息,內核已加載成功並開始執行
message_kernel_load_succ db '  If you seen this message,that means we '
            db 'are now in protect mode,and the system '
            db 'core is loaded,and the video display '
            db 'routine works perfectly.', 0x0d, 0x0a, 0

; 提示信息,分頁功能已開啓,而且內核已被映射到線性地址0x8000_0000以上
message_pagemode_load_succ  db '  Paging is enabled.System core is mapped to'
                            db  ' address 0x80000000.',0x0d,0x0a,0

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

; 提示信息,開始加載用戶程序            
message_app_load_begin  db '  Loading user program...', 0

; 提示信息,用戶程序加載並重定位完成
message_app_load_succ   db 'Done.', 0x0d, 0x0a, 0

message_kernelmode      db  0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
                        db  '  User program terminated,control returned.',0

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



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

core_buf    times 2048 db 0             ; 自定義的內核緩衝區
    
kernel_esp_pointer  dd 0                ; 臨時保存內核的堆棧指針

bin_hex     db '0123456789ABCDEF'       ; show_hex_dword過程需要的查找表

; 任務控制塊TCB鏈表
tcb_chain_head   dd 0

; 內核需記住下一個可用於分配的線性地址
; 這裏內核主體部分佔據着線性地址空間0x8000_0000~0x800f_ffff的1MB空間
; 在此之後的空間0x8010_0000~0xffff_ffff,是可以自由分配的
; 每當分配了新的內存空間後,該雙字修正爲下一個可分配的地址
core_next_laddr     dd 0x8010_0000     

; 程序管理器的任務信息
; 0特權級的內核任務
prgman_tss      dd 0        ; 基地址
                dw 0        ; 描述符選擇子
                
prgman_msg1     db 0x0d,0x0a
                db '[PROGRAM MANAGER]: Hello! I am Program Manager,'
                db 'run at CPL=0.Now,create user task and switch '
                db 'to it by the CALL instruction...',0x0d,0x0a,0

prgman_msg2     db  0x0d,0x0a
                db  '[PROGRAM MANAGER]: I am glad to regain control.'
                db  'Now,create another user task and switch to '
                db  'it by the JMP instruction...',0x0d,0x0a,0

prgman_msg3     db  0x0d,0x0a
                db  '[PROGRAM MANAGER]: I am gain control again,'
                db  'HALT...',0                

core_msg_call   db  0x0d,0x0a
                db  '[SYSTEM CORE]: Uh...This task initiated with '
                db  'CALL instruction or an exeception/ interrupt,'
                db  'should use IRETD instruction to switch back...'
                db  0x0d,0x0a,0

core_msg_jmp    db  0x0d,0x0a
                db  '[SYSTEM CORE]: Uh...This task initiated with '
                db  'JMP instruction,  should switch to Program '
                db  'Manager directly by the JMP instruction...'
                db  0x0d,0x0a,0


core_data_end:


                    
; ===============================================================================
SECTION sys_routine vstart=0               ; 系統api代碼段

; Function: 頻幕上顯示文本,並移動光標
; Input: ds:ebx 字符串起始地址,以0結尾
show_string:
    push ecx
 .loop_show_string:
    mov cl, [ebx]
    or cl, cl
    jz .exit                ; 以0結尾
    call show_char
    inc ebx
    jmp .loop_show_string
    
 .exit:
    pop ecx
    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位數
    
 ; 判斷是否爲回車符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:
    push es
    
    mov eax, sel_video_ram_seg  ; 0xb8000段的選擇子,顯存映射在 0xb8000~0xbffff
    mov es, eax
    shl bx, 1       ; 光標指示字符位置,顯存中一個字符佔2字節,光標位置乘2得到該字符在顯存中得偏移地址    
    mov [es:bx], cl
    
    pop es
    
    shr bx, 1       ; 恢復bx
    inc bx          ; 將光標推進到下一個位置
    
 ; 判斷是否需要向上滾動一行屏幕
 .roll_screen:
    cmp bx, 2000    ; 25行x80列
    jl .set_cursor
    
    push ds
    push es
    
    mov eax, sel_video_ram_seg    
    mov ds, eax      ; movsd的源地址ds:esi
    mov es, eax      ; movsd的目的地址es:edi
    mov esi, 0xa0
    mov edi, 0
    cld             ; 傳送方向cls std
    mov cx, 1920    ; rep次數 24行*每行80個字符*每個字符加顯示屬性佔2字節 / 一個字爲2字節
    rep movsd
    
    ; 清除屏幕最底一行,即寫入黑底白字的空白字符0x0720
    mov bx, 3840    ; 24行*每行80個字符*每個字符加顯示屬性佔2字節
    mov cx, 80
 .cls:
    mov word [es:bx], 0x0720
    add bx, 2
    loop .cls
    
    pop es
    pop ds
    
    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:

    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
    
    retf        ; 段間返回




; ===============================================================================    
; Function: 分配內存
; Input: ecx 希望分配的字節數
; Output: ecx 起始地址
allocate_memory:

    push eax
    push ebx
    push ds
    
    mov eax, sel_core_data_seg
    mov ds, eax                     ; 切換ds到內核數據段
    
    mov eax, [ram_allocate_base]
    add eax, ecx                    ; 下次分配時的起始地址    
    
    ; 這裏應當檢測可用內存數量,但本程序很簡單,就忽略了
    
    mov ecx, [ram_allocate_base]    ; 返回分配的起始地址
    
    ; 4字節對齊下次分配時的起始地址, 即最低2位爲0
    ; 32位的系統建議內存地址最好是4字節對齊,這樣訪問速度能最快
    mov ebx, eax
    and ebx, 0xffff_fffc
    add ebx, 4                      ; 4字節對齊
    test eax, 0x0000_0003           ; 判斷是否對齊
    cmovnz eax, ebx                 ; 如果非零,即沒有對齊,則強制對齊
                                    ; cmovcc避免了低效率的控制轉移
    mov [ram_allocate_base], eax    ; 下次分配時的起始地址
    
    pop ds
    pop ebx
    pop eax

    retf        ; 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
    push ds
    push es
    
    mov ebx, sel_core_data_seg  ; 切換ds到內核數據段
    mov ds, ebx
    
    ; sgdt, Store Global Descriptor Table Register
    ; 將gdtr寄存器的基地址和邊界信息保存到指定的內存位置
    ; 低2字節爲gdt界限(大小),高4字節爲gdt的32位物理地址
    sgdt [gdt_size]
    
    mov ebx, sel_mem_0_4gb_seg
    mov es, ebx                 ; 使es指向4GB內存段以操作全局描述符表gdt
    
    ; 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 [es:ebx], eax
    mov [es: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 es
    pop ds
    pop edx
    pop ebx
    pop eax
    
    retf
    
    
    
; ===============================================================================    
; Function: 將ds的值以十六進制的形式在屏幕上顯示
; Input: 
; Output: 
show_hex_dword:
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    push ds
    
    mov ax, sel_core_data_seg
    mov ds, ax

    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
    
    pop ds
    popad
    
    retf
    
; ===============================================================================    
; 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: 終止當前任務,並轉換到其他任務
; Input: 
; Output: 
terminate_current_task:
; 現在仍處在用戶任務中,要結束當前的用戶任務,可以先切換到程序管理器任務,然後回收用戶程序所佔用的內存空間
; 爲了切換到程序管理器任務,需要根據當前任務的EFLAGS寄存器的NT位決定是採用iret指令,還是jmp指令

    pushfd          ; 將EFLAGS寄存器內容壓棧
    mov edx, [esp]  ; 獲得EFLAGS寄存器內容
    add esp, 4      ; 恢復堆棧指針。這2條指令等同於pop edx
    
    mov eax, sel_core_data_seg
    mov ds, eax     ; 使ds指向mini內核數據段
    
    ; 根據當前任務的EFLAGS寄存器的NT位決定是採用iret指令,還是jmp指令
    ; 此時dx寄存器包含了標誌寄存器EFLAGS的低16位,其中,位14是NT位
    test dx, 0x4000 ; 測試NT位
    jnz .nt1_iret
    
    ; NT位爲0
    ; 當前任務不是嵌套的,直接jmp切換
    ; mov ebx, core_msg_jmp
    ; call sel_sys_routine_seg:show_string
    jmp far [prgman_tss]    ; 程序管理器任務
    
 .nt1_iret:
    ; NT位爲1
    ; 當前任務是嵌套的,即使用的是call指令,需執行iretd指令切換回去
    ; mov ebx, core_msg_call
    ; call sel_sys_routine_seg:show_string
    iretd   ; 通過iretd指令轉換到前一個任務
            ; 執行任務切換時,當前用戶任務的TSS描述符的B位被清零,
            ; EFLAGS寄存器的NT位也被清零,並被保存到它的TSS中
            ; 當程序管理器任務恢復執行時,它所有原始狀態都從TSS中加載到處理器,包括指令指針寄存器EIP


; ===============================================================================    
; Function: 申請一個4KB物理頁
; Input: 
; Output: eax, 頁的物理地址 
allocate_memory_page:
; 搜索頁映射位串查找空閒的頁,並分配頁
    push ebx
    push ecx ; ???
    push edx ; ???
    push ds 
    
    mov eax, sel_core_data_seg  
    mov ds, eax     ; 使ss指向mini內核數據段,用於定位page_bitmap

    ; 從頁映射位串的第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_sys_routine_seg:show_string
    hlt
 
 .done:
    shl eax, 12 ; 將該比特在位串中的位置數值乘以每個頁的大小4KB,就是該比特對應的那個頁的物理地址
    
    pop ds
    pop edx
    pop ecx
    pop ebx
    
    ret ; 這是段內的內部過程,僅供同一段內的其他過程使用


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

; 在可用的物理內存中搜索空閒的頁,然後根據線性地址來創建頁目錄項和頁表項,並將頁的地址填寫在頁表項中
    push eax
    push ebx
    push esi
    push ds
    
    mov eax, sel_mem_0_4gb_seg
    mov ds, eax
    
    ; 訪問頁目錄表,檢查該線性地址對應的頁目錄項是否存在
    ; 分頁機制下,訪問內存需要通過頁目錄表和頁表,而這裏卻要訪問頁目錄表
    ; 要先得到要修改的那個頁目錄項的線性地址,把頁目錄當作普通頁來訪問
    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 ds
    pop esi
    pop ebx
    pop eax
    
    retf


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

; 創建新的頁目錄,並複製當前頁目錄內容
    push ebx
    push ecx
    push esi
    push edi
    push ds
    push es
    
    mov ebx, sel_mem_0_4gb_seg
    mov ds, ebx
    mov es, ebx
    
    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
    
    mov esi, 0xffff_f000    ; 當前頁目錄的線性地址
    mov edi, 0xffff_e000    ; 新頁目錄的線性地址
    mov ecx, 1024           ; 傳送次數
    cld                     ; 傳送方向爲正向    
    repe movsd
    
    pop es
    pop ds
    pop edi
    pop esi
    pop ecx
    pop ebx
    
    retf

                                
sys_routine_end:


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

    

 

# file_02: c16.asm

; FILE: c16.asm
; DATE: 20200203
; TITLE: 用戶程序

; ===============================================================================
; 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
;--------------------------------------------------------------------

         reserved  times 256*500 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 0x0d, 0x0a
                    db '**********User program is runing**********'
                    db 0x0d,0x0a, 0
                    
space db 0x20, 0x20, 0  ; 兩個空格
; space db '  ', 0

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


; ===============================================================================
; SECTION code vstart=0                       ; 定義用戶程序代碼段
beginning:
    ; mov eax, ds     ; 進入用戶程序時,ds指向頭部段
    ; mov fs, eax     ; 使fs指向頭部段,目的是保存指向頭部段的指針以備後用
    
    ; 棧的相關信息已在執行任務切換時完成,包括ss和esp寄存器   
    ; mov eax, [segment_stack]
    ; mov ss, eax     ; ss切換到用戶程序自己的堆棧,並初始化esp爲0    
    ; mov esp, 0
    
    ; mov eax, [segment_data]
    ; mov ds, eax     ; ds切換到用戶程序自己的數據段    
    
    ; 調用系統API
    ; 顯示提示信息,正在運行的用戶程序的當前特權級CPL
    mov ebx, message_usermode
    call far [ShowString]   

    ; 以十六進制形式顯示當前任務4GB虛擬地址空間內的前88個雙字
    ; 每次先顯示2個空格,然後再顯示雙字的值
    ; 顯示效果爲每行8個雙字,共11行
    xor esi, esi
    mov ecx, 88
 .loop_show_laddr_space:
    mov ebx, space
    call far [ShowString]   ; 顯示兩個空格
    
    mov edx, [esi*4]
    call far [ShowDwordAsHexString] ; 顯示雙字    
    
    inc esi
    loop .loop_show_laddr_space

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

; code_end:

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

 

 

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