[书]x86汇编语言:从实模式到保护模式 -- 第17章 中断、任务切换、分页机制、平坦模型

# 任务切换

内核任务、用户任务1、用户任务2,之前的轮询切换

利用RTC芯片的硬件中断来实现任务切换

    计算机主板上有实时时钟芯片RTC,可以设置RTC芯片,使得它每次更新CMOS中的时间信息后,发出更新周期结束的中断信号0x70;

    编写0x70号中断处理程序,操作TCB链表,实现任务切换。

    操作TCB链表:找到当前任务(即,状态为忙的任务)、将该任务从TCB链表的当前位置删除并添加至链表末尾、遍历TCB链表找到第一个空闲的任务、重新设置当前任务和所找到的空闲任务的忙碌状态、跳转到新任务去执行

备注:主流的操作系统一般都不会用处理器固件来实现任务切换,因为这种方法代价比较高,可以用软件算法代替硬件实现任务切换

; -----------------------------------------------------------------
; Function: 实时时钟中断处理过程,实现任务切换
;           利用硬件中断实现任务切换
; Input: 
handler_interrupt_rtm_0x70:
; 计算机主板上有实时时钟芯片RTC,可以设置RTC芯片,使得它每次更新CMOS中的时间信息后,发出更新周期结束的中断信号,从而进行任务切换
; 其实,用实时时钟更新周期结束中断来实施任务切换并不是一个好主意,和别的中断相比,它更啰嗦

    pushad
    
    ; 向8259A芯片发送中断结束命令EOI(End of Interrupt)
    ; 否则,它不会再向处理器发送另一个中断通知
    mov al, 0x20    
    out 0xa0, al    ; 向8259A从片发送
    out 0x20, al    ; 向8259A主片发送
    
    ; 必须读一下CMOS芯片内的寄存器C,使它复位一下, 才能使RTC产生下一个中断信号。否则,它只产生一次中断信号
    mov al, 0x0c    ; 寄存器C的索引,且放开NMI
    out 0x70, al
    in al, 0x71     ; 读一下RTC的寄存器C
    
    ; 找到当前任务(状态为忙的任务)在链表中的位置,即状态值为0xFFFF的节点
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    or ebx, ebx
    jz .irtn    ; 链表为空,或已到末尾,从中断返回
    cmp word [ebx+0x04], 0xffff     ; 判断状态是否为忙,即是否为当前任务
    je .move_current_to_chaintail
    mov eax, ebx ; TCB内偏移为0x00处,是下一个TCB的线性地址
    jmp .search_current_task
    
    ; 把当前任务(状态为忙的任务)移到链尾
 .move_current_to_chaintail:
    ; 从链表中删除该节点
    mov ecx, [ebx]  ; 下一个TCB节点的线性地址
    mov [eax], ecx  ; 将当前任务从TCB链表中删除
 .goto_chaintail:
    ; 遍历至链表尾部
    mov edx, [eax]
    or edx, edx     ; 判断是否已到链表尾部
    jz .add_current_to_chaintail
    mov eax, edx
    jmp .goto_chaintail
 .add_current_to_chaintail:
    ; 将该节点添加至链表尾部
    mov [eax], ebx
    mov dword [ebx], 0 ; 将当前任务标记为链尾。TCB内偏移为0处,是下一个TCB的线性地址
    
    ; 从TCB链表中搜索第一个空闲任务
    mov eax, tcb_chain_head
 .search_next_task:
    mov eax, [eax]
    or eax, eax     ; 已到链尾,即未发现空闲任务
    jz .irtn        ; 从中端返回
    cmp word [eax+0x04], 0x0000     ; 是空闲任务。空闲为0,忙为0xffff
    jnz .search_next_task
    
    ; 将空闲任务和当前任务的状态都取反
    not word [eax+0x04]     ; 设置空闲任务的状态为忙
    not word [ebx+0x04]     ; 设置当前任务的状态为空闲
    
    ; 任务切换
    jmp far [eax+0x14] ; TCB内偏移为0x14处,依次是该任务TSS的线性地址和TSS描述符选择子

 .irtn:
    popad   ; 当前任务再次获得执行权时的返回点
    iretd   ; 从中断返回,正常执行任务的其他代码

# 执行结果

内核任务、用户任务1、用户任务2,这三个任务不断地切换执行。

# file_01: c17_mbr.asm    主引导程序代码,Mast Boot Record

; FILE: c17_mbr.asm
; TITLE: 硬盘主引导扇区代码
; DATE: 20200206 

; 内核程序的大小是不确定的,但可以规定它的起始位置
core_base_address   equ 0x00040000  ; 自定义mini内核加载的起始内存地址,即64KB
core_begin_sector   equ 1           ; 自定义mini内核的起始逻辑扇区号

; ===============================================================================
SECTION mbr vstart=0x7c00   ; 主引导程序的加载位置是物理地址0x7c00
; 如果没有vstart子句,所有标号的地址都以程序开头为基准,从0开始计算
; 有vstart子句时,标号所代表的地址就以程序开头为基准,从给定的虚拟地址开始计算

; 设置堆栈段和指针
; 在32位处理器上,即使是在实模式下,也可以使用32位寄存器
; mov ax, cs
; mov ss, ax
mov eax, cs
mov ss, eax
mov sp, 0x7c00

; 计算GDT所在的逻辑段地址
; 从标号gdt_base处取得自定义的GDT物理地址,并计算它在实模式下的段地址和偏移地址
; 64位的被除数在edx:eax中,商为eax,余数为edx
mov eax, [cs:gdt_base]
xor edx, edx
mov ebx, 16
div ebx
mov ds, eax         ; 商eax为段地址,仅低16位有效
mov ebx, edx        ; 余数edx为偏移地址,仅低16位有效


; 进入保护模式之前,初始化程序(mbr主引导程序)需在gdt中安装必要的描述符
; =====================================================================

; 创建gdt第#0号描述符
; 处理器规定,gdt中第一个描述符必须是空描述符
; 这2行代码也可不写
mov dword [ebx], 0x00000000
mov dword [ebx+0x04], 0

; 创建gdt第#1号描述符,保护模式下的代码段描述符
; 该段:线性基地址为0,段界限为0xF FFFF,DPL=00
;       粒度为4KB,向上扩展
mov dword [ebx+0x08], 0x0000_ffff     
mov dword [ebx+0x0c], 0x00cf_9800

; 创建gdt第#2号描述符,保护模式下的数据段和堆栈段描述符
; 该段:线性基地址为0,段界限为0xF FFFF,DPL=00
;       粒度为4KB,向上扩展
mov dword [ebx+0x10], 0x0000_ffff     
mov dword [ebx+0x14], 0x00cf_9200

; 初始化描述符表寄存器GDTR
; 描述符表的界限(总字节数减1)。这里共有5个描述符,每个描述符8字节,共40字节
mov word [cs:gdt_size], 23


; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
; GDTR, 全局描述符表寄存器
lgdt [cs:gdt_size]

; 打开地址线A20
; 芯片ICH的处理器接口部分,有一个用于兼容老式设备的端口0x92,端口0x92的位1用于控制A20 
in al, 0x92
or al, 0000_0010B
out 0x92, al

; 禁止中断,中断机制尚未工作
; 保护模式和实模式下的中断机制不同,在重新设置保护模式下的中断环境之前,必须关中断
cli

; 开启保护模式
; CR0的第1位(位0)是保护模式允许位(Protection Enabel, PE)
mov eax, cr0
or eax, 1
mov cr0, eax

; 清空流水线、重新加载段选择器
; 处理器建议,在进入保护模式后,执行的第一条指令应当是跳转或者过程调用指令,以清空流水线和乱序执行的结果,并串行化处理器。
; 遇到jmp或call指令,处理器一般会清空流水线,另一方面,还会重新加载段选择器,并刷新描述符高速缓存器中的内容
; 建议:在设置了控制寄存器CR0的PE位之后,立即用jmp或call指令
jmp dword 0x0008:flush      ; 16位的描述符选择子:32位偏移
                            ; 不管是16位还是32位远转移,现在已经处于保护模式下,
                            ; 处理器将把第一个操作数0x0008视为段选择子,而不是是模式下的逻辑段地址
                            ; 段选择子0x0008,即 0000_0000_00001_0_00(RPL为00,TI为0,索引号为1)
                            ; 当指令执行时,处理器加载CS,从GDT中取出相应的描述符加载到CS描述符高速缓存
                            ; jmp dword, 32位的远转移指令。
                            ; 16位的绝对远转移指令只有5字节,使用16位的偏移量,它会使标号flush的汇编地址相应地变小
                            
[bits 32]       ; 从进入保护模式开始,之后的指令都应当按32位操作数方式编译
                ; 当处理器执行到这里时,它会按32位模式进行译码

flush:
    ; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)
    ; mov eax, 0x0010
    mov eax, 0000_0000_00010_0_00B   ; 已初始化的GDT中,数据段为第2号描述符
    ; 因为平坦模式下所有段都指向4GB数据段,只不过栈段向下增长,其他各段都向上增长
    mov ds, eax  ; 当处理器执行任何改变段选择器的指令时,就将指令中提供的索引号乘以8作为偏移地址,同GDTR中提供的线性地址相加,
                 ; 以访问GDT,将找到的描述符加载到不可见的描述符高速缓存部分

    ; 设置堆栈段ss和段指针esp
    mov es, eax
    mov fs, eax
    mov gs, eax
    mov ss, eax
    mov esp, 0x7000     ; 栈从地址0x7000开始向低地址方向扩展
    

    ; 加载mini内核
    ; 从硬盘把内核程序读入内存
    mov edi, core_base_address      ; 自定义的mini内核物理内存地址
    mov eax, core_begin_sector      ; 自定义的mini内核在硬盘上的起始逻辑扇区号
    mov ebx, edi
    
    call read_hard_disk_0           ; 先读一个扇区
                                    ; 包含了头部信息:程序大小、入口点、段重定位表
    
    ; 判断需要加载的整个程序有多大
    mov eax, [edi]                  ; 0x00, 应用程序的头部包含了程序大小
    xor edx, edx
    mov ecx, 512
    div ecx
    cmp edx, 0               
    jnz @1
    dec eax                         ; 余数edx为0则商eax减1,已读取一个扇区    
    
 @1:
    cmp eax, 0
    jz setup_page           ; 实际长度小于512字节,则已读取完
    
    ; 读取剩余的扇区    
    mov ecx, eax            ; 循环次数(剩余扇区数)
    mov eax, core_begin_sector
    inc eax                 ; 读取下一个逻辑扇区
 @2:    
    add ebx, 512            ; 每次读时,指向物理内存的下一个512字节
    call read_hard_disk_0
    inc eax                 ; 下一个扇区
    loop @2
    
setup_page:
;准备打开分页机制。从此,再也不用在段之间转来转去
    
    ; 创建系统内核的页目录表PDT
    mov ebx, 0x2_0000   ; 自定义页目录表的物理地址为0x20000,即128KB
    
    ; 创建PDT的第1023号表项,指向页目录表本身0x20000
    ; 令最后一个页目录项指向页目录表自己,便于修改页目录表本身
    ; 0x0002_0003, 前20位是物理地址的高20位;P=1,页位于内存中;RW=1,该目录项指向的页表可读可写;
    ;   US位为1,此目录项指向的页表不允许特权级为3的程序和任务访问
    mov dword [ebx+4092], 0x2_0003  ; 索引1023*每个表项4字节,得偏移
    
    ; 创建PDT的第0号表项,指向系统内核的页表0x21000
    ; 这个目录项只在开启页功能得时候使用,作为临时过渡
    ; 后续使用全局地址空间,即高2GB指向内核
    mov dword [ebx+0], 0x2_1003
    
    ; 创建与线性地址0x8000_0000对应的目录项
    ; 作为高2GB的全局空间指向内核
    mov dword [ebx+0x800], 0x2_1003 ; 0x800/4 * 1024*4KB = 0x800 00000, 即2GB
    
    
    ; 初始化内核页表0x21000
    ; 将内存低端1MB所包含的那些页的物理地址按顺序一个一个地填写到页表中
    ; 这里的mini内核占用着内存的低端1MB,即256个页表项
    mov ebx, 0x2_1000       ; 页表的物理地址0x21000
    xor eax, eax            ; 起始页的物理地址0x0
    xor esi, esi
 .pt_kernel_pre256:
    mov edx, eax
    or edx, 0x0000_0003     ; 低12位为页属性
                            ; 属性值3,P=1, RW=1, US=0
    
    mov [ebx+esi*4], edx    ; 在页表中登记页的物理地址
    add eax, 0x1000         ; 下一个相邻页的物理地址,每个页4KB
    inc esi                 ; 页表的下一个页表项
    cmp esi, 256
    jl .pt_kernel_pre256    
    
    ; 将上面内核页表的其余页表项置为无效
 .pdt_1_others:
    mov dword [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

    ; 将GDT中的段描述符映射到线性地址0x8000_0000,即内核全局空间
    sgdt [gdt_size]
    mov ebx, [gdt_base]
    add dword [gdt_base], 0x8000_0000 ; 全局描述符表寄存器GDTR也用的是线性地址
    lgdt [gdt_size] ; 将修改后的GDT基地址和界限值加载到GDTR
    
    ; 开启分页机制
    ; 从此,段部件产生的地址就不再被看成物理地址,而是要送往页部件进行变换,以得到真正的物理地址
    mov eax, cr0
    or eax, 0x8000_0000
    mov cr0, eax
    ; 这里切换至分页模式后,不需要重新加载各个段寄存器以刷新它们的描述符高速缓存器,因为所有这些段都是4GB的

; 将内核栈映射到高端,这是非常容易被忽略的一件事。应当把内核的所有东西都移到高2GB
; 否则,一定会和正在加载的用户任务局部空间里的内容冲突,而且很难想到问题会出在这里
    add esp, 0x8000_0000
    
    ; 跳转到内核
    ; 内核已从硬盘上加载,线性地址为0x8004_0000
    ; 从线性地址0x8004_0004处取得一个32位段内偏移,传送到eip寄存器
    jmp [0x8004_0004]   ; 32位段内转移
                        ; 0x04, 内核头部包含了mini内核入口点地址
                        

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



; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0x00008000


times 510-($-$$) db 0
                 db 0x55, 0xaa

# file_02: c17_core.asm    内核代码

; FILE: c17_core.asm
; TITLE: mini内核
; DATE: 20200207

; 常数的定义仅在编译期间有用,编译之后不占用任何地址空间
; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)
sel_code_4gb_seg        equ 0x08 ; gdt第1号描述符
sel_data_4gb_seg        equ 0x10 ; gdt第2号描述符,未使用到

idt_linear_base     equ 0x8001_f000     ; 中断描述符表的线性基地址

; -----------------------------------------------------------------
; 定义宏

; 在内核空间中分配虚拟内存
%macro allocate_memory_kernel 0
    mov ebx, [kernel_tcb+0x06]  ; 0x06, TCB中包含下一个可分配的线性地址
    add dword [kernel_tcb+0x06], 0x1000 ; 写回下一个可分配的线性地址。每次分配一个页(4KB)
    call sel_code_4gb_seg:allocate_install_memory_page
%endmacro

; 在用户空间中分配虚拟内存
%macro allocate_memory_user 0
    mov ebx, [esi+0x06]  ; 0x06, TCB中包含下一个可分配的线性地址
    add dword [esi+0x06], 0x1000 ; 写回下一个可分配的线性地址。每次分配一个页(4KB)
    call sel_code_4gb_seg:allocate_install_memory_page
%endmacro




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


; ===============================================================================
SECTION core_code vstart=0x8004_0000               ; mini内核代码

; mini内核的头部,用于mbr加载mini内核
core_length         dd core_end                     ; mini内核总长度, 0x00
core_entry          dd beginning                    ; mini内核入口点(32位的段内偏移地址),0x04    


beginning:

; 创建保护模式下的中断系统
; 保护模式下的中断机制不同于实模式,因此,在进入保护模式之前,已经用cli指令关掉了外部硬件中断,以免出现问题
; 只有在创建好了中断描述符表,并安装了中断处理程序之后,才能使用sti指令放开硬件中断


    ; 前20个向量是处理器异常使用的    
    ; 创建函数handler_exception_general的中断门描述符
    mov eax, handler_exception_general  ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段选择子
    mov cx, 0x8e00      ; 属性值,32位的中断门,特权级为0
    call sel_code_4gb_seg:make_gate_descriptor
    ; 这里还是采用远调用,函数也是用retf返回
    ; 但该函数其实可以定义成用ret指令返回的近过程,因为内核不会允许3特权级的用户任务调用该过程
    
    ; 在IDT中安装前20个描述符,都指向通用异常处理程序handler_exception_general
    mov ebx, idt_linear_base
    xor esi, esi
 .idt0_19:
    mov [ebx+esi*8], eax
    mov [ebx+esi*8+4], edx
    inc esi
    cmp esi, 19
    jle .idt0_19

    ; 20~255中断向量是Intel保留的中断向量,以及外部硬件中断
    ; 创建函数handler_interrupt_general的中断门描述符    
    mov eax, handler_interrupt_general  ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段选择子
    mov cx, 0x8e00      ; 属性值,32位的中断门,特权级为0
    call sel_code_4gb_seg:make_gate_descriptor

    ; 在IDT中安装0~255描述符,都指向通用中断处理程序handler_interrupt_general
    mov ebx, idt_linear_base
 .idt20_255:
    mov [ebx+esi*8], eax
    mov [ebx+esi*8+4], edx
    inc esi
    cmp esi, 255
    jle .idt20_255

    ; 设置实时时钟中断处理过程,用于
    ; 创建函数handler_interrupt_rtm_0x70的中断门描述符    
    mov eax, handler_interrupt_rtm_0x70 ; 偏移地址
    mov bx, sel_code_4gb_seg            ; 段选择子
    mov cx, 0x8e00      ; 属性值,32位的中断门,特权级为0
    call sel_code_4gb_seg:make_gate_descriptor    

    ; 在IDT中安装0x70号中断,指向实时时钟中断处理过程handler_interrupt_rtm_0x70
    mov ebx, idt_linear_base
    mov [ebx+0x70*8], eax
    mov [ebx+0x70*8+4], edx
    
    ; 中断描述符表寄存器IDTR
    ; 将中断描述符表的基地址和界限值加载到IDTR
    ; 一旦设置了IDT,并加载了IDTR,处理器的中断机制就开始起作用了。但现在还没有放开硬件中断
    mov word [idt_size], 256*8-1
    mov dword [idt_base], idt_linear_base
    lidt [idt_size]     ; Load interrupt descriptor table register
    
    ; 重新初始化中断控制器芯片8259A
    ; 在保护模式下,需要重新初始化8259A,否则其主片的中断向量和处理器的异常向量冲突
    ; 计算机启动之后,主片的中断向量为0x08~0x0F, 从片为0x70~0x77。但在32位处理器上,0x08~0x0F已被处理器用做异常向量
    ; 8259A是可编程的,允许重新设置中断向量
    
    ; 对8259A编程需要使用初始化命令字ICW,以设置其工作方式
    mov al, 0x11
    out 0x20, al ; ICW1设置中断请求的触发方式,边沿触发/级联方式
    mov al, 0x20
    out 0x21, al ; ICW2设置每个芯片的中断向量,起始中断向量
    mov al, 0x04
    out 0x21, al ; ICW3指定用哪个引脚实现芯片的级联,从片级联到IR2
    mov al, 0x01
    out 0x21, al ; ICW4控制芯片的工作方式: 非总线缓冲 全嵌套 正常EOI    
    
    ; 设置和主片相连的从片
    mov al, 0x11
    out 0xa0, al ; ICW1:边沿触发/级联方式
    mov al, 0x70
    out 0xa1, al ; ICW2:起始中断向量
    mov al, 0x04
    out 0xa1, al ; ICW3:从片级联到IR2
    mov al, 0x01
    out 0xa1, al ; ICW4:非总线缓冲,全嵌套,正常EOI

    ; 设置和时钟中断相关的硬件状态,包括RTC、8259A
    mov al, 0x0b    ; RTC寄存器B
    or al, 0x80     ; 阻断NMI
    out 0x70, al
    mov al, 0x12    ; 设置寄存器B,禁止周期性中断,开放更新结束后中断,BCD码,24小时制
    out 0x71, al

    in al, 0xa1     ; 读8259从片的IMR寄存器
    and al, 0xfe    ; 清除bit 0(此位连接RTC)
    out 0xa1, al    ; 写回此寄存器

    mov al, 0x0c
    out 0x70, al
    in al, 0x71     ; 读RTC寄存器C,复位未决的中断状态    

    ; 放开硬件中断
    sti     ; 用sti指令设置EFLAGS寄存器的IF位

    ; 显示提示信息,内核已工作在保护和分页模式
    mov ebx, message_kernel_at_protect_page_mode
    call sel_code_4gb_seg:show_string
    
    ; 获取处理器品牌信息
    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_code_4gb_seg:show_string
    mov ebx, cpu_brand          ; 处理器品牌信息
    call sel_code_4gb_seg:show_string
    mov ebx, cpu_brand1         ; 空行
    call sel_code_4gb_seg:show_string
        
    ; 在全局表述符表GDT中安装整个系统服务的调用门
    ; 特权级之间的控制转移必须使用门
    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_code_4gb_seg:make_gate_descriptor   ; 创建调用门描述符
    call sel_code_4gb_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_code_4gb_seg:show_string
    
    
    ; 初始化程序管理器任务的任务控制块TCB
    mov word [kernel_tcb+0x04], 0xffff  ; 设置任务的状态值,0xffff(忙)
    mov dword [kernel_tcb+0x06], 0x8010_0000 ; 自定义可分配的起始地址。内核虚拟空间的分配从这里开始
    mov word [kernel_tcb+0x0a], 0xffff ; 设置该任务LDT的初识界限值。内核任务没有LDT,这个值是用不上的
    
    mov ecx, kernel_tcb
    call append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中
    
    ; 为程序管理器任务的TSS分配内存空间
    allocate_memory_kernel  ; 宏,在内核空间中分配虚拟内存
    
    ; 在程序管理器的TSS中设置必要的项目
    mov eax, cr3
    mov dword [ebx+28], eax  ; 登记CR3(PDBR)
    mov word [ebx+0], 0          ; 反向链=0
    mov word [ebx+96], 0         ; 没有LDT。这里是将所有的段描述符安装在GDT中
    mov word [ebx+100], 0        ; T=0。不需要0 1 2特权级堆栈,0特权级不会向低特权级转移控制
    ; 登记I/O许可位映射区的地址
    ; 在这里填写的是TSS段界限(103),表明不存在该区域
    mov word [ebx+102], 103      ; 没有I/O位图。事实上0特权级不需要

    ; 创建TSS描述符,并安装到GDT中
    mov eax, ebx        ; 起始地址
    mov ebx, 103        ; 段界限
    mov ecx, 0x0040_8900; 段属性,特权级0
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    ; mov [prgman_tss+0x04], cx       ; 保存TSS描述符选择子
    mov [kernel_tcb+0x18], cx   ; 登记内核任务的TSS选择子到其TCB    
    

    ; 将当前任务的TSS选择子传送到任务寄存器TR
    ; TR中的内容是任务存在的标志,表明当前任务是谁,表明当前任务正在执行中
    ; 执行这条指令后,处理器用该选择子访问GDT,找到相对应地TSS,将其B位置1,表示该任务正在执行中
    ; 同时,还将该描述符传送到TR寄存器的描述符高速缓存器中
    ltr cx  ; 任务寄存器TR

    ; 现在可认为"程序管理器"任务正在执行中    
    
    
    ; 创建用户任务的任务控制块TCB
    allocate_memory_kernel    ; 宏,在内核空间中分配虚拟内存
    
    mov word [ebx+0x04], 0  ; 设置任务的状态值,0(空闲)
    mov dword [ebx+0x06], 0 ; 自定义可分配的起始地址。用户任务局部空间的分配从0开始
    mov word [ebx+0x0a], 0xffff ; 设置该任务LDT的初始界限值    
    
    push dword 50   ; 用户程序位于逻辑50扇区
    push ebx        ; TCB的起始线性地址
    call load_relocate_program
    mov ecx, ebx
    call append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中
    ; 当中断产生时,会切换到刚才创建的那个用户任务去执行
    
    
    ; 创建用户任务的任务控制块TCB
    allocate_memory_kernel    ; 宏,在内核空间中分配虚拟内存
    
    mov word [ebx+0x04], 0  ; 设置任务的状态值,0(空闲)
    mov dword [ebx+0x06], 0 ; 自定义可分配的起始地址。用户任务局部空间的分配从0开始
    mov word [ebx+0x0a], 0xffff ; 设置该任务LDT的初始界限值    
    
    push dword 100   ; 用户程序位于逻辑100扇区
    push ebx        ; TCB的起始线性地址
    call load_relocate_program
    mov ecx, ebx
    call append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中    

 .kernel_task:
    ; 这里是内核任务,每次得到处理器控制权时,显示message
    mov ebx, message_kernel_task
    call sel_code_4gb_seg:show_string
    
    ; 这里可以编写回收已终止任务内存的代码
    
    jmp .kernel_task
    

    







; -----------------------------------------------------------------
; Function: 加载并重定位用户程序
; Input: PUSH app起始逻辑扇区号; PUSH app任务控制块TCB线性地址
load_relocate_program:
    
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    ; 此时堆栈中,从下往上依次是:8个通用寄存器、EIP、TCB基地址、逻辑扇区号
    
    mov ebp, esp    ; 栈基址寄存器, 为访问通过堆栈传递的参数做准备

    ; 清空当前页目录的前半部分(对应低2GB的局部地址空间),页目录表的前512个目录项
    ; 后半部分是由内核使用的,内核的虚拟地址空间倍映射在每个任务的高地址段,0x8000_0000之后(对应高2GB的全局地址空间)
    mov ebx, 0xffff_f000 ; 当前页目录表的起始地址
                         ; 线性地址高20位为0xfffff时,访问的就是页目录表自己
    xor esi, esi
 .clear_pdt_pre2gb:
    mov dword [ebx+esi*4], 0
    inc esi
    cmp esi, 512
    jl .clear_pdt_pre2gb
    
    ; 刷新快表TLB
    ; TLB是软件不可直接访问的。但,将CR3寄存器的内容读出,再原样写入,就会使得TLB中的所有条目失效
    ; 本程序起码创建了2个用户任务,如果不刷新TLB,是不行的。
    mov eax, cr3
    mov cr3, eax
    
    ; 分配内存并加载用户程序

    ; 开始加载用户程序
    ; 先读取一个扇区
    mov eax, [ebp+10*4]                 ; 从堆栈中取得用户程序所在硬盘的起始逻辑扇区号 
    mov ebx, core_buf                   ; 自定义的一段内核缓冲区
                                        ; 在内核中开辟出一段固定的空间,有便于分析、加工和中转数据
    call sel_code_4gb_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

    ; 分配内存页,并将用户程序加载至内存
    ; 外循环每次分配一个4KB内存页,内循环每次读取8个扇区(8*512)
    ; 分页机制下,内存是先登记,后使用的。
    mov ecx, eax    ; 需要申请的内存大小
    shr ecx, 12     ; 除以4096得到需要的4KB页数,即循环分配的次数
    
    mov eax, [ebp+10*4]     ; 起始扇区号
    mov esi, [ebp+9*4]     ; 从堆栈中取得TCB的基地址
 .loop_allocate_install_memory_page:
    allocate_memory_user    ; 宏,在用户空间中分配虚拟内存
    
    push ecx
    mov ecx, 8  ; 内循环每次读取8个扇区(8*512)
 .loop_read_hard_disk:
    call sel_code_4gb_seg:read_hard_disk_0
    inc eax
    add ebx, 512
    loop .loop_read_hard_disk       ; 循环读
    
    pop ecx
    loop .loop_allocate_install_memory_page


    ; 创建用户任务的任务状态段TSS
    ; 任务是由内核管理的,TSS必须在内核的虚拟地址空间中创建
    allocate_memory_kernel      ; 宏,在内核空间中分配虚拟内存
                                ; 用户任务的TSS必须在全局空间上分配
    
    mov [esi+0x14], ebx          ; 在TCB中填写TSS的线性地址
    mov word [esi+0x12], 103     ; 在TCB中填写TSS的界限值
    ; TSS界限值必须至少是103,任何小于该值的TSS,在执行任务切换时,都会引发处理器异常中断

    ; 创建用户任务的局部描述符表LDT
    ; LDT是任务私有的,要在它自己的虚拟地址空间里分配,其基地址要登记到TCB中
    allocate_memory_user
    mov [esi+0x0c], ebx  ; 在TCB中填写LDT线性地址

    ; 创建用户任务的代码段描述符
    mov eax, 0              ; 段基地址
    mov ebx, 0x000f_ffff    ; 段界限值,粒度为4KB
    mov ecx, 0x00c0_f800    ; 段属性,特权级3
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi                    ; TCB基地址
    call setup_ldt_descriptor       ; 写入ldt    
    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3    
    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [ebx+76], cx   ; 填写TSS的CS域

    ; 创建用户任务的数据段描述符
    mov eax, 0              ; 段基地址
    mov ebx, 0x000f_ffff    ; 段界限值,粒度为4KB
    mov ecx, 0x00c0_f200    ; 段属性,特权级3
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi                    ; TCB基地址
    call setup_ldt_descriptor       ; 写入ldt    
    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3
    
    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [es:ebx+84], cx     ; 填写TSS的DS域
    ; 平坦模型下,段寄存器DS ES FS GS都指向同一个4GB数据段
    mov [ebx+72], cx     ; 填写TSS的ES域
    mov [ebx+88], cx     ; 填写TSS的FS域
    mov [ebx+92], cx     ; 填写TSS的GS域

    ; 创建用户任务的堆栈段
    ; 将数据段作为用户任务的3特权级固有堆栈
    ; 分配4KB内存
    allocate_memory_user    ; 宏,在用户空间中分配虚拟内存

    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [ebx+80], cx     ; 填写TSS的SS域
    mov edx, [esi+0x06]  ; 从TCB中取得堆栈的高端线性地址
    ; 由于栈从内存的高端向低端推进,所以esp的内容被指定为TCB中的下一个可分配的线性地址
    mov [ebx+56], edx    ; 填写TSS的ESP域
    
    ; 创建用户任务的0、1、2特权级栈
    ; 段基地址也是0,向上扩张的数据段,段界限为0x000f_ffff,粒度4KB
    ; 这3个栈段其描述符的特权级不同,段选择子也不一样

    ; 创建用户任务的0特权级栈
    allocate_memory_user    ; 宏,在用户空间中分配虚拟内存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_9200    ; 4KB粒度的堆栈段描述符,特权级0
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0000B     ; 设置选择子的请求特权级RPL为0
    
    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [ebx+8], cx      ; 填写TSS的SS0域
    mov edx, [esi+0x06]  ; 从TCB中取得堆栈的高端线性地址
    ; 由于栈从内存的高端向低端推进,所以esp的内容被指定为TCB中的下一个可分配的线性地址
    mov [ebx+4], edx     ; 填写TSS的ESP0域
    
    ; 创建用户任务的1特权级栈
    allocate_memory_user    ; 宏,在用户空间中分配虚拟内存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_b200    ; 4KB粒度的堆栈段描述符,特权级1
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0001B     ; 设置选择子的请求特权级RPL为1
    
    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [ebx+16], cx     ; 填写TSS的SS1域
    mov edx, [esi+0x06]  ; 从TCB中取得堆栈的高端线性地址
    ; 由于栈从内存的高端向低端推进,所以esp的内容被指定为TCB中的下一个可分配的线性地址
    mov [ebx+12], edx     ; 填写TSS的ESP1域


    ; 创建用户任务的2特权级栈
    allocate_memory_user    ; 宏,在用户空间中分配虚拟内存
    
    mov eax, 0x0000_0000
    mov ebx, 0x000f_ffff
    mov ecx, 0x00c0_d200    ; 4KB粒度的堆栈段描述符,特权级2
    call sel_code_4gb_seg:make_gdt_descriptor
    mov ebx, esi            ; TCB基地址
    call setup_ldt_descriptor
    or cx, 0000_0000_0000_0010B     ; 设置选择子的请求特权级RPL为2
    
    mov ebx, [esi+0x14]  ; 从TCB中取得TSS的基地址
    mov [ebx+24], cx     ; 填写TSS的SS0域
    mov edx, [esi+0x06]  ; 从TCB中取得堆栈的高端线性地址
    ; 由于栈从内存的高端向低端推进,所以esp的内容被指定为TCB中的下一个可分配的线性地址
    mov [ebx+20], edx     ; 填写TSS的ESP0域


    ; 重定位用户程序所调用的系统API
    ; 回填它们对应的入口地址
    ; 内外循环:外循环依次取出用户程序需调用的系统api,内循环遍历内核所有的系统api找到用户需调用那个
    
    cld     ; 清标志寄存器EFLAGS中的方向标志位,使cmps指令正向比较
    
    mov ecx, [0x0c]  ; 0x0c, 应用程序的头部包含了所需调用系统API个数
    mov edi, [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 [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


    ; 在GDT中登记LDT描述符, 处理器要求LDT描述符必须登记在GDT中
    mov esi, [ebp+9*4]         ; 从堆栈中取得TCB的基地址
    mov eax, [esi+0x0c]      ; LDT起始地址
    movzx ebx, word [esi+0x0a] ; LDT段界限,movzx先零扩展再传送
    mov ecx, 0x0040_8200        ; LDT描述符属性,特权级DPL为0,TYPE为2表示这是一个LDT描述符
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    mov [esi+0x10], cx       ; 登记LDT选择子到TCB中
    
    mov ebx, [esi+0x14]      ; 从TCB中取得TSS的基地址
    mov [ebx+96], cx         ; 填写TSS的LDT域
    
    
    ; 登记基本的TSS表格内容
    mov word [ebx+0], 0      ; 反向链=0,将指向前一个任务的指针(任务链接域)填写为0
                                ; 这表明这是唯一的任务
    
    ; 登记I/O许可位映射区的地址
    ; 在这里填写的是TSS段界限(103),表明不存在该区域
    mov dx, [esi+0x12]
    mov [ebx+102], dx
    
    mov word [ebx+100], 0     ; T=0
    
    mov eax, [0x04]     ; 从用户程序头部取得程序入口点,0x04    
    mov [ebx+32], eax   ; 填写TSS的EIP域, 即用户程序的入口点
    ; 从内核任务切换到用户任务时,是用TSS中的内容恢复现场的

    ; 将标志寄存器EFLAGS内容写入TSS中的EFLAGS域
    pushfd      ; 将EFLAGS寄存器内容压栈
    pop edx     ; 弹出到edx
    mov dword [ebx+36], edx  ; 将EFLAGS内容写入TSS中EFLAGS域

    
    ; 登记TSS描述符到GDT中
    ; 和局部描述符表LDT一样,也必须在GDT中安装TSS的描述符
    ; 一方面是为了对TSS进行段和特权级的检查,另一方面也是执行任务切换的需要
    ; 当call far和jmp far指令的操作数是TSS描述符选择子时,处理器执行任务切换操作
    mov eax, [esi+0x14]      ; 从TCB中取得TSS的基地址
    movzx ebx, word [esi+0x12] ; TSS的界限值
    mov ecx, 0x0040_8900        ; TSS的属性,特权级DPL为0,字节粒度
    call sel_code_4gb_seg:make_gdt_descriptor
    call sel_code_4gb_seg:setup_gdt_descriptor
    mov [esi+0x18], cx       ; 登记TSS描述符选择子到TCB,RPL为0
    
    ; 创建用户任务的页目录
    ; 页的分配和使用是由页位图决定的,可以不占用线性地址空间 
    call sel_code_4gb_seg:create_user_pdt_by_copy
    mov ebx, [esi+0x14]      ; 从TCB中取得TSS的基地址
    mov dword [ebx+28], eax  ; 填写TSS的CR3(PDBR)域
    
    popad
    
    ret 8       ; 丢弃调用本过程前压入的参数
                ; 该指令执行时,除了将控制返回到过程的调用者之外,还会调整栈的指针esp=esp+8字节






; -----------------------------------------------------------------
; Function: 频幕上显示文本,并移动光标
; Input: ebx 字符串起始地址,以0结尾
show_string:
    push ebx
    push ecx
    
    ; 临时关闭硬件中断
    ; 否则,字符串显示期间,随时会被中断而切换到另一个任务,将导致2个任务所显示的内容在屏幕上交替出现
    cli     ; 用cli指令清零EFLAGS寄存器的IF位
    
 .loop_show_string:
    mov cl, [ebx]
    or cl, cl
    jz .exit                ; 以0结尾
    call show_char
    inc ebx
    jmp .loop_show_string
    
 .exit:
    ; 放开硬件中断
    sti 
 
    pop ecx
    pop ebx
    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位数
    
    ; 因为访问显示缓冲区时用的是32位寻址方式,故必须使用EBX寄存器
    and ebx, 0x0000_ffff ; 清除EBX寄存器高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:
    shl bx, 1 ; 光标指示字符位置,显存中一个字符占2字节,光标位置乘2得到该字符在显存中得偏移地址    
    mov [0x800b_8000+ebx], cl ; 物理内存的低端1MB已被完整地映射到从0x8000_0000开始的高端。
    ; 显示缓冲区的线性基地址为0x800b_8000,会被处理器的页部件转换成物理地址0x000b_8000
    
    shr bx, 1       ; 恢复bx
    inc bx          ; 将光标推进到下一个位置
    
 ; 判断是否需要向上滚动一行屏幕
 .roll_screen:
    cmp bx, 2000    ; 25行x80列
    jl .set_cursor
    
    mov esi, 0x800b_80a0
    mov edi, 0x800b_8000
    mov ecx, 1920    ; rep次数 
    cld             ; 传送方向cls std24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节
    rep movsd
    
    ; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720
    mov bx, 3840    ; 24行*每行80个字符*每个字符加显示属性占2字节
    mov ecx, 80
 .cls:
    mov word [0x800b_8000+ebx], 0x0720
    add bx, 2
    loop .cls
    
    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:
    
    ; 屏蔽硬件中断
    ; 防止对同一个硬盘控制器端口的交叉修改
    ; 多任务环境下,当一个任务正在读硬盘时,会被另一个任务打断。如果另一个任务也访问硬盘,将破坏前一个任务对硬盘的操作状态
    cli

    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

    ; 放开硬件中断
    sti
    
    retf        ; 段间返回



; ===============================================================================    
; Function: 申请一个4KB物理页
; Input: 
; Output: eax, 页的物理地址 
allocate_memory_page:
; 搜索页映射位串查找空闲的页,并分配页
    push ebx
    push ecx ; ???
    push edx ; ???
    
    ; 从页映射位串的第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_code_4gb_seg:show_string
    hlt
 
 .done:
    shl eax, 12 ; 将该比特在位串中的位置数值乘以每个页的大小4KB,就是该比特对应的那个页的物理地址
    
    pop edx
    pop ecx
    pop ebx
    
    ret ; 这是段内的内部过程,仅供同一段内的其他过程使用



; ===============================================================================    
; Function: 申请一个4KB物理页,并写入分页结构中(页目录表和页表)
; Input: ebx, 线性地址
; Output: 
allocate_install_memory_page:

; 在可用的物理内存中搜索空闲的页,然后根据线性地址来创建页目录项和页表项,并将页的地址填写在页表项中
    push eax
    push ebx
    push esi
    
    ; 访问页目录表,检查该线性地址对应的页目录项是否存在
    ; 分页机制下,访问内存需要通过页目录表和页表,而这里却要访问页目录表
    ; 要先得到要修改的那个页目录项的线性地址,把页目录当作普通页来访问
    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 esi
    pop ebx
    pop eax
    
    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

    ; sgdt, Store Global Descriptor Table Register
    ; 将gdtr寄存器的基地址和边界信息保存到指定的内存位置
    ; 低2字节为gdt界限(大小),高4字节为gdt的32位物理地址
    sgdt [gdt_size]
    
    ; 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 [ebx], eax
    mov [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 edx
    pop ebx
    pop eax
    
    retf
    
    
    
; =============================================================================== 
; Function: 在ldt中安装一个新的段描述符
; Input: edx:eax 段描述符; ebx 任务控制块TCB基地址
; Output: cx 段描述符的选择子
setup_ldt_descriptor:
    push eax
    push edx
    push edi
    
    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 edi
    pop edx
    pop eax

    ret

    
    
; ===============================================================================    
; 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: 在TCB链上追加任务控制块
; Input: ecx 需要追加的那项TCB线性基地址
append_to_tcb_link:
    cli     ; 屏蔽硬件中断

    push eax
    push ebx
    
    mov eax, tcb_chain_head
 .totailTCB:
    mov ebx, [eax]
    or ebx, ebx
    jz .appendTCB                ; 链表为空,或已到末尾    
    mov eax, ebx
    jmp .totailTCB
    
 .appendTCB:
    cli
    mov [eax], ecx
    mov dword [ecx], 0 ; 将当前TCB指针域清零,表示这是链表中最后一个TCB

    pop ebx
    pop eax
    
    sti     ; 放开硬件中断
    ret



; ===============================================================================    
; Function: 创建用户任务的页目录表
; Input:
; Output: eax, 新页目录的物理地址
create_user_pdt_by_copy:

; 创建新的页目录,并复制当前页目录内容
    push ebx
    push ecx
    push esi
    push edi
    
    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

    ; 刷新TLB中的单个条目:
    ; invalidate TLB entry, 是特权指令,当前特权级CPL必须为0
    invlpg [0xffff_fff8]    ; 0xfffffff8是内核页目录表内倒数第2个目录项,每次都用它来指向新任务的页目录表
    ; 虽然在上一条指令中改写了它,使它指向新任务的页目录表,但这个更改只在内存中有效,没有反映到TLB中
    
    mov esi, 0xffff_f000    ; 当前页目录的线性地址
    mov edi, 0xffff_e000    ; 新页目录的线性地址
    mov ecx, 1024           ; 传送次数
    cld                     ; 传送方向为正向    
    repe movsd
    
    pop edi
    pop esi
    pop ecx
    pop ebx
    
    retf
    
    
; ===============================================================================    
; Function: 将ds的值以十六进制的形式在屏幕上显示
; Input: 
; Output: 
show_hex_dword:
    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad

    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
    
    popad
    retf



; ===============================================================================    
; Function: 终止当前任务,并转换到其他任务
; Input: 
; Output: 
terminate_current_task:
; 现在仍处在用户任务中,要结束当前的用户任务,可以先切换到程序管理器任务,然后回收用户程序所占用的内存空间

    ; 找到当前任务(状态为忙的任务)在链表中的位置,即状态值为0xFFFF的节点
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    cmp word [ebx+0x04], 0xffff     ; 判断状态是否为忙,即是否为当前任务
    je .modify_state_to_exit
    mov eax, ebx ; TCB内偏移为0x00处,是下一个TCB的线性地址
    jmp .search_current_task
    
 .modify_state_to_exit:
    ; 修改当前任务的状态为“退出”
    mov word [ebx+0x04], 0x3333
    
 .halt:
    hlt         ; 停机,等待程序管理器恢复运行时,将其回收
    jmp .halt
    
    
; -----------------------------------------------------------------
; Function: 通用的异常处理过程
; Input: 
handler_exception_general:
    mov ebx, message_exception_general
    call sel_code_4gb_seg:show_string
    ; show_string函数为方便用户任务调用,被包装成调用门,通过retf返回
    ; 通过调用门的控制转移属于远过程调用,尽管show_string属于内核
    
    hlt
    
    
    
; -----------------------------------------------------------------
; Function: 通用的中断处理过程
; Input: 
handler_interrupt_general:
    push eax
    
    ; 向8259A芯片发送中断结束命令EOI(End of Interrupt)    
    mov al, 0x20    
    out 0xa0, al    ; 向8259A从片发送
    out 0x20, al    ; 向8259A主片发送
    
    pop eax
    iretd    
    
    
    
; -----------------------------------------------------------------
; Function: 实时时钟中断处理过程,实现任务切换
;           利用硬件中断实现任务切换
; Input: 
handler_interrupt_rtm_0x70:
; 计算机主板上有实时时钟芯片RTC,可以设置RTC芯片,使得它每次更新CMOS中的时间信息后,发出更新周期结束的中断信号,从而进行任务切换
; 其实,用实时时钟更新周期结束中断来实施任务切换并不是一个好主意,和别的中断相比,它更啰嗦

    pushad
    
    ; 向8259A芯片发送中断结束命令EOI(End of Interrupt)
    ; 否则,它不会再向处理器发送另一个中断通知
    mov al, 0x20    
    out 0xa0, al    ; 向8259A从片发送
    out 0x20, al    ; 向8259A主片发送
    
    ; 必须读一下CMOS芯片内的寄存器C,使它复位一下, 才能使RTC产生下一个中断信号。否则,它只产生一次中断信号
    mov al, 0x0c    ; 寄存器C的索引,且放开NMI
    out 0x70, al
    in al, 0x71     ; 读一下RTC的寄存器C
    
    ; 找到当前任务(状态为忙的任务)在链表中的位置,即状态值为0xFFFF的节点
    mov eax, tcb_chain_head
 .search_current_task:
    mov ebx, [eax]
    or ebx, ebx
    jz .irtn    ; 链表为空,或已到末尾,从中断返回
    cmp word [ebx+0x04], 0xffff     ; 判断状态是否为忙,即是否为当前任务
    je .move_current_to_chaintail
    mov eax, ebx ; TCB内偏移为0x00处,是下一个TCB的线性地址
    jmp .search_current_task
    
    ; 把当前任务(状态为忙的任务)移到链尾
 .move_current_to_chaintail:
    ; 从链表中删除该节点
    mov ecx, [ebx]  ; 下一个TCB节点的线性地址
    mov [eax], ecx  ; 将当前任务从TCB链表中删除
 .goto_chaintail:
    ; 遍历至链表尾部
    mov edx, [eax]
    or edx, edx     ; 判断是否已到链表尾部
    jz .add_current_to_chaintail
    mov eax, edx
    jmp .goto_chaintail
 .add_current_to_chaintail:
    ; 将该节点添加至链表尾部
    mov [eax], ebx
    mov dword [ebx], 0 ; 将当前任务标记为链尾。TCB内偏移为0处,是下一个TCB的线性地址
    
    ; 从TCB链表中搜索第一个空闲任务
    mov eax, tcb_chain_head
 .search_next_task:
    mov eax, [eax]
    or eax, eax     ; 已到链尾,即未发现空闲任务
    jz .irtn        ; 从中端返回
    cmp word [eax+0x04], 0x0000     ; 是空闲任务。空闲为0,忙为0xffff
    jnz .search_next_task
    
    ; 将空闲任务和当前任务的状态都取反
    not word [eax+0x04]     ; 设置空闲任务的状态为忙
    not word [ebx+0x04]     ; 设置当前任务的状态为空闲
    
    ; 任务切换
    jmp far [eax+0x14] ; TCB内偏移为0x14处,依次是该任务TSS的线性地址和TSS描述符选择子

 .irtn:
    popad   ; 当前任务再次获得执行权时的返回点
    iretd   ; 从中断返回,正常执行任务的其他代码
    
    
    


; -----------------------------------------------------------------
; 用于设置和修改GDT
; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0

; IDT
idt_size dw 0   ; 界限值,描述符个数*8-1
idt_base dd 0   ; 基地址

; 任务控制块链表的头部
tcb_chain_head  dd 0

; 内核(程序管理器)的TCB
kernel_tcb times 32 db 0 

; 页面的位映射串
; 这里没有去检测实际可用内存,仅仅假定只有2MB的物理内存可用
; 2MB物理内存,即512个4KB页,需要512个比特的位串
; 这里前32字节的值基本都是0xff。因为它们对应着最低端1MB内存的那些页(256个页),它们已经整体上划归内核使用了,
; 没有被内核占用的部分多数也被外围硬件占用了,比如ROM-BIOS
; 这里0x55, 即0101_0101, 是有意将空闲的页在物理上分开,用于说明连续的地址空间不必对应着连续的页
page_bitmap   db  0xff,0xff,0xff,0xff,0xff,0xff,0x55,0x55
              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_code_4gb_seg
            
 sys_api_2  db '@ReadDiskData'
            times 256-($-sys_api_2) db 0
            dd read_hard_disk_0
            dw sel_code_4gb_seg

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

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

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

core_buf    times 512 db 0              ; 自定义的内核缓冲区
    
bin_hex     db '0123456789ABCDEF' ; show_hex_dword过程需要的查找表

; 提示信息,捕获到异常
message_exception_general db  '********Exception encounted********',0

; 提示信息,内核已工作在保护和分页模式
message_kernel_at_protect_page_mode db  '  Working in system core with protection '
    db  'and paging are all enabled.System core is mapped '
    db  'to address 0x80000000.',0x0d,0x0a,0

; 提示信息,系统api的调用门安装完成
message_callgate_mount_succ db  '  System wide CALL-GATE mounted.',0x0d,0x0a,0

; 提示信息, 没有可用于分配的空闲页 
message_page_notenough db  '********No more pages********',0

; 提示信息,正执行内核任务
message_kernel_task db  '  System core task running!',0x0d,0x0a,0

    




core_code_end:



; ===============================================================================    
SECTION tail        ; 这里用于计算程序大小,不需要vstart=0
core_end:    

# file_03: c17_1.asm    用户程序1代码

; FILE: c17_1.asm
; TITLE: 用户程序
; DATE: 20200209

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

    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  '  User task A->;;;;;;;;;;;;;;;;;;;;;;;;;;'
                    db 0x0d,0x0a, 0
                    

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


; ===============================================================================
; SECTION code vstart=0                       ; 定义用户程序代码段
beginning:   
    ; 调用系统API
    ; 显示提示信息,正在运行的用户程序的当前特权级CPL
    mov ebx, message_usermode
    call far [ShowString]   

    jmp beginning

    ; 调用系统API, 退出,并将控制权返回给内核   
    call far [TerminateProgram]

; code_end:

    
; ===============================================================================    
; SECTION tail align=16       ; 这里用于计算程序大小,不需要vstart=0
program_end:    
    
    

# file_04: c17_2.asm    用户程序1代码

; FILE: c17_1.asm
; TITLE: 用户程序
; DATE: 20200209

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

    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  '  User task B->********************'
                    db 0x0d,0x0a, 0
                    

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


; ===============================================================================
; SECTION code vstart=0                       ; 定义用户程序代码段
beginning:   
    ; 调用系统API
    ; 显示提示信息,正在运行的用户程序的当前特权级CPL
    mov ebx, message_usermode
    call far [ShowString]   

    jmp beginning

    ; 调用系统API, 退出,并将控制权返回给内核   
    call far [TerminateProgram]

; code_end:

    
; ===============================================================================    
; SECTION tail align=16       ; 这里用于计算程序大小,不需要vstart=0
program_end:    
    
    

 

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