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

# mbr加载内核

1、0x7c00,16位实模式

2、进入保护模式前的准备工作:创建段描述符(代码段、数据段、堆栈段、显示缓冲区),构建gdt

3、进入保护模式

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

4、从硬盘加载内核到内存中

5、为内核创建段描述符(内核代码段、内核数据段、系统API代码段),并写入gdt中

6、512字节(1个扇区)的MBR

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

 

# 内核加载用户程序

1、先读取一个扇区(读到内核程序自己的数据段中),从用户程序的头部信息中得到整个程序的大小

2、根据程序大小,为用户程序分配物理内存

2.1 由于这里硬盘的读取是以扇区512字节为单位,所以分配的内存也需是512的倍数

3、从硬盘加载整个用户程序到已分配的物理内存中

4、根据用户程序的头部信息,为用户程序创建段描述符(头部段、代码段、数据段、堆栈段),并写入gdt中

4.1 根据段基址、段界限、段属性构造段描述符,再将段选择子回填至用户程序头部

4.2 为用户堆栈段分配内存

5、重定位用户程序所调用的系统API,回填对应的入口地址至用户程序头部

    call sel_sys_routine_seg:read_hard_disk_0 ; 先读一个扇区
                                              ; 包含了头部信息:程序大小、入口点、段重定位表
                                              
    ; 判断需要加载的整个程序有多大
    mov eax, [core_buf]             ; 0x00, 应用程序的头部包含了程序大小
    mov ebx, eax
    and ebx, 0xfffffe00             ; 能被512整除的数,其低9位都为0
                                    ; 将低9位清零,等于是去掉那些不足512字节的零头
    add ebx, 512                    ; 加上512,等于是将那些零头凑整
    test eax, 0x000001ff            ; 判断程序大小是否恰好为512的倍数
    cmovnz eax, ebx                 ; 条件传送指令,nz 不为零则传送
                                    ; 为零,则不传送,依然采用用户程序原本的长度值eax
                                    
    mov ecx, eax                    ; 需要申请的内存大小
    call sel_sys_routine_seg:allocate_memory
    mov ebx, ecx                    ; 申请到的内存首地址
                                    ; 作为起始地址,从硬盘上加载整个用户程序

    ; 从硬盘上加载整个用户程序到已分配的物理内存中
    xor edx, edx
    mov ecx, 512
    div ecx                         ; 用户程序占硬盘的逻辑扇区个数
    mov ecx, eax                    ; 循环读取的次数

    mov eax, sel_mem_0_4gb_seg
    mov ds, eax                     ; 切换ds到0~4GB的段
    
    mov eax, esi                    ; 起始扇区号
    push ebx                        ; 用于后面访问用户程序头部
 .loop_read_hard_disk:
    call sel_sys_routine_seg:read_hard_disk_0
                                    ; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址
    inc eax
    add ebx, 512
    loop .loop_read_hard_disk       ; 循环读    
    
    ; 根据头部信息创建段描述符
    pop edi                         ; 恢复程序装载的首地址
    ; 创建gdt第#8号描述符
    ; 建立用户程序头部段描述符
    mov eax, edi                    ; 基地址
    mov ebx, [edi+0x04]             ; 0x04, 应用程序的头部包含了用户程序头部段的长度
    dec ebx                         ; 粒度为字节的段,段界限在数值上等于段长度减1
    mov ecx, 0x0040_9200            ; 字节粒度的数据段属性值(无关位则置0)
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [edi+0x04], cx              ; 将该段的段选择子写回到用户程序头部

 

# GDT(Global Descriptor Table)全局描述符表

1、gdt由mbr从实模式进入保护模式之前创建,并写入了mbr自己所需的段描述符。

lgdt指令(load gdt)、sgdt(store gdt)。

2、内核所需要的段选择子及其编号,由内核和mbr规范化定义好。

mbr加载完内核后,根据内核的头部信息,构建内核所需的段描述符,其编号与内核中定义的一致。

3、用户程序所需的段选择子及其编号,是在用户程序需要加载并执行时,由内核临时构建。

用户程序的段选择子编号,是内核动态构建好之后,再写回用户程序头部。

    mov eax, [segment_stack]
    mov ss, eax     ; ss切换到用户程序自己的堆栈,并初始化esp为0    
    mov esp, 0
    
    mov eax, [segment_data]
    mov ds, eax     ; ds切换到用户程序自己的数据段  

 

# 运行结果

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

 

# file_01: c13_mbr.asm

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

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

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

; 计算gdt所在的逻辑段地址
; 64位的被除数在edx:eax中
mov eax, [cs:gdt_base+0x7c00]
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~4GB的线性地址空间
; 该段:线性基地址为0,段界限为0xF FFFF,粒度为4KB
mov dword [ebx+0x08], 0x0000ffff     
mov dword [ebx+0x0c], 0x00cf9200

; 创建gdt第#2号描述符,保护模式下的代码段描述符,对应0x7c00~0x7dff的线性地址空间
; 该段:线性基地址为0x0000 7c00,段界限为0x0 01FF,粒度为1字节(G=0)
mov dword [ebx+0x10], 0x7c0001ff     
mov dword [ebx+0x14], 0x00409800

; 创建gdt第#3号描述符,保护模式下的堆栈段描述符,对应0x6c00~0x7c00的线性地址空间
; 该段:线性基地址为0x0000 7c00,段界限为0xF FFFE,粒度为4KB
mov dword [ebx+0x18],0x7c00fffe
mov dword [ebx+0x1c],0x00cf9600

; 创建gdt第#4号描述符,保护模式下的显示缓冲区描述符,对应0xb 8000~0xb ffff的线性地址空间
; 该段:线性基地址为0x000b 8000,段界限为0x0 7FFF,粒度为1字节
mov dword [ebx+0x20],0x80007fff
mov dword [ebx+0x24],0x0040920b


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



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

; 打开地址线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 0x0010:flush      ; 不管是16位还是32位远转移,现在已经处于保护模式下,
                            ; 处理器将把第一个操作数0x0010视为段选择子,而不是是模式下的逻辑段地址
                            ; 段选择子0x0010,即 0000_0000_00010_0_00(PRL为00,TI为0,索引号为2)
                            ; 当指令执行时,处理器加载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, 0x0018
    mov eax, 0000_0000_00001_0_00B   ; 已初始化的GDT中,数据段为第1号描述符
    mov ds, eax  ; 当处理器执行任何改变段选择器的指令时,就将指令中提供的索引号乘以8作为偏移地址,同GDTR中提供的线性地址相加,
                 ; 以访问GDT,将找到的描述符加载到不可见的描述符高速缓存部分

    ; 设置堆栈段ss和段指针esp
    mov eax, 0x0018                 ; 已初始化GDT中,栈段为第3号描述符
    mov ss, eax
    xor esp, esp

    ; 加载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                ; 实际长度小于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:
    mov esi, [0x7c00+gdt_base]     ; 通过数据段来写入需添加的描述符
    
    ; 创建gdt第#5号描述符
    ; 建立系统API代码段描述符
    mov eax, [edi+0x04]             ; 0x04, 应用程序的头部包含了系统API代码段的起始汇编地址
    mov ebx, [edi+0x08]             ; 0x08, 应用程序的头部包含了系统API代码段的长度
    ; sub ebx, eax          ; 系统API代码段长度
    dec ebx                 ; 粒度为字节的段,段界限在数值上等于段长度减1
    add eax, edi            ; 系统API代码段在物理内存中的基地址
    mov ecx, 0x00409800     ; 字节粒度的代码段属性值(无关位则置0)
    call make_gdt_descriptor
    mov [esi+0x28], eax     ; 写入第#5号描述符至gdt    
    mov [esi+0x2c], edx
    
    ; 创建gdt第#6号描述符
    ; 建立mini内核数据段描述符
    mov eax, [edi+0x0c]             ; 0x0c, 应用程序的头部包含了mini内核数据段的起始汇编地址
    mov ebx, [edi+0x10]             ; 0x10, 应用程序的头部包含了mini内核数据段的长度
    ; sub ebx, eax          ; mini内核数据段长度
    dec ebx                 ; 段界限
    add eax, edi            ; mini内核数据段在物理内存中的基地址
    mov ecx, 0x00409200     ; 字节粒度的数据段属性值(无关位则置0)
    call make_gdt_descriptor
    mov [esi+0x30], eax
    mov [esi+0x34], edx

    ; 创建gdt第#7号描述符
    ; 建立mini内核代码段描述符
    mov eax, [edi+0x14]             ; 0x14, 应用程序的头部包含了mini内核代码段的起始汇编地址
    mov ebx, [edi+0x18]             ; 0x18, 应用程序的头部包含了mini内核代码段的长度
    ; sub ebx, eax          ; mini内核代码段长度
    dec ebx                 ; 段界限
    add eax, edi            ; mini内核代码段在物理内存中的基地址
    mov ecx, 0x00409800     ; 字节粒度的代码段属性值(无关位则置0)
    call make_gdt_descriptor
    mov [esi+0x38], eax
    mov [esi+0x3c], edx

    ; 重新初始化描述符表寄存器GDTR
    ; 描述符表的界限(总字节数减1)。这里共有8个描述符,每个描述符8字节,共64字节    
    mov word [gdt_size+0x7c00], 63
    
    
    ; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
    ; GDTR, 全局描述符表寄存器
    lgdt [gdt_size+0x7c00]

    ; 开始执行mini内核代码
    jmp far [edi+0x1c]              ; 0x1c, 应用程序的头部包含了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


    

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



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


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

 

# file_02: c13_core.asm

; FILE: c13_core.asm
; DATE: 20200104
; TITLE: mini内核

; 常量
; 伪指令equ仅仅是允许用符号代替具体的数值,但声明的数值并不占用空间
; 这些选择子对应的gdt描述符会在mbr中的内核初始化阶段创建
; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)
sel_core_code_seg   equ 0x38    ; gdt第7号描述符,内核代码段选择子
sel_core_data_seg   equ 0x30    ; gdt第6号描述符,内核数据段选择子
sel_sys_routine_seg equ 0x28    ; gdt第5号描述符,系统API代码段的选择子
sel_video_ram_seg   equ 0x20    ; gdt第4号描述符,视频显示缓冲区的段选择子
sel_core_stack_seg  equ 0x18    ; gdt第3号描述符,内核堆栈段选择子
sel_mem_0_4gb_seg   equ 0x08    ; gdt第1号描述符,整个0~4GB内存的段选择子

app_lba_begin       equ 50      ; 将配套的的用户程序从磁盘lba逻辑扇区50开始写入

; ===============================================================================
SECTION head vstart=0               ; mini内核的头部,用于mbr加载mini内核

core_length         dd core_end                     ; mini内核总长度, 0x00

segment_sys_routine dd section.sys_routine.start    ; 系统API代码段起始汇编地址,0x04
sys_routine_length  dd sys_routine_end              ; 0x08

segment_core_data   dd section.core_data.start      ; mini内核数据段起始汇编地址,0x0c
core_data_length    dd core_data_end                ; 0x10
 
segment_core_code   dd section.core_code.start      ; mini内核代码段起始汇编地址,0x14
core_code_length    dd core_code_end                ; 0x18

core_entry          dd beginning                    ; mini内核入口点(32位的段内偏移地址),0x1c
                    dw sel_core_code_seg            ; 16位的段选择子


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


; ===============================================================================
SECTION core_code vstart=0               ; mini内核代码
beginning:
    mov ecx, sel_core_data_seg
    mov ds, ecx                 ; 使ds指向mini内核数据段

    ; 显示提示信息,内核已加载成功并开始执行
    mov ebx, message_kernel_load_succ
    call sel_sys_routine_seg:show_string ; 调用系统api,显示一段文字
                                         ; call 段选择子:段内偏移
    
    ; 获取处理器品牌信息
    mov eax, 0          ; 先用0号功能探测处理器最大能支持的功能号
    cpuid               ; 会在eax中返回最大可支持的功能号
    
    ; 要返回处理器品牌信息,需使用0x80000002~0x80000004号功能,分3次进行
    mov eax, 0x80000002
    cpuid
    mov [cpu_brand], eax
    mov [cpu_brand+0x04], ebx
    mov [cpu_brand+0x08], ecx
    mov [cpu_brand+0x0c], edx
    
    mov eax, 0x80000003
    cpuid
    mov [cpu_brand+0x10], eax
    mov [cpu_brand+0x14], ebx
    mov [cpu_brand+0x18], ecx
    mov [cpu_brand+0x1c], edx

    mov eax, 0x80000004
    cpuid
    mov [cpu_brand+0x20], eax
    mov [cpu_brand+0x24], ebx
    mov [cpu_brand+0x28], ecx
    mov [cpu_brand+0x2c], edx
    
    ; 显示处理器品牌信息
    mov ebx, cpu_brand0         ; 空行
    call sel_sys_routine_seg:show_string
    mov ebx, cpu_brand          ; 处理器品牌信息
    call sel_sys_routine_seg:show_string
    mov ebx, cpu_brand1         ; 空行
    call sel_sys_routine_seg:show_string
    
    ; 显示提示信息,开始加载用户程序
    mov ebx, message_app_load_begin
    call sel_sys_routine_seg:show_string
    
    ; 加载并重定位用户程序
    mov esi, app_lba_begin
    call load_relocate_program
    
    ; 显示提示信息,用户程序加载完成
    mov ebx, message_app_load_succ
    call sel_sys_routine_seg:show_string
    
    mov [kernel_esp_pointer], esp   ; 临时保存内核的堆栈指针
                                    ; 进入用户程序后,会切换到用户的堆栈
                                    ; 从用户程序返回时,可通过这里还原内核栈指针

    mov ds, ax      ; 使ds指向用户程序头部段
                    ; 此处的ax值是load_relocate_program的返回值

    jmp far [0x10]  ; 跳转到用户程序执行,控制权交给用户程序
                    ; 0x10, 应用程序的头部包含了用户程序的入口点
    
    
    
    
; Function: 加载并重定位用户程序
; Input: esi 起始逻辑扇区号
; Output: ax 指向用户程序头部的段选择子
load_relocate_program:
    
    push ebx
    push ecx
    push edx
    push esi
    push edi
    push ds
    push es
    
    mov eax, sel_core_data_seg          ; 切换ds到内核数据段
    mov ds, eax
    
    mov eax, esi
    mov ebx, core_buf                   ; 自定义的一段内核缓冲区
                                        ; 在内核中开辟出一段固定的空间,有便于分析、加工和中转数据
    call sel_sys_routine_seg:read_hard_disk_0 ; 先读一个扇区
                                              ; 包含了头部信息:程序大小、入口点、段重定位表
                                              
    ; 判断需要加载的整个程序有多大
    mov eax, [core_buf]             ; 0x00, 应用程序的头部包含了程序大小
    mov ebx, eax
    and ebx, 0xfffffe00             ; 能被512整除的数,其低9位都为0
                                    ; 将低9位清零,等于是去掉那些不足512字节的零头
    add ebx, 512                    ; 加上512,等于是将那些零头凑整
    test eax, 0x000001ff            ; 判断程序大小是否恰好为512的倍数
    cmovnz eax, ebx                 ; 条件传送指令,nz 不为零则传送
                                    ; 为零,则不传送,依然采用用户程序原本的长度值eax
                                    
    mov ecx, eax                    ; 需要申请的内存大小
    call sel_sys_routine_seg:allocate_memory
    mov ebx, ecx                    ; 申请到的内存首地址
                                    ; 作为起始地址,从硬盘上加载整个用户程序
                                    
    push ebx                        ; 用于后面访问用户程序头部

    ; 从硬盘上加载整个用户程序到已分配的物理内存中
    xor edx, edx
    mov ecx, 512
    div ecx                         ; 用户程序占硬盘的逻辑扇区个数
    mov ecx, eax                    ; 循环读取的次数

    mov eax, sel_mem_0_4gb_seg
    mov ds, eax                     ; 切换ds到0~4GB的段
    
    mov eax, esi                    ; 起始扇区号
 .loop_read_hard_disk:
    call sel_sys_routine_seg:read_hard_disk_0
                                    ; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址
    inc eax
    add ebx, 512
    loop .loop_read_hard_disk       ; 循环读    
    
    ; 根据头部信息创建段描述符
    pop edi                         ; 弹出ebx,恢复程序装载的首地址
    ; 创建gdt第#8号描述符
    ; 建立用户程序头部段描述符
    mov eax, edi                    ; 基地址
    mov ebx, [edi+0x04]             ; 0x04, 应用程序的头部包含了用户程序头部段的长度
    dec ebx                         ; 粒度为字节的段,段界限在数值上等于段长度减1
    mov ecx, 0x0040_9200            ; 字节粒度的数据段属性值(无关位则置0)
    call sel_sys_routine_seg:make_gdt_descriptor    ; 构建段描述符
    call sel_sys_routine_seg:setup_gdt_descriptor   ; 写入gdt
    mov [edi+0x04], cx              ; 0x04, 将该段的段选择子写回到用户程序头部
    
    ; 创建gdt第#9号描述符
    ; 建立用户程序代码段描述符
    mov eax, edi
    add eax, [edi+0x18]             ; 0x18, 应用程序的头部包含了用户程序代码段的起始汇编地址
                                    ; 内核加载用户程序的首地址,加上代码段的起始汇编地址,得到代码段在物理内存中的基地址
    mov ebx, [edi+0x1c]             ; 0x1c, 应用程序的头部包含了用户程序代码段长度
    dec ebx                         ; 段界限
    mov ecx, 0x0040_9800            ; 字节粒度的代码段属性值(无关位则置0)
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    ; mov [edi+0x18], cx              ; 0x18, 将该段的段选择子写回到用户程序头部
    mov [edi+0x14], cx              ; 0x14, 将该段的段选择子写回到用户程序头部
                                    ; 应用程序头部中,和0x10处的双字一起,共同组成一个6字节的入口点,内核从这里转移控制给用户程序
    
    ; 创建gdt第#10号描述符
    ; 建立用户程序数据段描述符
    mov eax, edi
    add eax, [edi+0x20]             ; 0x20, 应用程序的头部包含了用户程序数据段的起始汇编地址
                                    ; 内核加载用户程序的首地址,加上数据段的起始汇编地址,得到数据段在物理内存中的基地址
    mov ebx, [edi+0x24]             ; 0x24, 应用程序的头部包含了用户程序数据段长度
    dec ebx                         ; 段界限
    mov ecx, 0x0040_9200            ; 字节粒度的数据段属性值(无关位则置0)
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [edi+0x20], cx     
    
    ; 创建gdt第#11号描述符
    ; 建立用户程序堆栈段描述符
    mov ecx, [edi+0x0c]             ; 0x0c, 应用程序的头部包含了用户程序栈段大小,以4KB为单位
    ; 计算栈段的界限
    ; 粒度为4KB,栈段界限值=0xFFFFF - 栈段大小(4KB个数), 例如 0xFFFFF-2=0xFFFFD
    ; 当处理器访问该栈段时,实际使用的段界限为 (0xFFFFD+1)*0x1000 - 1 = 0xFFFFDFFF
    ; 即,ESP的值只允许在0xFFFF DFFF和0xFFFF FFFF之间变化,共8KB
    ; 4KB, 即2^12=0x1000; 4GB, 即2^32; 4GB/4KB=2^20=0x10_0000, 段界限=段长-1=0xF_FFFF
    mov ebx, 0x000f_ffff
    sub ebx, ecx                    ; 段界限
    mov eax, 0x0000_1000            ; 粒度为4KB
    ; 32位eax乘另一个32位,结果为edx:eax
    mul dword [edi+0x0c]            ; 栈大小
    mov ecx, eax                    ; 准备为堆栈分配内存, eax为上面乘的结果,即栈大小
    call sel_sys_routine_seg:allocate_memory
    add eax, ecx                    ; 和数据段不通,栈描述符的基地址是栈空间的高端地址
    mov ecx, 0x00c0_9600            ; 4KB粒度的堆栈段属性值(无关位则置0)
    call sel_sys_routine_seg:make_gdt_descriptor
    call sel_sys_routine_seg:setup_gdt_descriptor
    mov [edi+0x08], cx              ; 0x08, 写回到应用程序的头部
    


    ; 重定位用户程序所调用的系统API
    ; 回填它们对应的入口地址
    ; 内外循环:外循环依次取出用户程序需调用的系统api,内循环遍历内核所有的系统api找到用户需调用那个
    mov eax, [edi+0x04]
    mov es, eax                     ; 使es指向用户程序头部段
    mov eax, sel_core_data_seg
    mov ds, eax                     ; 使ds指向mini内核数据段
    
    cld     ; 清标志寄存器EFLAGS中的方向标志位,使cmps指令正向比较
    mov ecx, [es:0x28]              ; 0x28, 应用程序的头部包含了所需调用系统API个数
                                    ; 外循环次数
    mov edi, 0x2c                   ; 0x2c, 应用程序头部中调用系统api列表的起始偏移地址
 .search_sys_api_external:
    push ecx
    push edi
    
    mov ecx, sys_api_items          ; 内循环次数
    mov esi, sys_api                ; 内核中系统api列表的起始偏移地址
 .search_sys_api_internal:
    push esi
    push edi
    push ecx
    
    mov ecx, 64             ; 检索表中,每一条的比较次数
                            ; 每一项256字节,每次比较4字节,故64次
    repe cmpsd              ; cmpsd每次比较4字节,repe如果相同则继续
    jnz .b4                 ; ZF=1, 即结果为0,表示比较结果为相同,ZF=0, 即结果为1,不同
                            ; 不同,则开始下一条目的比较
                            
    ; 将系统api的入口地址写回到用户程序头部中对应api条目的开始6字节
    mov eax, [esi]          ; 匹配成功时,esi指向每个条目后的入口地址
    mov [es:edi-256], eax
    mov ax, [esi+4]         ; 对应的段选择子
    mov [es:edi-252], ax
 .b4:
    pop ecx
    pop edi
    pop esi
    add esi, sys_api_item_length    ; 内核中系统api列表的下一条目的偏移地址
    loop .search_sys_api_internal
    
    pop edi
    pop ecx
    add edi, 256                    ; 应用程序头部中调用系统api列表的下一条目的偏移地址
    loop .search_sys_api_external

    ; 用户程序加载并重定位完成    
    mov ax, [es:0x04]               ; 0x04, 加载时已回写到用户程序头部中的头部段选择子

    pop es      
    pop ds
    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    
    ret
    
; 内核重新接管处理器的控制权    
return_kernel:    
    mov eax, sel_core_data_seg
    mov ds, eax                 ; 使ds指向mini内核数据段
    
    mov eax, sel_core_stack_seg
    mov ss, eax                 ; 使ss指向mini内核堆栈段
    mov esp, [kernel_esp_pointer]
    
    mov ebx, message_kernelmode ; 显示提示信息,已返回内核态
    call sel_sys_routine_seg:show_string
    
    ; 对于一个操作系统来说,此刻应该回收前一个用户程序所占用的内存,并启动下一个用户程序
    
    hlt     ; 进入保护模式之前,用cli指令关闭了中断,所以,
            ; 这里除非有NMI产生,否则处理器将一直处于停机状态
    
core_code_end:

    
    
; ===============================================================================
SECTION core_data vstart=0               ; mini内核数据段

; sgdt, Store Global Descriptor Table Register
; 将gdtr寄存器的基地址和边界信息保存到指定的内存位置
; 低2字节为gdt界限(大小),高4字节为gdt的32位物理地址
; lgdt, load gdt, 指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
gdt_size dw 0
gdt_base dd 0

; 内存分配时的起始地址
; 每次请求分配内存时,返回这个值,作为所分配内存的起始地址;
; 同时,将这个值加上所分配的长度,作为下次分配的起始地址写回该内存单元
ram_allocate_base dd 0x0010_0000

; 系统API的符号-地址检索表
; 自命名 Symbol-Address Lookup Table, SALT
sys_api:
 sys_api_1  db '@ShowString'
            times 256-($-sys_api_1) db 0
            dd show_string
            dw sel_sys_routine_seg
            
 sys_api_2  db '@ReadDiskData'
            times 256-($-sys_api_2) db 0
            dd read_hard_disk_0
            dw sel_sys_routine_seg

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

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



; 提示信息,内核已加载成功并开始执行
message_kernel_load_succ db '  If you seen this message,that means we '
            db 'are now in protect mode,and the system '
            db 'core is loaded,and the video display '
            db 'routine works perfectly.', 0x0d, 0x0a, 0

; 提示信息,开始加载用户程序            
message_app_load_begin  db '  Loading user program...', 0

; 提示信息,用户程序加载并重定位完成
message_app_load_succ   db 'Done.', 0x0d, 0x0a, 0

message_kernelmode      db  0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
                        db  '  User program terminated,control returned.',0

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

core_buf    times 2048 db 0             ; 自定义的内核缓冲区
    
kernel_esp_pointer  dd 0                ; 临时保存内核的堆栈指针


core_data_end:


                    
; ===============================================================================
SECTION sys_routine vstart=0               ; 系统api代码段

