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

 

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