# 任务切换

内核任务、用户任务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的索引,且放开NMIout 0x70, alin al, 0x71     ; 读一下RTC的寄存器C; 找到当前任务(状态为忙的任务)在链表中的位置,即状态值为0xFFFF的节点mov eax, tcb_chain_head.search_current_task:mov ebx, [eax]or ebx, ebxjz .irtn    ; 链表为空,或已到末尾,从中断返回cmp word [ebx+0x04], 0xffff     ; 判断状态是否为忙,即是否为当前任务je .move_current_to_chaintailmov 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_chaintailmov eax, edxjmp .goto_chaintail.add_current_to_chaintail:; 将该节点添加至链表尾部mov [eax], ebxmov 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,忙为0xffffjnz .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, 0x0010mov eax, 0000_0000_00010_0_00B   ; 已初始化的GDT中,数据段为第2号描述符; 因为平坦模式下所有段都指向4GB数据段,只不过栈段向下增长,其他各段都向上增长mov ds, eax  ; 当处理器执行任何改变段选择器的指令时,就将指令中提供的索引号乘以8作为偏移地址,同GDTR中提供的线性地址相加,; 以访问GDT,将找到的描述符加载到不可见的描述符高速缓存部分; 设置堆栈段ss和段指针espmov es, eaxmov fs, eaxmov gs, eaxmov ss, eaxmov esp, 0x7000     ; 栈从地址0x7000开始向低地址方向扩展; 加载mini内核; 从硬盘把内核程序读入内存mov edi, core_base_address      ; 自定义的mini内核物理内存地址mov eax, core_begin_sector      ; 自定义的mini内核在硬盘上的起始逻辑扇区号mov ebx, edicall read_hard_disk_0           ; 先读一个扇区; 包含了头部信息:程序大小、入口点、段重定位表; 判断需要加载的整个程序有多大mov eax, [edi]                  ; 0x00, 应用程序的头部包含了程序大小xor edx, edxmov ecx, 512div ecxcmp edx, 0               jnz @1dec eax                         ; 余数edx为0则商eax减1,已读取一个扇区    @1:cmp eax, 0jz setup_page           ; 实际长度小于512字节,则已读取完; 读取剩余的扇区    mov ecx, eax            ; 循环次数(剩余扇区数)mov eax, core_begin_sectorinc eax                 ; 读取下一个逻辑扇区@2:    add ebx, 512            ; 每次读时,指向物理内存的下一个512字节call read_hard_disk_0inc eax                 ; 下一个扇区loop @2setup_page:
;准备打开分页机制。从此,再也不用在段之间转来转去; 创建系统内核的页目录表PDTmov 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       ; 页表的物理地址0x21000xor eax, eax            ; 起始页的物理地址0x0xor esi, esi.pt_kernel_pre256:mov edx, eaxor edx, 0x0000_0003     ; 低12位为页属性; 属性值3,P=1, RW=1, US=0mov [ebx+esi*4], edx    ; 在页表中登记页的物理地址add eax, 0x1000         ; 下一个相邻页的物理地址,每个页4KBinc esi                 ; 页表的下一个页表项cmp esi, 256jl .pt_kernel_pre256    ; 将上面内核页表的其余页表项置为无效.pdt_1_others:mov dword [ebx+esi*4], 0    ; 页表项内容为0,即为无效表项inc esicmp 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, cr0or eax, 0x8000_0000mov 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 eaxpush ebxpush ecxpush 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                  ; 0x1f3pop eaxout dx, al              ; LBA地址7~0inc dx                  ; 0x1f4mov cl, 8shr eax, clout dx, al              ; in和out 操作寄存器只能是al或者ax; LBA地址15~8inc dx                  ; 0x1f5shr eax, clout dx, al              ; LBA地址23~16; 8bits端口0x1f6,低4位存放28位逻辑扇区号的24~27位;; 第4位指示硬盘号,0为主盘,1为从盘;高3位,111表示LBA模式inc dx                  ; 0x1f6shr eax, cl             or al, 0xe0             ; al 高4位设为 1110; al 低4位设为 LBA的的高4位out dx, al; 3) 请求读硬盘; ==========================; 向端口写入0x20,请求硬盘读inc dx                  ; 0x1f7mov al, 0x20out dx, al.wait:; 4) 等待硬盘读写操作完成; ===========================; 端口0x1f7既是命令端口,又是状态端口; 通过这个端口发送读写命令之后,硬盘就忙乎开了。; 0x1f7端口第7位,1为忙,0忙完了同时将第3位置1表示准备好了,; 即0x08时,主机可以发送或接收数据in al, dx               ; 0x1f7and al, 0x88            ; 取第8位和第3位cmp al, 0x08            jnz .wait; 5) 连续取出数据; ============================; 0x1f0是硬盘接口的数据端口,16bitsmov ecx, 256             ; loop循环次数,每次读取2bytesmov dx, 0x1f0           ; 0x1f0.readw:in ax, dxmov [ebx], axadd ebx, 2loop .readwpop edxpop ecxpop ebxpop eaxret; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0x00008000times 510-($-$$) db 0db 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 0mov 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 0mov 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位的中断门,特权级为0call sel_code_4gb_seg:make_gate_descriptor; 这里还是采用远调用,函数也是用retf返回; 但该函数其实可以定义成用ret指令返回的近过程,因为内核不会允许3特权级的用户任务调用该过程; 在IDT中安装前20个描述符,都指向通用异常处理程序handler_exception_generalmov ebx, idt_linear_basexor esi, esi.idt0_19:mov [ebx+esi*8], eaxmov [ebx+esi*8+4], edxinc esicmp esi, 19jle .idt0_19; 20~255中断向量是Intel保留的中断向量,以及外部硬件中断; 创建函数handler_interrupt_general的中断门描述符    mov eax, handler_interrupt_general  ; 偏移地址mov bx, sel_code_4gb_seg            ; 段选择子mov cx, 0x8e00      ; 属性值,32位的中断门,特权级为0call sel_code_4gb_seg:make_gate_descriptor; 在IDT中安装0~255描述符,都指向通用中断处理程序handler_interrupt_generalmov ebx, idt_linear_base.idt20_255:mov [ebx+esi*8], eaxmov [ebx+esi*8+4], edxinc esicmp esi, 255jle .idt20_255; 设置实时时钟中断处理过程,用于; 创建函数handler_interrupt_rtm_0x70的中断门描述符    mov eax, handler_interrupt_rtm_0x70 ; 偏移地址mov bx, sel_code_4gb_seg            ; 段选择子mov cx, 0x8e00      ; 属性值,32位的中断门,特权级为0call sel_code_4gb_seg:make_gate_descriptor    ; 在IDT中安装0x70号中断,指向实时时钟中断处理过程handler_interrupt_rtm_0x70mov ebx, idt_linear_basemov [ebx+0x70*8], eaxmov [ebx+0x70*8+4], edx; 中断描述符表寄存器IDTR; 将中断描述符表的基地址和界限值加载到IDTR; 一旦设置了IDT,并加载了IDTR,处理器的中断机制就开始起作用了。但现在还没有放开硬件中断mov word [idt_size], 256*8-1mov dword [idt_base], idt_linear_baselidt [idt_size]     ; Load interrupt descriptor table register; 重新初始化中断控制器芯片8259A; 在保护模式下,需要重新初始化8259A,否则其主片的中断向量和处理器的异常向量冲突; 计算机启动之后,主片的中断向量为0x08~0x0F, 从片为0x70~0x77。但在32位处理器上,0x08~0x0F已被处理器用做异常向量; 8259A是可编程的,允许重新设置中断向量; 对8259A编程需要使用初始化命令字ICW,以设置其工作方式mov al, 0x11out 0x20, al ; ICW1设置中断请求的触发方式,边沿触发/级联方式mov al, 0x20out 0x21, al ; ICW2设置每个芯片的中断向量,起始中断向量mov al, 0x04out 0x21, al ; ICW3指定用哪个引脚实现芯片的级联,从片级联到IR2mov al, 0x01out 0x21, al ; ICW4控制芯片的工作方式: 非总线缓冲 全嵌套 正常EOI    ; 设置和主片相连的从片mov al, 0x11out 0xa0, al ; ICW1:边沿触发/级联方式mov al, 0x70out 0xa1, al ; ICW2:起始中断向量mov al, 0x04out 0xa1, al ; ICW3:从片级联到IR2mov al, 0x01out 0xa1, al ; ICW4:非总线缓冲,全嵌套,正常EOI; 设置和时钟中断相关的硬件状态,包括RTC、8259Amov al, 0x0b    ; RTC寄存器Bor al, 0x80     ; 阻断NMIout 0x70, almov al, 0x12    ; 设置寄存器B,禁止周期性中断,开放更新结束后中断,BCD码,24小时制out 0x71, alin al, 0xa1     ; 读8259从片的IMR寄存器and al, 0xfe    ; 清除bit 0(此位连接RTC)out 0xa1, al    ; 写回此寄存器mov al, 0x0cout 0x70, alin al, 0x71     ; 读RTC寄存器C,复位未决的中断状态    ; 放开硬件中断sti     ; 用sti指令设置EFLAGS寄存器的IF位; 显示提示信息,内核已工作在保护和分页模式mov ebx, message_kernel_at_protect_page_modecall sel_code_4gb_seg:show_string; 获取处理器品牌信息mov eax, 0          ; 先用0号功能探测处理器最大能支持的功能号cpuid               ; 会在eax中返回最大可支持的功能号; 要返回处理器品牌信息,需使用0x80000002~0x80000004号功能,分3次进行mov eax, 0x80000002cpuidmov [cpu_brand], eaxmov [cpu_brand+0x04], ebxmov [cpu_brand+0x08], ecxmov [cpu_brand+0x0c], edxmov eax, 0x80000003cpuidmov [cpu_brand+0x10], eaxmov [cpu_brand+0x14], ebxmov [cpu_brand+0x18], ecxmov [cpu_brand+0x1c], edxmov eax, 0x80000004cpuidmov [cpu_brand+0x20], eaxmov [cpu_brand+0x24], ebxmov [cpu_brand+0x28], ecxmov [cpu_brand+0x2c], edx; 显示处理器品牌信息mov ebx, cpu_brand0         ; 空行call sel_code_4gb_seg:show_stringmov ebx, cpu_brand          ; 处理器品牌信息call sel_code_4gb_seg:show_stringmov ebx, cpu_brand1         ; 空行call sel_code_4gb_seg:show_string; 在全局表述符表GDT中安装整个系统服务的调用门; 特权级之间的控制转移必须使用门mov edi, sys_apimov ecx, sys_api_items.make_call_gate:push ecxmov 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   ; 将调用门描述符写入gdtmov [edi+260], cx               ; 将门描述符的选择子(即调用门选择子)写回add edi, sys_api_item_length    ; 指向下一个sys_api条目pop ecxloop .make_call_gate; 测试一下刚安装好的调用门; 显示字符串mov ebx, message_callgate_mount_succcall far [sys_api_1+256]        ; 取得32位偏移地址 和16位的段选择子; 处理器会检查选择子是调用门的描述符还是普通的段描述符; 不通过调用门,以传统方式调用系统api; 显示提示信息,开始加载用户程序; mov ebx, message_app_load_begin; call sel_code_4gb_seg:show_string; 初始化程序管理器任务的任务控制块TCBmov word [kernel_tcb+0x04], 0xffff  ; 设置任务的状态值,0xffff(忙)mov dword [kernel_tcb+0x06], 0x8010_0000 ; 自定义可分配的起始地址。内核虚拟空间的分配从这里开始mov word [kernel_tcb+0x0a], 0xffff ; 设置该任务LDT的初识界限值。内核任务没有LDT,这个值是用不上的mov ecx, kernel_tcbcall append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中; 为程序管理器任务的TSS分配内存空间allocate_memory_kernel  ; 宏,在内核空间中分配虚拟内存; 在程序管理器的TSS中设置必要的项目mov eax, cr3mov dword [ebx+28], eax  ; 登记CR3(PDBR)mov word [ebx+0], 0          ; 反向链=0mov 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; 段属性,特权级0call sel_code_4gb_seg:make_gdt_descriptorcall 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; 现在可认为"程序管理器"任务正在执行中    ; 创建用户任务的任务控制块TCBallocate_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_programmov ecx, ebxcall append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中; 当中断产生时,会切换到刚才创建的那个用户任务去执行; 创建用户任务的任务控制块TCBallocate_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_programmov ecx, ebxcall append_to_tcb_link     ; 将该任务的TCB添加到TCB链表中    .kernel_task:; 这里是内核任务,每次得到处理器控制权时,显示messagemov ebx, message_kernel_taskcall 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,EDIpushad; 此时堆栈中,从下往上依次是: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], 0inc esicmp esi, 512jl .clear_pdt_pre2gb; 刷新快表TLB; TLB是软件不可直接访问的。但,将CR3寄存器的内容读出,再原样写入,就会使得TLB中的所有条目失效; 本程序起码创建了2个用户任务,如果不刷新TLB,是不行的。mov eax, cr3mov 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, 0x1000test eax, 0x0000_0fffcmovnz 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 ecxmov ecx, 8  ; 内循环每次读取8个扇区(8*512).loop_read_hard_disk:call sel_code_4gb_seg:read_hard_disk_0inc eaxadd ebx, 512loop .loop_read_hard_disk       ; 循环读pop ecxloop .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_usermov [esi+0x0c], ebx  ; 在TCB中填写LDT线性地址; 创建用户任务的代码段描述符mov eax, 0              ; 段基地址mov ebx, 0x000f_ffff    ; 段界限值,粒度为4KBmov ecx, 0x00c0_f800    ; 段属性,特权级3call sel_code_4gb_seg:make_gdt_descriptormov 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    ; 段界限值,粒度为4KBmov ecx, 0x00c0_f200    ; 段属性,特权级3call sel_code_4gb_seg:make_gdt_descriptormov ebx, esi                    ; TCB基地址call setup_ldt_descriptor       ; 写入ldt    or cx, 0000_0000_0000_0011B     ; 设置选择子的请求特权级RPL为3mov 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_0000mov ebx, 0x000f_ffffmov ecx, 0x00c0_9200    ; 4KB粒度的堆栈段描述符,特权级0call sel_code_4gb_seg:make_gdt_descriptormov ebx, esi            ; TCB基地址call setup_ldt_descriptoror cx, 0000_0000_0000_0000B     ; 设置选择子的请求特权级RPL为0mov 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_0000mov ebx, 0x000f_ffffmov ecx, 0x00c0_b200    ; 4KB粒度的堆栈段描述符,特权级1call sel_code_4gb_seg:make_gdt_descriptormov ebx, esi            ; TCB基地址call setup_ldt_descriptoror cx, 0000_0000_0000_0001B     ; 设置选择子的请求特权级RPL为1mov 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_0000mov ebx, 0x000f_ffffmov ecx, 0x00c0_d200    ; 4KB粒度的堆栈段描述符,特权级2call sel_code_4gb_seg:make_gdt_descriptormov ebx, esi            ; TCB基地址call setup_ldt_descriptoror cx, 0000_0000_0000_0010B     ; 设置选择子的请求特权级RPL为2mov 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 ecxpush edimov ecx, sys_api_items          ; 内循环次数mov esi, sys_api                ; 内核中系统api列表的起始偏移地址.search_sys_api_internal:push esipush edipush ecxmov 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。即,这些调用门选择子的请求特权级为0mov [edi-252], ax            ; 回填调用门选择子.b4:pop ecxpop edipop esiadd esi, sys_api_item_length    ; 内核中系统api列表的下一条目的偏移地址loop .search_sys_api_internalpop edipop ecxadd 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_descriptorcall sel_code_4gb_seg:setup_gdt_descriptormov [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], dxmov word [ebx+100], 0     ; T=0mov eax, [0x04]     ; 从用户程序头部取得程序入口点,0x04    mov [ebx+32], eax   ; 填写TSS的EIP域, 即用户程序的入口点; 从内核任务切换到用户任务时,是用TSS中的内容恢复现场的; 将标志寄存器EFLAGS内容写入TSS中的EFLAGS域pushfd      ; 将EFLAGS寄存器内容压栈pop edx     ; 弹出到edxmov 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_descriptorcall sel_code_4gb_seg:setup_gdt_descriptormov [esi+0x18], cx       ; 登记TSS描述符选择子到TCB,RPL为0; 创建用户任务的页目录; 页的分配和使用是由页位图决定的,可以不占用线性地址空间 call sel_code_4gb_seg:create_user_pdt_by_copymov ebx, [esi+0x14]      ; 从TCB中取得TSS的基地址mov dword [ebx+28], eax  ; 填写TSS的CR3(PDBR)域popadret 8       ; 丢弃调用本过程前压入的参数; 该指令执行时,除了将控制返回到过程的调用者之外,还会调整栈的指针esp=esp+8字节; -----------------------------------------------------------------
; Function: 频幕上显示文本,并移动光标
; Input: ebx 字符串起始地址,以0结尾
show_string:push ebxpush ecx; 临时关闭硬件中断; 否则,字符串显示期间,随时会被中断而切换到另一个任务,将导致2个任务所显示的内容在屏幕上交替出现cli     ; 用cli指令清零EFLAGS寄存器的IF位.loop_show_string:mov cl, [ebx]or cl, cljz .exit                ; 以0结尾call show_charinc ebxjmp .loop_show_string.exit:; 放开硬件中断sti pop ecxpop ebxretf                    ; 段间调用返回; Function:
; Input: cl 字符
show_char:; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDIpushad; 读取当前光标位置; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位; 数据端口0x3d5mov dx, 0x3d4   mov al, 0x0e   out dx, almov dx, 0x3d5in al, dxmov ah, almov dx, 0x3d4mov al, 0x0fout dx, almov dx, 0x3d5in al, dxmov bx, ax      ; 此处用bx存放光标位置的16位数; 因为访问显示缓冲区时用的是32位寻址方式,故必须使用EBX寄存器and ebx, 0x0000_ffff ; 清除EBX寄存器高16位; 判断是否为回车符0x0dcmp cl, 0x0d    ; 0x0d 为回车符jnz .show_0a    ; 不是回车符0x0d,再判断是否换行符0x0amov ax, bx      ; 是回车符,则将光标置位到行首mov bl, 80div blmul blmov bx, axjmp .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_8000shr bx, 1       ; 恢复bxinc bx          ; 将光标推进到下一个位置; 判断是否需要向上滚动一行屏幕.roll_screen:cmp bx, 2000    ; 25行x80列jl .set_cursormov esi, 0x800b_80a0mov edi, 0x800b_8000mov ecx, 1920    ; rep次数 cld             ; 传送方向cls std24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节rep movsd; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720mov bx, 3840    ; 24行*每行80个字符*每个字符加显示属性占2字节mov ecx, 80.cls:mov word [0x800b_8000+ebx], 0x0720add bx, 2loop .clsmov bx, 1920    ; 重置光标位置为最底一行行首; 根据bx重置光标位置; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位; 数据端口0x3d5.set_cursor:mov dx, 0x3d4   mov al, 0x0e   out dx, almov dx, 0x3d5mov al, bh      ; in和out 只能用al或者axout dx, almov dx, 0x3d4mov al, 0x0fout dx, almov dx, 0x3d5mov al, blout dx, al; 依次pop EDI,ESI,EBP,EBX,EDX,ECX,EAXpopadret; ===============================================================================
; Function: 读取主硬盘的1个逻辑扇区
; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址
read_hard_disk_0:; 屏蔽硬件中断; 防止对同一个硬盘控制器端口的交叉修改; 多任务环境下,当一个任务正在读硬盘时,会被另一个任务打断。如果另一个任务也访问硬盘,将破坏前一个任务对硬盘的操作状态clipush eaxpush ebxpush ecxpush edxpush 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                  ; 0x1f3pop eaxout dx, al              ; LBA地址7~0inc dx                  ; 0x1f4mov cl, 8shr eax, clout dx, al              ; in和out 操作寄存器只能是al或者ax; LBA地址15~8inc dx                  ; 0x1f5shr eax, clout dx, al              ; LBA地址23~16; 8bits端口0x1f6,低4位存放28位逻辑扇区号的24~27位;; 第4位指示硬盘号,0为主盘,1为从盘;高3位,111表示LBA模式inc dx                  ; 0x1f6shr eax, cl             or al, 0xe0             ; al 高4位设为 1110; al 低4位设为 LBA的的高4位out dx, al; 3) 请求读硬盘; ==========================; 向端口写入0x20,请求硬盘读inc dx                  ; 0x1f7mov al, 0x20out dx, al.wait:; 4) 等待硬盘读写操作完成; ===========================; 端口0x1f7既是命令端口,又是状态端口; 通过这个端口发送读写命令之后,硬盘就忙乎开了。; 0x1f7端口第7位,1为忙,0忙完了同时将第3位置1表示准备好了,; 即0x08时,主机可以发送或接收数据in al, dx               ; 0x1f7and al, 0x88            ; 取第8位和第3位cmp al, 0x08            jnz .wait; 5) 连续取出数据; ============================; 0x1f0是硬盘接口的数据端口,16bitsmov ecx, 256             ; loop循环次数,每次读取2bytesmov dx, 0x1f0           ; 0x1f0.readw:in ax, dxmov [ebx], axadd ebx, 2loop .readwpop edxpop ecxpop ebxpop eax; 放开硬件中断stiretf        ; 段间返回; ===============================================================================
; Function: 申请一个4KB物理页
; Input:
; Output: eax, 页的物理地址
allocate_memory_page:
; 搜索页映射位串查找空闲的页,并分配页push ebxpush ecx ; ???push edx ; ???; 从页映射位串的第0个比特开始搜索xor eax, eax.search_freepage:bts [page_bitmap], eax ; bit test and set, 将指定位置的比特传送到CF标志位,然后将其置位jnc .done   ; 判断位串中指定的位是否原本为0inc eaxcmp eax, page_bitmap_len * 8 ; 判断是否已经测试了位串中的所有比特jl .search_freepage; 没有可用于分配的空闲页,显示一条错误信息,并停机; 但这样是不对的。正确的做法是:看哪些已分配的页较少使用,然后将它换出到磁盘,; 腾出空间给当前需要的程序,当需要的时候再换回来mov ebx, message_page_notenoughcall sel_code_4gb_seg:show_stringhlt.done:shl eax, 12 ; 将该比特在位串中的位置数值乘以每个页的大小4KB,就是该比特对应的那个页的物理地址pop edxpop ecxpop ebxret ; 这是段内的内部过程,仅供同一段内的其他过程使用; ===============================================================================
; Function: 申请一个4KB物理页,并写入分页结构中(页目录表和页表)
; Input: ebx, 线性地址
; Output:
allocate_install_memory_page:; 在可用的物理内存中搜索空闲的页,然后根据线性地址来创建页目录项和页表项,并将页的地址填写在页表项中push eaxpush ebxpush esi; 访问页目录表,检查该线性地址对应的页目录项是否存在; 分页机制下,访问内存需要通过页目录表和页表,而这里却要访问页目录表; 要先得到要修改的那个页目录项的线性地址,把页目录当作普通页来访问mov esi, ebxand esi, 0xffc0_0000    ; 线性地址的高10位是页目录表的索引shr esi, 22shl 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, ebxshr esi, 10and esi, 0x003f_f000    ; 高10位移到中间,再清除2边or esi, 0xffc0_0000     ; 构造的高10位0x3ff指向页目录的最后一项。最后一项已在初始化时指向页目录本身; 得到页表的线性地址and ebx, 0x003f_f000shr ebx, 12shl ebx, 2      ; 中间10位移到右边,再乘以4,得到偏移量or esi, ebx     ; 得到页表项的线性地址    call allocate_memory_page ; 分配一个物理页,这才是要安装的页or eax, 0x0000_0007     ; 添加属性值0x007    mov [esi], eax          ; 将页的物理地址写入页表项pop esipop ebxpop eaxretf; ===============================================================================
; Function: 构造段描述符
; Input: 1) eax 线性基地址 2) ebx 段界限 3) ecx 属性(无关位则置0)
; Output: edx:eax 完整的8字节(64位)段描述符
make_gdt_descriptor:; 构造段描述符的低32位; 低16位,为段界限的低16位; 高16位,为段基址的低16位mov edx, eaxshl eax, 16or 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无关位需先置0retf; ===============================================================================
; Function: 在gdt中安装一个新的段描述符
; Input: edx:eax 段描述符
; Output: cx 段描述符的选择子
setup_gdt_descriptor:push eaxpush ebxpush 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], eaxmov [ebx+4], edxadd 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, dxmov bx, 8                   ; 界限值总是比gdt总字节数小1。除以8,余7(丢弃不用)   div bx                      ; 商就是所需要的描述符索引号mov cx, axshl cx, 3                   ; 将索引号移到正确位置,即左移3位,留出TI位和RPL位; 这里 TI=0, 指向gdt RPL=000; 于是生成了相应的段选择子pop edxpop ebxpop eaxretf; ===============================================================================
; Function: 在ldt中安装一个新的段描述符
; Input: edx:eax 段描述符; ebx 任务控制块TCB基地址
; Output: cx 段描述符的选择子
setup_ldt_descriptor:push eaxpush edxpush edimov edi, [ebx+0x0c]     ; 从用户程序的TCB中取得程序LDT基地址xor ecx, ecxmov cx, [ebx+0x0a]      ; 从用户程序的TCB中取得程序LDT界限inc cx                  ; LDT的总字节数,即新描述符偏移地址mov [edi+ecx+0x00], eaxmov [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, cxxor dx, dxmov cx, 8                   ; 界限值总是比gdt总字节数小1。除以8,余7(丢弃不用)   div cx                      ; 商就是所需要的描述符索引号mov cx, axshl cx, 3                   ; 将索引号移到正确位置,即左移3位,留出TI位和RPL位or cx, 0000_0000_0000_0100B ; 这里 TI=1, 指向ldt; RPL=000; 于是生成了相应的段选择子    pop edipop edxpop eaxret; ===============================================================================
; Function: 构造调用门的门描述符
; Input: eax 门代码在段内的偏移地址; bx 门代码所在段的段选择子; cx 门属性
; Output: edx:eax 门描述符
make_gate_descriptor:    push ebxpush ecxmov edx, eaxand edx, 0xffff_0000    ; 得到偏移地址高16位    or dx, cx               ; 组装属性部分到edxand eax, 0x0000_ffff    ; 得到偏移地址低16位shl ebx, 16or eax, ebx             ; 组装段选择子到eaxpop ecxpop ebxretf                ; retf 说明该过程必须以远调用的方式使用; -----------------------------------------------------------------
; Function: 在TCB链上追加任务控制块
; Input: ecx 需要追加的那项TCB线性基地址
append_to_tcb_link:cli     ; 屏蔽硬件中断push eaxpush ebxmov eax, tcb_chain_head.totailTCB:mov ebx, [eax]or ebx, ebxjz .appendTCB                ; 链表为空,或已到末尾    mov eax, ebxjmp .totailTCB.appendTCB:climov [eax], ecxmov dword [ecx], 0 ; 将当前TCB指针域清零,表示这是链表中最后一个TCBpop ebxpop eaxsti     ; 放开硬件中断ret; ===============================================================================
; Function: 创建用户任务的页目录表
; Input:
; Output: eax, 新页目录的物理地址
create_user_pdt_by_copy:; 创建新的页目录,并复制当前页目录内容push ebxpush ecxpush esipush edicall allocate_memory_pagemov ebx, eaxor 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必须为0invlpg [0xffff_fff8]    ; 0xfffffff8是内核页目录表内倒数第2个目录项,每次都用它来指向新任务的页目录表; 虽然在上一条指令中改写了它,使它指向新任务的页目录表,但这个更改只在内存中有效,没有反映到TLB中mov esi, 0xffff_f000    ; 当前页目录的线性地址mov edi, 0xffff_e000    ; 新页目录的线性地址mov ecx, 1024           ; 传送次数cld                     ; 传送方向为正向    repe movsdpop edipop esipop ecxpop ebxretf; ===============================================================================
; Function: 将ds的值以十六进制的形式在屏幕上显示
; Input:
; Output:
show_hex_dword:; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDIpushadmov ebx, bin_hexmov ecx, 8              ; 循环8次.hex2word:rol edx, 4              ; 循环左移mov eax, edxand eax, 0x0000_000f; xlat, 处理器的查表指令; 用al作为偏移量,从ds:ebx指向的内存空间中取出一个字节,传回alxlatpush ecxmov cl, alcall show_char          ; 显示pop ecxloop .hex2wordpopadretf; ===============================================================================
; 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_exitmov 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_generalcall 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 eaxiretd    ; -----------------------------------------------------------------
; 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的索引,且放开NMIout 0x70, alin al, 0x71     ; 读一下RTC的寄存器C; 找到当前任务(状态为忙的任务)在链表中的位置,即状态值为0xFFFF的节点mov eax, tcb_chain_head.search_current_task:mov ebx, [eax]or ebx, ebxjz .irtn    ; 链表为空,或已到末尾,从中断返回cmp word [ebx+0x04], 0xffff     ; 判断状态是否为忙,即是否为当前任务je .move_current_to_chaintailmov 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_chaintailmov eax, edxjmp .goto_chaintail.add_current_to_chaintail:; 将该节点添加至链表尾部mov [eax], ebxmov 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,忙为0xffffjnz .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,0x55db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xffdb  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xffdb  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xffdb  0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00db  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 0dd show_stringdw sel_code_4gb_segsys_api_2  db '@ReadDiskData'times 256-($-sys_api_2) db 0dd read_hard_disk_0dw sel_code_4gb_segsys_api_3  db '@ShowDwordAsHexString'times 256-($-sys_api_3) db 0dd show_hex_dworddw sel_code_4gb_seg            sys_api_4  db '@TerminateProgram'times 256-($-sys_api_4) db 0dd terminate_current_taskdw sel_code_4gb_segsys_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,0core_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, SALTsalt_position   dd salt             ; salt表偏移量[0x08]        salt_itmes      dd (salt_end-salt)/256  ; salt条目数[0x0c]salt:                                     ; [0x2c]ShowString      db '@ShowString'times 256-($-ShowString) db 0TerminateProgram db '@TerminateProgram'times 256-($-TerminateProgram) db 0ReadDiskData    db '@ReadDiskData'times 256-($-ReadDiskData) db 0ShowDwordAsHexString db '@ShowDwordAsHexString'times 256-($-ShowDwordAsHexString) db 0salt_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; 显示提示信息,正在运行的用户程序的当前特权级CPLmov ebx, message_usermodecall 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, SALTsalt_position   dd salt             ; salt表偏移量[0x08]        salt_itmes      dd (salt_end-salt)/256  ; salt条目数[0x0c]salt:                                     ; [0x2c]ShowString      db '@ShowString'times 256-($-ShowString) db 0TerminateProgram db '@TerminateProgram'times 256-($-TerminateProgram) db 0ReadDiskData    db '@ReadDiskData'times 256-($-ReadDiskData) db 0ShowDwordAsHexString db '@ShowDwordAsHexString'times 256-($-ShowDwordAsHexString) db 0salt_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; 显示提示信息,正在运行的用户程序的当前特权级CPLmov ebx, message_usermodecall far [ShowString]   jmp beginning; 调用系统API, 退出,并将控制权返回给内核   call far [TerminateProgram]; code_end:; ===============================================================================
; SECTION tail align=16       ; 这里用于计算程序大小,不需要vstart=0
program_end:    

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

  1. [书]x86汇编语言:从实模式到保护模式 -- 第13章 mbr加载内核、内核加载应用程序

    # mbr加载内核 1.0x7c00,16位实模式 2.进入保护模式前的准备工作:创建段描述符(代码段.数据段.堆栈段.显示缓冲区),构建gdt 3.进入保护模式 ; 开启保护模式 ; CR0的第1位 ...

  2. [书]x86汇编语言:从实模式到保护模式 -- 第11章 进入保护模式,初识全局描述符表GDT; 第12章 别名,冒泡排序

    第11章 进入保护模式:初始化全局描述符表,通过GDT进入代码段.数据段.堆栈段 ; FILE: c11_mbr.asm ; DATE: 20191229 ; TITLE: 硬盘主引导扇区代码; 设置 ...

  3. [书]x86汇编语言:从实模式到保护模式 -- 第16章 分页机制、平坦模型

    # 分页机制 二级页表:页目录.页表 ==> 4KB物理页 32位线性地址中:高10位为页目录中的索引号(乘4得偏移量),该目录项指向页表的基地址:中间10位为页表中的索引号,该页表项指向4KB ...

  4. [书]x86汇编语言:从实模式到保护模式 -- 第15章 任务切换

    # 执行结果 # TODO:字符串显示函数的滚屏部分应该是有bug. # file_02: c15_core.asm ; FILE: c13_core.asm ; DATE: 20200104 ; T ...

  5. [书]x86汇编语言:从实模式到保护模式 -- 第14章 任务和特权级保护,调用门、LDT、TSS、TCB

    # 加载用户程序 Part 1.TCB, Task Control Block, 任务控制块 分配内存作为该任务的TCB,并插入至TCB链表. Part 2.LDT, Locak Descriptor ...

  6. [书]x86汇编语言:从实模式到保护模式 -- 第九章 硬中断,使用RTC芯片实现实时时间的显示;软中断,使用BIOS中断实现键盘输入的读取和显示

    PART 1 >> 使用BIOS中断实现键盘输入的读取和显示 ; File: c09_2.asm ; Date: 20191222; =========================== ...

  7. [书]x86汇编语言:从实模式到保护模式 -- 第八章 硬盘和显卡的访问与控制,mbr加载并重定位应用程序

    第八章 硬盘和显卡的访问与控制 mbr加载.重定位用户程序 PART 1 >> VirtualBox显示最终效果 ===================================== ...

  8. [书]x86汇编语言:从实模式到保护模式 -- 第六、七章 编写主引导扇区代码

    第六章 编写主引导扇区代码(启动时显示文字:Label offset:) PART 1 >> 用VirtualBox显示最终效果 1.1 汇编 启用nasm的工具"nasm-sh ...

  9. 硬盘和显卡的访问与控制(一)——《x86汇编语言:从实模式到保护模式》读书笔记01

    本文是<x86汇编语言:从实模式到保护模式>(电子工业出版社)的读书实验笔记. 这篇文章我们先不分析代码,而是说一下在Bochs环境下如何看到实验结果. 需要的源码文件 第一个文件是加载程 ...

最新文章

  1. centos7 python3安装numpy_Centos7安装python3、numpy、scipy、matplotlib、pandas等
  2. 大专计算机学语数英理化生吗,学考11-13号报名啦!你知道语数英和政史地/物化生成绩有什么作用嘛?不懂你会后悔的!!...
  3. Servlet3.1规范翻译 - 应用生命周期事件
  4. Tableau 2020.3 发布!新增 写入外部数据库 与 预测建模 等功能,进一步增强扩展分析
  5. 高可用高并发的 9 种技术架构
  6. PageHelper分页插件的原理是什么
  7. 视觉SLAM笔记(55) 位姿图
  8. Android Fragment手柄后退按钮按下[重复]
  9. 中文 哈工大_第六届中文语法错误诊断大赛,哈工大讯飞联合实验室再获多项冠军...
  10. 基于VHDL语言的一位全加器
  11. matlab差分法解拉普拉斯方程,拉普拉斯方程有限差分法的MATLAB实现
  12. Spring常用注解
  13. Houdini使用Python给点连线
  14. D11:Chickens and Rabbits(鸡兔同笼问题,附题解)
  15. 思路分享——hdu 3233
  16. 学长的白日梦C语言题目,bzoj4030【HEOI2015】小L的白日梦
  17. python 图片中的表格识别
  18. 【题解】10-19秀秀的森林(forest)
  19. 海康威视校招C++岗面经
  20. 拼多多店铺销量达到多少适合做付费推广?

热门文章

  1. 实战经验!视觉设计师进阶指南17条-20140104早读课
  2. python三维图形渲染 地图_原来炫酷的可视化地图,用Python就能搞定!
  3. 制作一个简单的FLV播放器 【转】
  4. 如何进行企业新闻传播策划
  5. Modernizr.js的介绍及使用
  6. AB测试实战案例讲解及踩坑事项
  7. AI几何&代数数学题自动答题系统(源码&教程)
  8. xm、midi音频格式文件互相转化方法及工具
  9. 安全访问服务边缘市场深度分析及发展研究预测报告
  10. 南京计算机科学与技术专业就业怎样,南京北大青鸟:计算机科学与技术就业方向与前景...