# 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切换到用户程序自己的数据段
# 运行结果
# 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: