# 執行結果
# 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: