编辑
2023-05-09
Misc
00
请注意,本文编写于 561 天前,最后修改于 557 天前,其中某些信息可能已经过时。

目录

主引导扇区
主引导扇区(输出字符)
主引导扇区结构
主引导扇区的功能
内存相关部署
主引导扇区优化
集成输出功能
优雅的启动
硬盘操作
扇区
IDE / ATA PIO Mode
硬盘读写模式
硬盘端口
硬盘读
硬盘写
内核加载器
Makefile优化
内核加载器实现
内存检测
实模式与保护模式
全局描述符
全局描述符表
段选择子
A20线
保护模式
保护模式的实现
进入内核
整理项目目录
内核相关实现

嘿嘿 OS 嘿嘿🤤

其实说好听叫写操作系统 其实就是抄啦
参考项目: https://github.com/StevenBaby/onix/

主引导扇区


asm
; 无限循环 jmp $ ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa

编译使用nasm即可

shell
nasm -f bin boot.asm -o boot.bin

然后使用qemu启动

qemu-system-x86_64 boot.bin

主引导扇区(输出字符)


asm
; BIOS要求主引导扇区的结尾必须是0x55 0xaa(主引导扇区大小为512) mov ah,0xe ; 显示字符的系统调用 mov al,'h' int 10h mov al,'e' int 10h mov al,'l' int 10h mov al,'l' int 10h mov al,'o' int 10h times 510-($-$$) db 0 dw 0xaa55

主引导扇区结构


  • 代码: 446B
  • 硬盘分区表: 64B=4*16B
  • MagicNum: 0xaa55

主引导扇区的功能


读取内核加载器并执行

内存相关部署


其实我们的主引导程序在加载时会自动加载到0x7c00的位置 这里可以做个实验验证一下

asm
; BIOS要求主引导扇区的结尾必须是0x55 0xaa(主引导扇区大小为512) test_word: db 'a' mov ah,0xe mov bx,test_word add bx,0x7c00 mov al,[bx] int 10h times 510-($-$$) db 0 dw 0xaa55

可以看到 通过0x7c00+$test_word成功索引到了定义的变量

但是每次如果想要输出都需要通过0x7c00进行索引显得太过笨拙了 所以可以设定一个固定的内存偏移 来方便输出

asm
[org 0x7c00] test_word: db 'a' mov ah,0xe mov bx,test_word add bx,0x7c00 mov al,[bx] int 10h mov al,[test_word] int 10h times 510-($-$$) db 0 dw 0xaa55

由于在第一次输出中 针对test_word相当于加了两次0x7c00所以输出结果会不正确 而第二次输出定义了全局偏移 便可以直接打印变量了

主引导扇区优化


asm
; 标记程序在内存中的位置 [org 0x7c00] ; 设置屏幕模式为文本模式 清除屏幕 mov ax,3 int 0x10 ; 初始化寄存器 mov ax,0 mov ds,ax mov es,ax mov ss,ax mov sp,0x7c00 ; 无限循环 jmp $ ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa

集成输出功能


每次都是每个字符单独系统调用显得太不优雅了 干脆直接将输出写成函数 以后有需要可以直接调用

asm
mov si,test_string call print print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret test_string: db "Booting...",10,13,0 ; \n\r

优雅的启动


每次手动编译也不是很优雅 直接写一个Makefile好了

makefile
boot.bin: boot.asm nasm -f bin boot.asm -o boot.bin master.img: qemu-img create -f raw master.img 16M dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc .PHONY: clean clean: rm -rf *.bin rm -rf *.img qemu: master.img qemu-system-i386 -m 32M -boot c -hda $< -fda boot.bin

硬盘操作


首先需要先创建一个硬盘出来给qemu使用

shell
qemu-img create -f raw master.img 16M

这样便可以创建一个 格式为raw 大小为16M的磁盘

当启动时需要附加磁盘时 可以执行如下命令来启动

shell
qemu-system-x86_64 -hda master.img boot.bin

扇区


  • 扇区: 是硬盘读写的基本单位 最小1个扇区 最多256个扇区
  • 机械臂的寻道时间是磁盘性能的主要瓶颈
  • 一般情况下 一个磁道有63个扇区
  • 磁道从外侧开始计数

IDE / ATA PIO Mode


  • PIO(Port Input Output) 端口输出模式
  • 端口是外部设备内部的寄存器
  • IDE(Intergrated Drive Electronics) 集成电子驱动器
  • ATA(Advanced Technology Attachment)

硬盘读写模式


  • CHS模式(Cylinder/Head/Sector): 通过柱面 磁道 扇区来定位要操作地址
  • LBA模式(Logical Block Address): 逻辑分块模式 通过分块来定位操作地址

硬盘端口


  • 0x1f0: 16bit端口 用来读取数据
  • 0x1f1: 检测前一个指令的错误
  • 0x1f2: 读写扇区的数量 最大为256
  • 0x1f3: 起始扇区的0~7位
  • 0x1f4: 起始扇区的8~15位
  • 0x1f5: 起始扇区的16~23位
  • 0x1f6
    • 0~3位: 起始扇区的24~27位
    • 4: 0为主盘 1为从盘
    • 6: 0为CHS模式 1为LBA模式
    • 5,7: 固定为1
  • 0x1f7 - out
    • 0xec: 识别硬盘
    • 0x20: 读硬盘
    • 0x30: 写硬盘
  • 0x1f7 - in
    • 0: ERR 错误
    • 3: DRQ 数据准备完毕
    • 7: BSY 硬盘繁忙

硬盘读


asm
mov edi,0x1000; 数据写入的目标内存 mov ecx,0; 起始扇区 mov bl,1; 读取的扇区数量 call read_disk read_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x20 out dx,al xor ecx,ecx mov cl,bl .read: push cx; 保存现场 call .waits; 等待数据准备完毕 call .reads pop cx loop .read ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_1000 cmp al,0b0000_1000 jnz .check ret .reads: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .readw: in ax,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .readw

硬盘写


asm
write_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x30 out dx,al xor ecx,ecx mov cl,bl .write: push cx; 保存现场 call .writes call .waits; 等待硬盘繁忙结束 pop cx loop .write ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_0000 cmp al,0b0000_0000 jnz .check ret .writes: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .writew: mov ax,[edi] out dx,ax jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .writew

在执行之后可以看到 硬盘中0x400的位置会有数据的拷贝

内核加载器


到了这一步 我们的启动完整流程就是

  • 编译loader
  • 将loader写入磁盘
  • 从磁盘中读出loader
  • 跳转到loader执行

Makefile优化


makefile
%.bin: %.asm nasm -f bin $< -o $@ master.img: boot.bin loader.bin qemu-img create -f raw master.img 16M dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc dd if=loader.bin of=master.img bs=512 count=4 seek=2 conv=notrunc .PHONY: clean clean: rm -rf *.bin rm -rf *.img qemu: master.img qemu-system-i386 -m 32M -boot c -hda $< -fda boot.bin

内核加载器实现


目前我们也没有什么必须要实现的内核功能 所以对于现在的内核加载器 只要能够正常调用即可 要求不必太高

asm
; loader.asm [org 0x1000] dw 0x55aa; 用于判断错误 mov si,loading call print jmp $ print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret loading: db "Loading...",10,13,0 ; \n\r

相对应的 在主引导扇区的汇编中也需要做些许的调整 主要就是调用loader内部的代码 并且对其载入情况进行判别 如果载入失败直接退出

asm
; 标记程序在内存中的位置 [org 0x7c00] ; 设置屏幕模式为文本模式 清除屏幕 mov ax,3 int 0x10 ; 初始化寄存器 mov ax,0 mov ds,ax mov es,ax mov ss,ax mov sp,0x7c00 mov edi,0x1000; 数据写入的目标内存 mov ecx,2; 起始扇区 mov bl,4; 读取的扇区数量 call read_disk mov si,boot_string call print cmp word [0x1000],0x55aa jnz error jmp 0:0x1002 ; 无限循环 jmp $ read_disk: ; 设置读扇区的数量 mov dx,0x1f2 mov al,bl out dx,al ... write_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al ... print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret boot_string: db "Booting...",10,13,0 ; \n\r error: mov si,.msg call print hlt jmp $ .msg db "Booting Error!!!",10,13,0 ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa

内存检测


针对内存加载器 其中很重要的一个功能就是内存检测 因为操作系统需要识别当前内存的状态 以及当前的内存是否适配当前的操作系统 内存是否可用等等问题 所以需要进行内存检测

其中BIOS的0x15系统调用 功能号0xe820就可以实现相关的功能

  • 返回值为一个名为ARDS(Address Range Descriptor Structure)的数据结构

    • 0~3位: 基地址的低32位
    • 4~7位: 基地址的高32位
    • 8~11位: 内存长度的低32位 以字节为单位
    • 12~15位: 内存长度的高32位 以字节为单位
    • 16位: 本段内存的类型
      • 1: AddressRangeMemory 本段内存可以被操作系统使用
      • 2: AddressRangeReserved 内存使用中或被操作系统保留 操作系统不可使用
      • 其他: 未定义 操作系统不可使用
  • 调用参数

    • EAX: 子功能号 使用内存检测需要设置为0xe820
    • EBX: 内存可能存在多段 内存信息需要多次返回 每次只返回一段 EBX记录下一个待返回的ARDS结构 BIOS会自动更新EBX寄存器 初始设置为0
    • ES:DI: BIOS将获取到的内存信息写入到此寄存器指向的内存
    • ECX: 用来指示BIOS写入的字节数 仅支持20B
    • EDX: 固定为签名标记0x534d4150
  • 返回值

    • CF: 为0未出错 为1出错
    • EAX: 固定为签名标记0x534d4150
    • ES:DI: 同调用参数
    • ECX: 同调用参数
    • EBX: 下一个ARDS的位置 BIOS自动更新 若返回值为0 则表示这是最后一个ARDS结构
asm
detect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print mov cx,[ards_count] mov si,0 .show: ; debug用 mov eax,[ards_buffer+si+0]; 内存基地址低32位 mov ebx,[ards_buffer+si+8]; 内存长度低32位 mov edx,[ards_buffer+si+16]; 内存类型 add si,20 loop .show ards_count: dw 0 detecting: db "Detecting Memory Success",10,13,0 ; ards_buffer要放最后 以免出现内存覆盖问题 ards_buffer:

实模式与保护模式


  • 在系统刚启动时 是8086模式(实模式) 总共有1M内存 对于任意操作都可以执行(取得系统的完整权限)
  • 因为上述的8086模式存在安全性问题 所以推出了 80286模式(保护模式)
    • 寄存器: 有一部分寄存器只能备操作系统访问
    • 高速缓存: 对程序员隐藏细节
    • 内存: 使用描述符的方式保护内存(限制一些内存只可以操作系统使用 另一些只能应用程序使用)
    • 外部设备(硬盘): 只允许操作系统访问in/out指令

全局描述符


80386的内存全局描述符如下

使用C语言的定义如下

c
typedef struct descriptor { unsigned short limit_low; // 段界限0~15位 unsigned int base_low:24; // 基地址0~23位 unsigned char type:4; //段类型 unsigned char segment:1; // 1代表代码段或数据段 0代表系统段 unsigned char DPL:2; // Descriptor Privilege Level 描述符权限等级 0~3 unsigned char present:1; // 存在位 1表示在内存中 0表示在磁盘中 unsigned char limit_high:4; // 段界限16~19 unsigned char avaliable:1; // 无用 unsigned char long_mode:1; // 64位拓展标志 unsigned char big:1; // 32位还是16位 unsigned char granularity:1; // 粒度4KB或是1B unsigned char base_high; // 基地址24~31位 }
  • 当segment=1时 type的组成为|X|C/E|R/W|A|
    • A: Accessed 是否被CPU访问过
    • X: 1为代码段 0为数据段
      • X=1时
        • C: 是否是依从代码段(执行时不需要修改特权级)
        • R: 是否可读
      • X=0时
        • E: 0表示向上生长 1表示向下生政
        • W: 是否可写

对于CPU如果需要知晓一段内存的属性的话 其实没必要使用到所有的全局描述符内的信息 当CPU知道内存的其实位置 内存的长度(界限) 内存属性 其实就能知道这段内存能否使用 是否报错了

全局描述符表


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

c
descriptor gdt[8192];
  • 下标0(第一个) 必须全为0 即NULL描述符
  • 可用的总共8191个描述符

对于全局描述符来说 其需要常驻内存 所以同样也需要一个数据结构来描述其起始位置和长度

c
typedef struct pointer { unsigned short limit; // size -1 unsigned int base; }

CPU同样也提供了一个寄存器gdtr用来存储全局描述符表的起始位置和长度 以及两个指令lgdtsgdt来对GDT表进行操作

c
lgdt [gdt_ptr]; // 加载gdt sgdt [gdt_ptr]; // 保存gdt

段选择子


在代码执行时往往需要内存 而每次对内存操作都需要CPU对内存全局描述符的访问 所以就有了段选择子

因为在保护模式下不需要段寄存器来进行地址索引 所以这里会将一个16位数加载到段寄存器中供CPU使用 数据结构如下

c
typedef struct selector { unsigned char RPL:2; // Request Privilege Level unsigned char TI:1; // 0表示全局描述符 1表示局部描述符(LDT) unsigned short index:13; // 全局描述符表索引 }

A20线


在8086时代 最大内存只有1M 索引一个地址需要段地址*0x10+偏移地址 如果计算结果大于1M则丢弃进位 让地址回归到1M以内 在80286时代 内存就有16M了 也就是24根地址线 到了80386就有了32根地址线 也就是4G内存

但是为了兼容8086 在后续的CPU上设置了一个功能 即A20线 需要打开0x92端口即可实现地址回绕

保护模式


启用保护模式只需要将cr0寄存器的第0位置为1即可

保护模式的实现

我们可以直接在检测内存完成之后直接开启保护模式

asm
detect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print jmp prepare_protected_mode prepare_protected_mode: cli; 关闭中断 ; 打开A20线 in al,0x92 or al,0b10 out 0x92,al ; 加载gdt lgdt [gdp_ptr] ;启动保护模式 mov eax,cr0 or eax,1 mov cr0,eax ; 用跳转刷新缓存启用保护模式 jmp dword code_selector:protect_mode [bits 32] protect_mode: ; 初始段化寄存器 mov ax,data_selector mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov esp,0x10000; 修改栈顶 mov byte [0xb8000],'p'; 显示p 表示已经可以直接访问1M以外的内存 jmp $ code_selector equ (1<<3); 代码段选择子 第二个描述符 data_selector equ (2<<3); 数据段选择子 第三个描述符 memory_base equ 0 ; 内存基地址 memory_limit equ ((1024*1024*1024*4) / (1024*4))-1; 内存界限 4G/4K-1 ; gdt指针 gdp_ptr: dw (gdt_end-gdt_base)-1; 界限 dd gdt_base; 基地址 ; NULL描述符 gdt_base: dd 0,0 ; 代码段描述符 gdt_code: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_1_0_1_0; 小端序 存在 特权级00 代码段 非依存 可读 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 ; 数据段描述符 gdt_data: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_0_0_1_0; 小端序 存在 特权级00 数据段 向上生长 可写 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 gdt_end:

因为 我们开启了保护模式之后 寄存器等全部都为16位寄存器了 便可以直接访问0xb8000 如果代码生效的话 应该可以将第一个字符变为p

进入内核


整理项目目录


后续就要开始Kernel相关的编写工作了 重新组织一下项目目录

  • src
    • boot
      • boot.asm
      • loader.asm
    • kernel
      • start.asm
    • Makefile
makefile
BUILD:=../BUILD SRC:=. $(BUILD)/boot/%.bin: $(SRC)/boot/%.asm $(shell mkdir -p $(dir $@)) nasm -f bin $< -o $@ $(BUILD)/master.img: $(BUILD)/boot/boot.bin $(BUILD)/boot/loader.bin qemu-img create -f raw $@ 16M dd if=$(BUILD)/boot/boot.bin of=$@ bs=512 count=1 conv=notrunc status=progress dd if=$(BUILD)/boot/loader.bin of=$@ bs=512 count=4 seek=2 conv=notrunc status=progress .PHONY: clean clean: rm -rf $(BUILD) qemu: $(BUILD)/master.img qemu-system-i386 -m 32M -boot c -hda $< -fda $(BUILD)/boot/boot.bin

内核相关实现


老样子 先简单构建一个原型 后续再填充功能

asm
[bits 32] global _start _start: mov byte [0xb8000],'K' jmp $

然后在loader.asm中调用即可

asm
[org 0x1000] dw 0x55aa; 用于判断错误 mov si,loading call print ; jmp $ call detect_memory print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret detect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print jmp prepare_protected_mode prepare_protected_mode: cli; 关闭中断 ; 打开A20线 in al,0x92 or al,0b10 out 0x92,al ; 加载gdt lgdt [gdp_ptr] ;启动保护模式 mov eax,cr0 or eax,1 mov cr0,eax ; 用跳转刷新缓存启用保护模式 jmp dword code_selector:protect_mode [bits 32] protect_mode: ; 初始段化寄存器 mov ax,data_selector mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov esp,0x10000; 修改栈顶 ; 读取内核 mov edi,0x10000 ;0xb8000 mov ecx,10 mov bl,200 call read_disk jmp dword code_selector:0x10000 ud2; 表示出错 read_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x20 out dx,al xor ecx,ecx mov cl,bl .read: push cx; 保存现场 call .waits; 等待数据准备完毕 call .reads pop cx loop .read ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_1000 cmp al,0b0000_1000 jnz .check ret .reads: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .readw: in ax,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .readw ret code_selector equ (1<<3); 代码段选择子 第二个描述符 data_selector equ (2<<3); 数据段选择子 第三个描述符 memory_base equ 0 ; 内存基地址 memory_limit equ ((1024*1024*1024*4) / (1024*4))-1; 内存界限 4G/4K-1 ; gdt指针 gdp_ptr: dw (gdt_end-gdt_base)-1; 界限 dd gdt_base; 基地址 ; NULL描述符 gdt_base: dd 0,0 ; 代码段描述符 gdt_code: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_1_0_1_0; 小端序 存在 特权级00 代码段 非依存 可读 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 ; 数据段描述符 gdt_data: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_0_0_1_0; 小端序 存在 特权级00 数据段 向上生长 可写 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 gdt_end: error: mov si,.msg call print hlt jmp $ .msg db "Loading Error!!!",10,13,0 ards_count: dw 0 loading: db "Loading...",10,13,0 ; \n\r detecting: db "Detecting Memory Success",10,13,0 ards_buffer:

这里需要特别注意read_disk的位置 放在上面是不行的 debug了半天..

本文作者:Du4t

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!