从启动扇区跳转到 loader

2020-07-03 22:36:16   最后更新: 2020-07-03 22:36:16   访问数量:109




上一篇文章中,我们详细介绍了 FAT12 文件系统的构成,并且在 linux 环境下构建了我们自己的软盘,虽然这在此前我们已经实现过了很多次

实战 FAT12 文件系统

 

本文,我们就来通过上述原理,实现软盘读取,并且加载并让我们的启动盘跳转进入到软盘上文件所写的程序

这样,我们就终于可以和长期以来伴随我们的 freedos 系统说再见了,终于可以使用原生的 bochs 调试功能了,再也不用使用蹩脚的 magic break 了

 

BIOS 中断对我们来说已经不陌生了,我们曾经使用过 10H 号中断来实现一个字符串的打印

BIOS 的 13H 号中断就是用来操作软盘的,他通过触发时寄存器的值,实现了下面两个功能

 

复位软盘

触发时,如果 ah = 0,dl 为 驱动器号,则中断将造成软驱复位

 

读取软盘

触发时,如果寄存器值如下,则读取相应数据到 es:bx 缓冲区中:

  • ah = 02h
  • al = 读取扇区数
  • ch = 柱面/磁道号
  • cl = 起始扇区号
  • dh = 磁头号
  • dl = 驱动器号
  • es:bx = 数据缓冲区

 

通过扇区号计算柱面号与磁头号

根据上一篇文章中介绍的原理,我们可以知道,对于一个 1.44M 的软盘来说,总共有两个盘面,每面 80 个磁道,每个磁道 18 个扇区,因此 2 * 18 * 80 * 512 = 1.44M

那么,如何获取柱面号、磁头号、盘片起始扇区号呢,根据上面的原理,也很简单:

柱面号 = 扇区号 / 每磁道扇区数 / 2

磁头号 = 扇区号 / 每磁道扇区数 & 1

起始扇区号 = 扇区号 % 每磁道扇区数 + 1

 

通过汇编程序读取软盘扇区

下面的函数实现了一个软盘指定数量扇区的读取:

; ----- 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中 ----- ReadSector: push bp mov bp, sp sub esp, 2 ; 开辟两个字节的堆栈区域保存要读的扇区数: byte [bp-2] mov byte [bp-2], cl push bx mov bl, [BPB_SecPerTrk] ; bl: 每磁道扇区数 div bl ; 商保存在 al 中,余数保存在 ah 中 inc ah ; 获取其实扇区号 mov cl, ah ; cl <- 起始扇区号 mov dh, al shr al, 1 ; 获取柱面号 mov ch, al ; ch <- 柱面号 and dh, 1 ; 获取磁头号 pop bx mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘) .GoOnReading: mov ah, 2 ; 读 mov al, byte [bp-2] ; 读 al 个扇区 int 13h jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止 add esp, 2 pop bp ret

 

 

通过上面的 ReadSector 函数,我们已经可以实现软盘上一个指定扇区的读取了,但是,一个文件只要大于 512 字节,就会被存储在多个扇区上,此时我们就需要去解析 FAT 扇区中存储的 FAT 项,从而循环跳转到下一个存储文件内容的扇区中,最终实现整个文件的读取

 

如上图所示,FAT 项存在一个问题,那就是他是 12 字节的,我们每次读取一个字节的话,要读取三次才能取到两个 FAT 项,因此解析起来存在一定的复杂性

下面的函数实现了指定 FAT 项的读取,结果存放在 ax 中:

GetFATEntry: push es push bx push ax ; 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT mov ax, BaseOfLoader sub ax, 0100h mov es, ax ; 判断 ax 奇偶性,赋值 bOdd 变量 pop ax mov byte [bOdd], 0 ; bOdd 变量用于存放当前是奇数次读取还是偶数次读取 mov bx, 3 mul bx ; dx:ax = ax * 3 mov bx, 2 div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数 cmp dx, 0 jz LABEL_EVEN mov byte [bOdd], 1 ; 奇数 LABEL_EVEN: ; 计算 FAT 项所在扇区号 xor dx, dx mov bx, [BPB_BytsPerSec] div bx ; dx:ax / BPB_BytsPerSec ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号) ; dx <- 余数 (FATEntry 在扇区内的偏移) push dx mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00 add ax, SectorNoOfFAT1 ; ax = FAT1 起始扇区号 + 指定读取扇区号 = FATEntry 所在的扇区号 mov cl, 2 call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个 ; 赋值结果给 ax 并矫正结果 pop dx add bx, dx mov ax, [es:bx] cmp byte [bOdd], 1 jnz LABEL_EVEN_2 shr ax, 4 LABEL_EVEN_2: and ax, 0FFFh LABEL_GET_FAT_ENRY_OK: pop bx pop es ret

 

 

我们在系列之初,就完成了一个启动盘的创建,输出了 hello world 字符串:

实现一个操作系统

 

那么,接下来我们要做的就是创建一个 loader.bin 程序,让我们的启动程序找到并加载 loader.bin,由 loader.bin 负责拉起我们后面要写的操作系统

 

loader demo

由于启动程序一旦将控制权交给 loader.bin,loader 就不再受任何限制,我们就可以自由的编写我们所需要的 loader 程序了,因此,本文我们重点在启动程序将控制权交给 loader 的过程,因此 loader 只要能显示一行字符串就可以了

org 0100h mov ax, cs mov ds, ax mov di, (80*3 + 2) * 2 ; 屏幕第 3 行, 第 2 列 mov ax, 0B800h mov gs, ax mov ah, 0Fh ; 0000: 黑底 1111: 白字 mov si, BootMessage xor cx, cx mov cl, byte [MessageLength] print_loop: lodsb mov [gs:di], ax add bl, 1 add di, 2 loop print_loop jmp $ BootMessage: db "techlog loader" MessageLength: db $-BootMessage

 

 

放入软盘

通过上一篇文章中所介绍的,我们直接通过命令将编译后的 loader.bin 放入软盘即可:

nasm -o loader.bin loader.asm

mount -o loop boot.img /mnt/floppy

cp loader.bin /mnt/floppy

umount /mnt/floppy

 

复位软驱

首先,我们要执行复位中断,复位软驱:

xor ah, ah xor dl, dl int 13h

 

 

循环读取根目录区找到元信息

接下来,我们要循环读取根目录区,找到 loader.bin 在根目录区中的元数据信息:

; 在根目录区寻找 LOADER.BIN ; wSectorNo 为根目录区扇区号,初始为 19 mov word [wSectorNo], SectorNoOfRootDirectory LABEL_SEARCH_IN_ROOT_DIR_BEGIN: ; 根目录区已读完,则说明未找到 cmp word [wRootDirSizeForLoop], 0 jz LABEL_NO_LOADERBIN dec word [wRootDirSizeForLoop] ; 读取扇区 mov ax, BaseOfLoader mov es, ax mov bx, OffsetOfLoader mov ax, [wSectorNo] mov cl, 1 call ReadSector mov si, LoaderFileName ; ds:si = "LOADER BIN" mov di, OffsetOfLoader ; es:di = BaseOfLoader:0100 cld ; df = 0 ; 循环读取目录条目 mov dx, 10h ; 当前扇区所有目录条目循环次数 LABEL_SEARCH_FOR_LOADERBIN: ; 循环结束,已完成当前扇区目录条目读取 cmp dx, 0 jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR dec dx ; 比较目录条目中 DIR_Name 是否与 LOADER.BIN 相同 mov cx, 11 LABEL_CMP_FILENAME: cmp cx, 0 jz LABEL_FILENAME_FOUND ; 如果比较了 11 个字符都相等, 表示找到 dec cx lodsb ; ds:si -> al cmp al, byte [es:di] jz LABEL_GO_ON jmp LABEL_DIFFERENT ; 字符不同,说明当前非目录条目 LABEL_GO_ON: inc di jmp LABEL_CMP_FILENAME ; 非当前条目,跳至下一条目 LABEL_DIFFERENT: and di, 0FFE0h ; 让 es:di 指向当前条目起始位置 add di, 20h ; 跳至下一条目 mov si, LoaderFileName jmp LABEL_SEARCH_FOR_LOADERBIN ; 非当前扇区,跳至下一扇区 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR: add word [wSectorNo], 1 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ; 未找到,终止流程 LABEL_NO_LOADERBIN: mov dh, 2 ; "No LOADER." call DispStr ; 显示字符串 jmp $ ; 找到 loader.bin,继续流程 LABEL_FILENAME_FOUND:

 

 

获取文件在数据区中起始扇区号

; 获取 loader.bin 对应的数据区簇号,保存在栈中 and di, 0FFE0h ; di = 当前条目起始位置 add di, 01Ah ; es:di 指向 DIR_FstClus,对应数据区簇号 mov cx, word [es:di] push cx ; 获取文件所在扇区号,保存在 cx 中 mov ax, RootDirSectors ; 根目录扇区数 add cx, ax ; 因为 BPB_SecPerClus 为 1,每簇 1 扇区 add cx, DeltaSectorNo ; 所以,文件所在扇区号 = 根目录起始扇区号 + 根目录扇区数 + 文件数据区簇号 - 2

 

 

读取文件并载入内存

现在,我们已经有了文件在数据区中的起始扇区号,通过 FAT 区中的 FAT 项,我们就可以递归获取整个文件了

; es:bx = loader.bin 将要被加载到的内存物理地址 mov ax, BaseOfLoader mov es, ax mov bx, OffsetOfLoader ; 循环读取 loader.bin mov ax, cx ; ax <- Sector 号 LABEL_GOON_LOADING_FILE: ; 打点,表示准备读取一个扇区,展示 Booting.... push ax push bx mov ah, 0Eh mov al, '.' mov bl, 0Fh int 10h pop bx pop ax mov cl, 1 call ReadSector pop ax ; 取出此 Sector 在 FAT 中的序号 call GetFATEntry ; 读取 FAT 项值 cmp ax, 0FFFh ; 判断是否完成读取 jz LABEL_FILE_LOADED push ax ; 保存 Sector 在 FAT 中的序号 ; 读取文件下一簇 mov dx, RootDirSectors add ax, dx add ax, DeltaSectorNo add bx, [BPB_BytsPerSec] jmp LABEL_GOON_LOADING_FILE LABEL_FILE_LOADED: ; 完成文件读取,并全部载入内存 mov dh, 1 ; "Ready." call DispStr ; 显示字符串

 

 

既然整个 loader.bin 已经被加载到了内存中,那么,我们只需要通过一个跳转指令,跳转到被加载到内存的起始地址,就可以完成控制权的转移,也就是开始 loader 的执行了:

jmp BaseOfLoader:OffsetOfLoader

 

 

运行我们的镜像,可以看到:

 

 

本文重点就在于我们对启动程序的修改,loader 其实并不重要,也就是说,我们可以把此前我们写过的任何程序作为 loader.bin 来启动

下面是之前中断相关文章的 demo 演示:

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

Makefile

BOOT:=boot.asm LDR:=loader.asm IDTLDR:=protect.asm BOOT_BIN:=$(subst .asm,.bin,$(BOOT)) LDR_BIN:=$(subst .asm,.bin,$(LDR)) IMG:=boot.img FLOPPY:=/mnt/floppy/ .PHONY : everything everything : $(BOOT_BIN) $(LDR_BIN) # bximg dd if=$(BOOT_BIN) of=$(IMG) bs=512 count=1 conv=notrunc sudo mount -o loop $(IMG) $(FLOPPY) sudo cp $(LDR_BIN) $(FLOPPY) -v sudo umount $(FLOPPY) clean : rm -f $(BOOT_BIN) $(LDR_BIN) $(BOOT_BIN) : $(BOOT) nasm $< -o $@ $(LDR_BIN) : $(LDR) nasm $< -o $@ idt: nasm boot.asm -o boot.bin nasm protect.asm -o loader.bin dd if=$(BOOT_BIN) of=$(IMG) bs=512 count=1 conv=notrunc sudo mount -o loop $(IMG) $(FLOPPY) sudo cp $(LDR_BIN) $(FLOPPY) -v sudo umount $(FLOPPY)

 

 

loader.asm

org 0100h mov ax, cs mov ds, ax mov di, (80*3 + 2) * 2 ; 屏幕第 3 行, 第 39 列 mov ax, 0B800h mov gs, ax mov ah, 0Fh ; 0000: 黑底 1111: 白字 mov si, BootMessage xor cx, cx mov cl, byte [MessageLength] print_loop: lodsb mov [gs:di], ax add bl, 1 add di, 2 loop print_loop jmp $ BootMessage: db "techlog loader" MessageLength: db $-BootMessage

 

 

boot.asm

org 07c00h BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长) BaseOfLoader equ 09000h ; LOADER.BIN 被加载段地址 OffsetOfLoader equ 0100h ; LOADER.BIN 被加载偏移地址 RootDirSectors equ 14 ; 根目录区扇区数 SectorNoOfRootDirectory equ 19 ; Root Directory 的第一个扇区号 SectorNoOfFAT1 equ 1 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt DeltaSectorNo equ 17 ; 用于计算文件的开始扇区号 BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2 jmp short LABEL_START ; Start to boot. nop ; jmp 语句 3 字节,nop 补足 4 字节 ; FAT12 磁盘头 BS_OEMName DB 'ForrestY' ; OEM String, 8 字节 BPB_BytsPerSec DW 512 ; 每扇区字节数 BPB_SecPerClus DB 1 ; 每簇扇区数 BPB_RsvdSecCnt DW 1 ; Boot 记录占用扇区数 BPB_NumFATs DB 2 ; FAT 表数量 BPB_RootEntCnt DW 224 ; 根目录文件数最大值 BPB_TotSec16 DW 2880 ; 逻辑扇区总数 BPB_Media DB 0xF0 ; 媒体描述符 BPB_FATSz16 DW 9 ; 每FAT扇区数 BPB_SecPerTrk DW 18 ; 每磁道扇区数 BPB_NumHeads DW 2 ; 磁头数(面数) BPB_HiddSec DD 0 ; 隐藏扇区数 BPB_TotSec32 DD 0 ; wTotalSectorCount为0时这个值记录扇区数 BS_DrvNum DB 0 ; 中断 13 的驱动器号 BS_Reserved1 DB 0 ; 未使用 BS_BootSig DB 29h ; 扩展引导标记 (29h) BS_VolID DD 0 ; 卷序列号 BS_VolLab DB 'OrangeS0.02' ; 卷标, 必须 11 个字节 BS_FileSysType DB 'FAT12 ' ; 文件系统类型, 8 字节 LABEL_START: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, BaseOfStack ; 清屏 mov ax, 0600h ; AH = 6, AL = 0h mov bx, 0700h ; 黑底白字(BL = 07h) mov cx, 0 ; 左上角: (0, 0) mov dx, 0184fh ; 右下角: (80, 50) int 10h mov dh, 0 call DispStr ; 复位软驱 xor ah, ah xor dl, dl int 13h ; 在根目录区寻找 LOADER.BIN ; wSectorNo 为根目录区扇区号,初始为 19 mov word [wSectorNo], SectorNoOfRootDirectory LABEL_SEARCH_IN_ROOT_DIR_BEGIN: ; 根目录区已读完,则说明未找到 cmp word [wRootDirSizeForLoop], 0 jz LABEL_NO_LOADERBIN dec word [wRootDirSizeForLoop] ; 读取扇区 mov ax, BaseOfLoader mov es, ax mov bx, OffsetOfLoader mov ax, [wSectorNo] mov cl, 1 call ReadSector mov si, LoaderFileName ; ds:si = "LOADER BIN" mov di, OffsetOfLoader ; es:di = BaseOfLoader:0100 cld ; df = 0 ; 循环读取目录条目 mov dx, 10h ; 当前扇区所有目录条目循环次数 LABEL_SEARCH_FOR_LOADERBIN: ; 循环结束,已完成当前扇区目录条目读取 cmp dx, 0 jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR dec dx ; 比较目录条目中 DIR_Name 是否与 LOADER.BIN 相同 mov cx, 11 LABEL_CMP_FILENAME: cmp cx, 0 jz LABEL_FILENAME_FOUND ; 如果比较了 11 个字符都相等, 表示找到 dec cx lodsb ; ds:si -> al cmp al, byte [es:di] jz LABEL_GO_ON jmp LABEL_DIFFERENT ; 字符不同,说明当前非目录条目 LABEL_GO_ON: inc di jmp LABEL_CMP_FILENAME ; 非当前条目,跳至下一条目 LABEL_DIFFERENT: and di, 0FFE0h ; 让 es:di 指向当前条目起始位置 add di, 20h ; 跳至下一条目 mov si, LoaderFileName jmp LABEL_SEARCH_FOR_LOADERBIN ; 非当前扇区,跳至下一扇区 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR: add word [wSectorNo], 1 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ; 未找到,终止流程 LABEL_NO_LOADERBIN: mov dh, 2 ; "No LOADER." call DispStr ; 显示字符串 jmp $ ; 找到 loader.bin,继续流程 LABEL_FILENAME_FOUND: ; 获取 loader.bin 对应的数据区簇号,保存在栈中 and di, 0FFE0h ; di = 当前条目起始位置 add di, 01Ah ; es:di 指向 DIR_FstClus,对应数据区簇号 mov cx, word [es:di] push cx ; 获取文件所在扇区号,保存在 cx 中 mov ax, RootDirSectors ; 根目录扇区数 add cx, ax ; 因为 BPB_SecPerClus 为 1,每簇 1 扇区 add cx, DeltaSectorNo ; 所以,文件所在扇区号 = 根目录起始扇区号 + 根目录扇区数 + 文件数据区簇号 - 2 ; es:bx = loader.bin 将要被加载到的内存物理地址 mov ax, BaseOfLoader mov es, ax mov bx, OffsetOfLoader ; 循环读取 loader.bin mov ax, cx ; ax <- Sector 号 LABEL_GOON_LOADING_FILE: ; 打点,表示准备读取一个扇区,展示 Booting.... push ax push bx mov ah, 0Eh mov al, '.' mov bl, 0Fh int 10h pop bx pop ax mov cl, 1 call ReadSector pop ax ; 取出此 Sector 在 FAT 中的序号 call GetFATEntry ; 读取 FAT 项值 cmp ax, 0FFFh ; 判断是否完成读取 jz LABEL_FILE_LOADED push ax ; 保存 Sector 在 FAT 中的序号 ; 读取文件下一簇 mov dx, RootDirSectors add ax, dx add ax, DeltaSectorNo add bx, [BPB_BytsPerSec] jmp LABEL_GOON_LOADING_FILE LABEL_FILE_LOADED: ; 完成文件读取,并全部载入内存 mov dh, 1 ; "Ready." call DispStr ; 显示字符串 jmp BaseOfLoader:OffsetOfLoader ; 跳转到已加载到内 ; -------------------------- 变量 -------------------------------- wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数, 在循环中会递减至零. wSectorNo dw 0 ; 要读取的扇区号 bOdd db 0 ; 奇数还是偶数 LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 之文件名 ; 为简化代码, 下面每个字符串的长度均为 MessageLength MessageLength equ 9 BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0 Message1 db "Ready. " ; 9字节, 不够则用空格补齐. 序号 1 Message2 db "No LOADER" ; 9字节, 不够则用空格补齐. 序号 2 ; ---- 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based) ---- DispStr: mov ax, MessageLength mul dh add ax, BootMessage mov bp, ax ; ┓ mov ax, ds ; ┣ ES:BP = 串地址 mov es, ax ; ┛ mov cx, MessageLength ; CX = 串长度 mov ax, 01301h ; AH = 13, AL = 01h mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) mov dl, 0 int 10h ; int 10h ret ; ----- 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中 ----- ReadSector: push bp mov bp, sp sub esp, 2 ; 开辟两个字节的堆栈区域保存要读的扇区数: byte [bp-2] mov byte [bp-2], cl push bx mov bl, [BPB_SecPerTrk] ; bl: 每磁道扇区数 div bl ; 商保存在 al 中,余数保存在 ah 中 inc ah ; 获取其实扇区号 mov cl, ah ; cl <- 起始扇区号 mov dh, al shr al, 1 ; 获取柱面号 mov ch, al ; ch <- 柱面号 and dh, 1 ; 获取磁头号 pop bx mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘) .GoOnReading: mov ah, 2 ; 读 mov al, byte [bp-2] ; 读 al 个扇区 int 13h jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止 add esp, 2 pop bp ret ; ---- 读取序号为 ax 的 Sector 在 FAT 中的条目, 放在 ax 中 ---- GetFATEntry: push es push bx push ax ; 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT mov ax, BaseOfLoader sub ax, 0100h mov es, ax ; 判断 ax 奇偶性,赋值 bOdd 变量 pop ax mov byte [bOdd], 0 ; bOdd 变量用于存放当前是奇数次读取还是偶数次读取 mov bx, 3 mul bx ; dx:ax = ax * 3 mov bx, 2 div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数 cmp dx, 0 jz LABEL_EVEN mov byte [bOdd], 1 ; 奇数 LABEL_EVEN: ; 计算 FAT 项所在扇区号 xor dx, dx mov bx, [BPB_BytsPerSec] div bx ; dx:ax / BPB_BytsPerSec ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号) ; dx <- 余数 (FATEntry 在扇区内的偏移) push dx mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00 add ax, SectorNoOfFAT1 ; ax = FAT1 起始扇区号 + 指定读取扇区号 = FATEntry 所在的扇区号 mov cl, 2 call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个 ; 赋值结果给 ax 并矫正结果 pop dx add bx, dx mov ax, [es:bx] cmp byte [bOdd], 1 jnz LABEL_EVEN_2 shr ax, 4 LABEL_EVEN_2: and ax, 0FFFh LABEL_GET_FAT_ENRY_OK: pop bx pop es ret times 510-($-$$) db 0 ; 填充剩余空间,使生成的二进制代码恰好为512字节 dw 0xaa55 ; 结束标志

 

 

bochsrc.bxrc

############################################################### # Configuration file for Bochs ############################################################### # how much memory the emulated machine will have megs: 32 # filename of ROM images romimage: file=$BXSHARE/BIOS-bochs-latest vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest vga: extension=vbe, update_freq=15 magic_break: enabled=1 # what disk images will be used floppya: 1_44=boot.img, status=inserted # choose the boot disk. boot: a # log: stdout.log # enable key mapping, using US layout as default. keyboard: keymap=$BXSHARE/keymaps/x11-pc-us.map mouse: enabled=1 mouse: type=imps2, enabled=1 mouse: type=serial, enabled=1 mouse: enabled=0, toggle=ctrl+f10 magic_break: enabled=1

 

 

实现一个操作系统






操作系统      os      system      nasm      asm      汇编      oranges      loader      fat12      image     


京ICP备15018585号