; Function: 频幕上显示文本,并移动光标
; Input: ds:ebx 字符串起始地址,以0结尾
show_string:
    push ecx
 .loop_show_string:
    mov cl, [ebx]
    or cl, cl
    jz .exit                ; 以0结尾
    call show_char
    inc ebx
    jmp .loop_show_string
    
 .exit:
    pop ecx
    retf                    ; 段间调用返回

; Function: 
; Input: cl 字符
show_char:

    ; 依次push EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI
    pushad
    
    ; 读取当前光标位置
    ; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
    ; 数据端口0x3d5
    mov dx, 0x3d4   
    mov al, 0x0e   
    out dx, al
    mov dx, 0x3d5
    in al, dx
    mov ah, al
    
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x3d5
    in al, dx
    mov bx, ax      ; 此处用bx存放光标位置的16位数
    
 ; 判断是否为回车符0x0d
    cmp cl, 0x0d    ; 0x0d 为回车符
    jnz .show_0a    ; 不是回车符0x0d,再判断是否换行符0x0a
    mov ax, bx      ; 是回车符,则将光标置位到行首
    mov bl, 80
    div bl
    mul bl
    mov bx, ax
    jmp .set_cursor
    
    ; ; 将光标位置移到行首,可以直接减去当前行吗??
    ; mov ax, bx
    ; mov dl, 80
    ; div dl
    ; sub bx, ah
    ; jmp .set_cursor
    
 
 ; 判断是否为换行符0x0a
 .show_0a:
    cmp cl, 0x0a    ; 0x0a 为换行符    
    jnz .show_normal; 不是换行符,则正常显示字符
    add bx, 80      ; 是换行符,再判断是否需要滚屏
    jmp .roll_screen
 
 ; 正常显示字符
 ; 在写入其它内容之前,显存里全是黑底白字的空白字符0x0720,所以可以不重写黑底白字的属性
 .show_normal:
    push es
    
    mov eax, sel_video_ram_seg  ; 0xb8000段的选择子,显存映射在 0xb8000~0xbffff
    mov es, eax
    shl bx, 1       ; 光标指示字符位置,显存中一个字符占2字节,光标位置乘2得到该字符在显存中得偏移地址    
    mov [es:bx], cl
    
    pop es
    
    shr bx, 1       ; 恢复bx
    inc bx          ; 将光标推进到下一个位置
    
 ; 判断是否需要向上滚动一行屏幕
 .roll_screen:
    cmp bx, 2000    ; 25行x80列
    jl .set_cursor
    
    push ds
    push es
    
    mov eax, sel_video_ram_seg    
    mov ds, eax      ; movsd的源地址ds:esi
    mov es, eax      ; movsd的目的地址es:edi
    mov esi, 0xa0
    mov edi, 0
    cld             ; 传送方向cls std
    mov cx, 1920    ; rep次数 24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节
    rep movsd
    
    ; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720
    mov bx, 3840    ; 24行*每行80个字符*每个字符加显示属性占2字节
    mov cx, 80
 .cls:
    mov word [es:bx], 0x0720
    add bx, 2
    loop .cls
    
    pop es
    pop ds
    
    mov bx, 1920    ; 重置光标位置为最底一行行首
 
 ; 根据bx重置光标位置
 ; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
 ; 数据端口0x3d5
 .set_cursor:
    mov dx, 0x3d4   
    mov al, 0x0e   
    out dx, al
    mov dx, 0x3d5
    mov al, bh      ; in和out 只能用al或者ax
    out dx, al
    
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x3d5
    mov al, bl
    out dx, al
    
    ; 依次pop EDI,ESI,EBP,EBX,EDX,ECX,EAX
    popad

    ret
                   


; ===============================================================================    
; Function: 读取主硬盘的1个逻辑扇区
; Input: 1) eax 起始逻辑扇区号 2) ds:ebx 目标缓冲区地址
read_hard_disk_0:

    push eax
    push ebx
    push ecx
    push edx

    push eax
    ; 1) 设置要读取的扇区数
    ; ==========================
    ; 向0x1f2端口写入要读取的扇区数。每读取一个扇区,数值会减1;
    ; 若读写过程中发生错误,该端口包含着尚未读取的扇区数
    mov dx, 0x1f2           ; 0x1f2为8位端口
    mov al, 1               ; 1个扇区
    out dx, al
    
    ; 2) 设置起始扇区号
    ; ===========================
    ; 扇区的读写是连续的。这里采用早期的LBA28逻辑扇区编址方法,
    ; 28个比特表示逻辑扇区号,每个扇区512字节,所以LBA25可管理128G的硬盘
    ; 28位的扇区号分成4段,分别写入端口0x1f3 0x1f4 0x1f5 0x1f6,都是8位端口
    inc dx                  ; 0x1f3
    pop eax
    out dx, al              ; LBA地址7~0
    
    inc dx                  ; 0x1f4
    mov cl, 8
    shr eax, cl
    out dx, al              ; in和out 操作寄存器只能是al或者ax
                            ; LBA地址15~8
                            
    inc dx                  ; 0x1f5
    shr eax, cl
    out dx, al              ; LBA地址23~16

    ; 8bits端口0x1f6,低4位存放28位逻辑扇区号的24~27位;
    ; 第4位指示硬盘号,0为主盘,1为从盘;高3位,111表示LBA模式
    inc dx                  ; 0x1f6
    shr eax, cl             
    or al, 0xe0             ; al 高4位设为 1110
                            ; al 低4位设为 LBA的的高4位
    out dx, al

    ; 3) 请求读硬盘
    ; ==========================
    ; 向端口写入0x20,请求硬盘读
    inc dx                  ; 0x1f7
    mov al, 0x20
    out dx, al
    
 .wait:
    ; 4) 等待硬盘读写操作完成
    ; ===========================
    ; 端口0x1f7既是命令端口,又是状态端口
    ; 通过这个端口发送读写命令之后,硬盘就忙乎开了。
    ; 0x1f7端口第7位,1为忙,0忙完了同时将第3位置1表示准备好了,
    ; 即0x08时,主机可以发送或接收数据
    in al, dx               ; 0x1f7
    and al, 0x88            ; 取第8位和第3位
    cmp al, 0x08            
    jnz .wait
    
    ; 5) 连续取出数据
    ; ============================
    ; 0x1f0是硬盘接口的数据端口,16bits
    mov ecx, 256             ; loop循环次数,每次读取2bytes
    mov dx, 0x1f0           ; 0x1f0
 .readw:
    in ax, dx
    mov [ebx], ax
    add ebx, 2
    loop .readw
    
    pop edx
    pop ecx
    pop ebx
    pop eax
    
    retf        ; 段间返回




; ===============================================================================    
; Function: 分配内存
; Input: ecx 希望分配的字节数
; Output: ecx 起始地址
allocate_memory:

    push eax
    push ebx
    push ds
    
    mov eax, sel_core_data_seg
    mov ds, eax                     ; 切换ds到内核数据段
    
    mov eax, [ram_allocate_base]
    add eax, ecx                    ; 下次分配时的起始地址    
    
    ; 这里应当检测可用内存数量,但本程序很简单,就忽略了
    
    mov ecx, [ram_allocate_base]    ; 返回分配的起始地址
    
    ; 4字节对齐下次分配时的起始地址, 即最低2位为0
    ; 32位的系统建议内存地址最好是4字节对齐,这样访问速度能最快
    mov ebx, eax
    and ebx, 0xffff_fffc
    add ebx, 4                      ; 4字节对齐
    test eax, 0x0000_0003           ; 判断是否对齐
    cmovnz eax, ebx                 ; 如果非零,即没有对齐,则强制对齐
                                    ; cmovcc避免了低效率的控制转移
    mov [ram_allocate_base], eax    ; 下次分配时的起始地址
    
    pop ds
    pop ebx
    pop eax

    retf        ; retf指令返回,因此只能通过远过程调用来进入




; ===============================================================================    
; Function: 构造段描述符
; Input: 1) eax 线性基地址 2) ebx 段界限 3) ecx 属性(无关位则置0)
; Output: edx:eax 完整的8字节(64位)段描述符
make_gdt_descriptor:
    ; 构造段描述符的低32位
    ; 低16位,为段界限的低16位; 高16位,为段基址的低16位
    mov edx, eax
    shl eax, 16
    or ax, bx           ; 段描述符低32位(eax)构造完毕
    
    ; 段基地址在描述符高32位edx两边就位
    and edx, 0xffff0000 ; 清除基地址的低32位(低32位前面已处理完成)    
    rol edx, 8          ; rol循环左移
    bswap edx           ; bswap, byte swap 字节交换

    ; 段界限的高4位在描述符高32位中就位
    and ebx, 0x000f0000 ; 20位的段界限只保留高4位(低16位前面已处理完成)
    or edx, ebx

    ; 段属性在描述符高32位中就位
    or edx, ecx         ; 入参的段界限ecx无关位需先置0
    
    retf
    
    
    
    
; ===============================================================================    
; Function: 在gdt中安装一个新的段描述符
; Input: edx:eax 段描述符
; Output: cx 段描述符的选择子
setup_gdt_descriptor:
    
    push eax
    push ebx
    push edx
    push ds
    push es
    
    mov ebx, sel_core_data_seg  ; 切换ds到内核数据段
    mov ds, ebx
    
    ; sgdt, Store Global Descriptor Table Register
    ; 将gdtr寄存器的基地址和边界信息保存到指定的内存位置
    ; 低2字节为gdt界限(大小),高4字节为gdt的32位物理地址
    sgdt [gdt_size]
    
    mov ebx, sel_mem_0_4gb_seg
    mov es, ebx                 ; 使es指向4GB内存段以操作全局描述符表gdt
    
    ; movzx, Move with Zero-Extend, 左边添加0扩展
    ; 或使用这2条指令替换movzx指令 xor ebx, ebx; mov bx, [gdt_size]
    movzx ebx, word [gdt_size]  ; gdt界限
    inc bx                      ; gdt总字节数,也是gdt中下一个描述符的偏移
                                ; 若使用inc ebx, 如果是启动计算机以来第一次在gdt中安装描述符就会有问题
    add ebx, [gdt_base]         ; 下一个描述符的线性地址
    
    mov [es:ebx], eax
    mov [es:ebx+4], edx
    
    add word [gdt_size], 8      ; 将gdt的界限值加8,每个描述符8字节

    ; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
    ; GDTR, 全局描述符表寄存器
    lgdt [gdt_size]             ; 对gdt的更改生效
    
    ; 生成相应的段选择子
    ; 段选择子:15~3位,描述符索引;2, TI(0为GDT,1为LDT); 1~0位,RPL(特权级)
    mov ax, [gdt_size]
    xor dx, dx
    mov bx, 8                   ; 界限值总是比gdt总字节数小1。除以8,余7(丢弃不用)   
    div bx                      ; 商就是所需要的描述符索引号
    mov cx, ax
    shl cx, 3                   ; 将索引号移到正确位置,即左移3位,留出TI位和RPL位
                                ; 这里 TI=0, 指向gdt RPL=000
                                ; 于是生成了相应的段选择子
    pop es
    pop ds
    pop edx
    pop ebx
    pop eax
    
    retf
    
    
    
