[书]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:    
    
    

 

 

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