[書]x86彙編語言:從實模式到保護模式 -- 第15章 任務切換

# 執行結果

# TODO:字符串顯示函數的滾屏部分應該是有bug。

# file_02: c15_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:
    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
    
    ; 安裝整個系統服務的調用門。特權級之間的控制轉移必須使用門
    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基地址
    
    ; 程序管理器TSS的基本設置
    mov word [es:ecx+96], 0         ; 沒有LDT。這裏是將所有的段描述符安裝在GDT中
    ; 登記I/O許可位映射區的地址
    ; 在這裏填寫的是TSS段界限(103),表明不存在該區域
    mov word [es:ecx+102], 103      ; 沒有I/O位圖。事實上0特權級不需要
    mov word [es:ecx+0], 0          ; 反向鏈=0
    mov dword [es:ecx+28], 0        ; 登記CR3(PDBR)
    mov word [es:ecx+100], 0        ; T=0。不需要0 1 2特權級堆棧,0特權級不會向低特權級轉移控制
    
    ; 創建TSS描述符,並安裝到GDT中
    mov eax, ecx        ; 起始地址
    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

    ; 現在可認爲"程序管理器"任務正在執行中
    ; 顯示提示信息,任務管理器正在執行中
    mov ebx, prgman_msg1
    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選擇子
    ; 處理器用得到的選擇子訪問GDT,當發現得到的是一個TSS描述符,就執行任務切換
    ; 首先,會把每個寄存器的快照保存到由TR指向的TSS中;然後,從新任務的TSS描述符中恢復各個寄存器的內容;最後,任務寄存器TR指向新任務的TSS,處理器開始執行新的任務
    ; 任務切換時要恢復TSS內容,所以在創建任務時TSS要填寫完整
    
    call far [es:ecx+0x14]

    ; 理論上這裏還需要回收舊任務所佔用的內存空間,並從任務控制塊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的段

    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的倍數
    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                        ; 用於後面訪問用戶程序頭部

    ; 從硬盤上加載整個用戶程序到已分配的物理內存中
    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       ; 循環讀    
    
    ; 根據頭部信息創建段描述符
    ; 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列表的起始偏移地址
 .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描述符
    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中
    
    ; 創建用戶程序的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:ecx+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:ecx+102], dx
    
    mov word [es:ecx+100], 0     ; T=0
    
    mov dword [es:ecx+28], 0     ; 登記CR3(PDBR)

    ; 訪問用戶程序頭部,獲取數據並填充到TSS
    mov ebx, [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


    ; 將標誌寄存器EFLAGS內容寫入TSS中的EFLAGS域
    pushfd      ; 將EFLAGS寄存器內容壓棧
    pop edx     ; 彈出到edx
    mov dword [es:ecx+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
    
    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

; 系統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

; 提示信息,開始加載用戶程序            
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


; 程序管理器的任務信息
; 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

                                
sys_routine_end:


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

    

 

# file_03: c15.asm

; FILE: c15.asm
; DATE: 20200127
; TITLE: 用戶程序

; ===============================================================================
SECTION head vstart=0                       ; 定義用戶程序頭部段
    ; 用戶程序可能很大,16位可能不夠
    program_length  dd program_end      ; 程序總長度[0x00]
    
    head_length     dd head_end         ; 程序頭部的長度[0x04]
                                        ; 以字節爲單位
    
    ; 由內核動態分配棧空間
    ; 當內核分配了棧空間後,會把棧段的選擇子填寫到這裏,用戶程序開始執行時從這裏讀取該選擇子
    segment_stack   dd 0                ; 存放棧段選擇子[0x08]    
    stack_length    dd 1                ; 用戶程序編寫者建議的棧大小[0x0c]
                                        ; 以4KB爲單位

    ; 程序入口點(Entry Point)
    program_entry   dd beginning        ; 偏移地址[0x10]
                    ; 編譯階段確定的起始彙編地址
                    ; 當內核完成對用戶程序的加載和重定位後,把該段的選擇子回填到這裏(僅佔用低字節部分)
                    ; 和0x10處的雙字一起,共同組成一個6字節的入口點,內核從這裏轉移控制給用戶程序
                    dd section.code.start ; 彙編地址[0x14]    
    
    segment_code    dd section.code.start; [0x18]
    code_length     dd code_end          ; [0x1c]    
    
    segment_data    dd section.data.start; [0x20]
    data_length     dd data_end          ; [0x24]

    ; 所需調用的系統API
    ; 自定義規則:用戶程序在頭部偏移量爲0x30處構造一個表格,並列出所有要用到的符號名
    ; 每個符號名的長度是256字節,不足部分用0x00填充
    ; 內核加載用戶程序時,會將每一個符號名替換成相應的內存地址,即重定位
    ; 符號-地址檢索表,Symbol-Address Lookup Table, SALT
    salt_itmes      dd (head_end-salt)/256; [0x28]
    
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
                    
head_end:


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

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

; 提示信息,正在運行用戶程序
message_usermode    db 0x0d, 0x0a
                    db '**********User program is runing**********'
                    db 0x0d,0x0a
                    db  '[USER TASK]: Hi! nice to meet you,'
                    db  'I am run at CPL=',0

; 提示信息
message_2   db  0
            db  '.Now,I must exit...',0x0d,0x0a,0
                    
data_end:

; ===============================================================================
[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 [fs:ShowString]   

    ; 計算當前特權級,轉換成ASCII碼後填寫到數據段中
    ; 當前特權級由段寄存器CS當前內容的低2位指示
    mov ax, cs
    and al, 0000_0011B
    or al, 0x30         ; 轉換爲ASCII碼,即加上48
    mov [message_2], al
    
    mov ebx, message_2
    call far [fs:ShowString]

    ; 調用系統API, 返回內核   
    call far [fs:TerminateProgram]

code_end:

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

 

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