; ===============================================================================    
; Function: 
; Input: 
; Output: 
show_hex_dword:
    ; 这里暂时不需要

                                
sys_routine_end:


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

    

 

# file_03: c13.asm

; FILE: c13.asm
; DATE: 20200111
; TITLE: 用户程序

diskdata_txt_lba_begin       equ 100      ; 将配套的的txt文档数据从磁盘lba逻辑扇区100开始写入

; ===============================================================================
SECTION head vstart=0                       ; 定义用户程序头部段
    ; 用户程序可能很大,16位可能不够
    program_length  dd program_end      ; 程序总长度[0x00]
    
    head_length     dd head_end         ; 程序头部的长度[0x04]
                                        ; 以字节为单位
    
    ; 由内核动态分配栈空间
    ; 当内核分配了栈空间后,会把栈段的选择子填写到这里,用户程序开始执行时从这里读取该选择子
    segment_stack   dd 0                ; 存放栈段选择子[0x08]    
    stack_length    dd 1                ; 用户程序编写者建议的栈大小[0x0c]
                                        ; 以4KB为单位

    ; 程序入口点(Entry Point)
    program_entry   dd beginning        ; 偏移地址[0x10]
                    ; 编译阶段确定的起始汇编地址
                    ; 当内核完成对用户程序的加载和重定位后,把该段的选择子回填到这里(仅占用低字节部分)
                    ; 和0x10处的双字一起,共同组成一个6字节的入口点,内核从这里转移控制给用户程序
                    dd section.code.start ; 汇编地址[0x14]    
    
    segment_code    dd section.code.start; [0x18]
    code_length     dd code_end          ; [0x1c]    
    
    segment_data    dd section.data.start; [0x20]
    data_length     dd data_end          ; [0x24]

    ; 所需调用的系统API
    ; 自定义规则:用户程序在头部偏移量为0x30处构造一个表格,并列出所有要用到的符号名
    ; 每个符号名的长度是256字节,不足部分用0x00填充
    ; 内核加载用户程序时,会将每一个符号名替换成相应的内存地址,即重定位
    ; 符号-地址检索表,Symbol-Address Lookup Table, SALT
    salt_itmes      dd (head_end-salt)/256; [0x28]
    
salt:                                     ; [0x2c]
    ShowString      db '@ShowString'
                    times 256-($-ShowString) db 0
    
    TerminateProgram db '@TerminateProgram'
                    times 256-($-TerminateProgram) db 0
    
    ReadDiskData    db '@ReadDiskData'
                    times 256-($-ReadDiskData) db 0
                    
head_end:


; ===============================================================================
SECTION data vstart=0                       ; 定义用户程序数据段

; 自定义的数据缓冲区
buffer  times 1024 db 0

; 提示信息,正在运行用户程序
message_usermode    db 0x0d, 0x0a, 0x0d, 0x0a
                    db '**********User program is runing**********'
                    db 0x0d,0x0a,0

; 提示信息,硬盘数据
message_diskdata    db '  Disk data:',0x0d,0x0a,0
                    
data_end:

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


; ===============================================================================
SECTION code vstart=0                       ; 定义用户程序代码段
beginning:
    mov eax, ds     ; 进入用户程序时,ds指向头部段
    mov fs, eax     ; 使fs指向头部段
    
    mov eax, [segment_stack]
    mov ss, eax     ; ss切换到用户程序自己的堆栈,并初始化esp为0    
    mov esp, 0
    
    mov eax, [segment_data]
    mov ds, eax     ; ds切换到用户程序自己的数据段    
    
    ; 调用系统API
    ; 显示提示信息,正在运行用户程序
    mov ebx, message_usermode
    call far [fs:ShowString]   
    
    ; 调用系统API
    ; 从硬盘读取一个扇区
    mov eax, diskdata_txt_lba_begin
    mov ebx, buffer
    call far [fs:ReadDiskData]
    
    ; 调用系统API
    ; 显示提示信息,硬盘数据
    mov ebx, message_diskdata
    call far [fs:ShowString]
    
    ; 调用系统API
    ; 显示前面从硬盘读取的数据
    mov ebx, buffer
    call far [fs:ShowString]

    ; 调用系统API, 返回内核   
    jmp far [fs:TerminateProgram]

code_end:

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

 

你可能感兴趣的:(汇编)