diff --git a/2020301918-李懋良-试点班第2个报告.docx b/2020301918-李懋良-试点班第2个报告.docx deleted file mode 100644 index 3abd64c..0000000 Binary files a/2020301918-李懋良-试点班第2个报告.docx and /dev/null differ diff --git a/Makefile b/Makefile index 9fcb3a7..6d851b7 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,133 @@ -################################################## -# Makefile -################################################## +# +# make的主文件 +# 这个可是个大文件,可能乍一看人吓傻了,没事,对于makefile,可以先不去理会。 +# 只需要make,make run/monitor/gdb/clean即可。 +# 如果要研究细节别光读代码,建议用make -n先去理解每条命令在干啥,这是xv6里写的非常漂亮的地方。 +# 我们miniOS也仅仅做了部分的移植,它的功能比你想的强大。 +# 当你的源代码/头文件/kernel的编译命令发生变动时都会及时重编译。 +# 需要添加新的文件只需要在kern/Makefrag中修改KERN_SRCFILES即可 +# -BOOT:=boot.asm -LDR:=loader.asm -BOOT_BIN:=$(subst .asm,.bin,$(BOOT)) -LDR_BIN:=$(subst .asm,.bin,$(LDR)) +# 文件夹 +# OBJ用于存放编译出来的可重定位文件 +OBJDIR := obj +# INC用于存放各种头文件(*.h) +INCDIR := inc -.PHONY : everything +# 编译以及日常工具 +CC := gcc +# 汇编器 +AS := nasm +# 静态库编辑器 +AR := ar +# 链接器 +LD := ld +# 复制文件 +OBJCOPY := objcopy +# 反编译 +OBJDUMP := objdump +# 查询可重定位文件符号表 +NM := nm -everything : $(BOOT_BIN) $(LDR_BIN) - @dd if=/dev/zero of=a.img bs=512 count=2880 - @mkfs -t vfat a.img - @dd if=$(BOOT_BIN) of=a.img bs=512 count=1 conv=notrunc - @dd if=/dev/zero of=aA1.txt bs=4096 count=1 - @sudo mount -o loop a.img /mnt - @sudo cp $(LDR_BIN) /mnt -v - @sudo cp aA1.txt /mnt -v +# 预定义,-Dwdnmd相当于在C程序中`#ifdef wdnmd`为真 +DEFS := + +# gcc的相关命令参数 +# $(DEFS) 定义一些可能的参数 +# -O0 0优化,保证程序按照代码语义走而不被优化,方便调试 +# -fno-builtin 静止使用gcc内置函数,具体查手册 +CFLAGS := $(CFLAGS) $(DEFS) -O0 -fno-builtin +# -I 编译时去指定文件夹查找头文件 +# -MD 一个黑科技暂时可以不需要了解,总之是在头文件依赖变动的时候能够及时更新target +CFLAGS += -I $(INCDIR) -MD +# -fno-stack-protector 禁止栈保护(金丝雀保护机制,内核代码扛不住) +CFLAGS += -fno-stack-protector +# -std=gnu99 规定编译的语言规范为gnu99 +CFLAGS += -std=gnu99 +# -static 编译静态程序 +# -m32 编译32位程序 +CFLAGS += -static -m32 +# -g 打开gdb调试信息,能够允许gdb的时候调试 +CFLAGS += -g +# 一车的warning,在编译的时候可能会很有用 +CFLAGS += -Wall -Wno-format -Wno-unused -Werror + +# ld链接器的相关命令参数 +# -m elf_i386 链接的格式为i386 +LDFLAGS := -m elf_i386 + +# 记录每个OBJDIR里存放的每个子文件夹 +# 对于这个系统来说,最后的值为./obj/kern和./obj/boot +OBJDIRS := + +# 保证all是第一个target,这样make的时候会先执行all +# all的依赖会在kern/Makefrag中填充 +all: + +# xv6黑科技,获取编译命令,如果命令较新则会重新编译所有文件 +.PRECIOUS: $(OBJDIR)/.vars.% +$(OBJDIR)/.vars.%: FORCE + @echo "$($*)" | cmp -s $@ || echo "$($*)" > $@ +.PHONY: FORCE + +# 导入两个文件,两个文件分别编写,方便管理,也让主makefile更加清晰 +include boot/Makefrag +include kern/Makefrag + +# FAT12镜像文件 +IMAGE = $(OBJDIR)/kern/a.img + +# 最后将boot.bin, loader.bin, kernel.bin组装成a.img +$(IMAGE): $(OBJDIR)/boot/boot.bin $(OBJDIR)/boot/loader.bin $(OBJDIR)/kern/kernel.bin + @dd if=/dev/zero of=$@ bs=512 count=2880 + @mkfs.vfat $@ + @dd if=$(OBJDIR)/boot/boot.bin of=$@ bs=512 count=1 conv=notrunc + @sudo mount -o loop $@ /mnt + @sudo cp $(OBJDIR)/boot/loader.bin /mnt -v + @sudo cp $(OBJDIR)/kern/kernel.bin /mnt -v @sudo umount /mnt -clean : - @rm -f $(BOOT_BIN) $(LDR_BIN) +all: $(IMAGE) -$(BOOT_BIN) : $(BOOT) - @nasm $< -o $@ +clean: + @rm -rf $(OBJDIR) -$(LDR_BIN) : $(LDR) - @nasm $< -o $@ - - -run: +run: $(IMAGE) @qemu-system-i386 \ - -boot order=c \ - -drive file=a.img,format=raw \ + -boot order=a \ + -drive file=$(IMAGE),format=raw \ -gdb: +gdb: $(IMAGE) @qemu-system-i386 \ - -boot order=c \ - -drive file=a.img,format=raw \ - -S -s + -boot order=a \ + -drive file=$(IMAGE),format=raw \ + -s -S \ -monitor: - @gdb \ - -ex 'set tdesc filename target.xml' \ - -ex 'target remote localhost:1234' +gdb-no-graphic: $(IMAGE) + @qemu-system-i386 \ + -nographic \ + -boot order=a \ + -drive file=$(IMAGE),format=raw \ + -s -S \ +# 调试的内核代码elf +KERNBIN := $(OBJDIR)/kern/kernel.bin + +monitor: $(KERNBIN) + @gdb \ + -ex 'set confirm off' \ + -ex 'target remote localhost:1234' \ + -ex 'file $(KERNBIN)' + +# 测试%s +tests: + @make DEFS=-DTESTS --no-print-directory + +# xv6黑科技,获取头文件依赖,如果头文件更新则会重新编译所有文件 +$(OBJDIR)/.deps: $(foreach dir, $(OBJDIRS), $(wildcard $(OBJDIR)/$(dir)/*.d)) + @mkdir -p $(@D) + @perl mergedep.pl $@ $^ + +-include $(OBJDIR)/.deps + +.PHONY: all clean run gdb gdb-no-graphic monitor diff --git a/boot/Makefrag b/boot/Makefrag new file mode 100644 index 0000000..8e12f00 --- /dev/null +++ b/boot/Makefrag @@ -0,0 +1,31 @@ +# +# makefile的boot部分, +# 将会导入到根目录的makefile文件 +# + +OBJDIRS += boot + +LOADER_OBJS := $(OBJDIR)/boot/loader.o $(OBJDIR)/boot/loadkernel.o +LOADER_LINKER := boot/linker.ld + +# 根据源文件pattern判断编译的方式 +$(OBJDIR)/boot/%.o: boot/%.c + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -fno-pie -c -o $@ $< + +$(OBJDIR)/boot/%.o: boot/%.asm + @echo + as obj $< + @mkdir -p $(@D) + @$(AS) -f elf -o $@ $< + +# 对于boot,它是特殊的,需要特殊编译 +$(OBJDIR)/boot/boot.bin: boot/boot.asm + @echo + as bin $< + @mkdir -p $(@D) + @$(AS) -o $@ $< +# loader通过一个特定的脚本进行链接,反正可以先不管,将核心放到内核代码上来 +$(OBJDIR)/boot/loader.bin: $(LOADER_OBJS) $(LOADER_LINKER) + @echo + ld $@ + @mkdir -p $(@D) + @$(LD) $(LDFLAGS) -s -T $(LOADER_LINKER) --oformat binary -o $@ $(LOADER_OBJS) diff --git a/boot.asm b/boot/boot.asm similarity index 94% rename from boot.asm rename to boot/boot.asm index c3f7cd8..f88b1df 100644 --- a/boot.asm +++ b/boot/boot.asm @@ -134,13 +134,27 @@ DispDot: ; cx: 读入多少个扇区 ; (es:bx): 读入的缓冲区的起始地址 ; -; 中断调用传入的参数规范请参考本节实验指导书的实验参考LBA部分 +; 这里使用的是bios的扩展读功能,写成C语言的结构体是这样的 +; struct buffer_packet { +; short buffer_packet_size; /* struct's size(可以为 0x10 或 0x18)*/ +; short sectors; /* 读多少个 sectors */ +; char *buffer; /* buffer address */ +; long long start_sectors; /* 从哪个 sector 开始读 */ +; long long *l_buffer; /* 64 位的 buffer address 这个我们不管!!!*/ +; }; +; 需要注意的是,buffer参数的高16位填充的是段地址(es),低16位填充的是偏移量(bx) +; +; int 13h扩展读功能要求 +; ah = 0x42 +; dl = 驱动号 +; (ds:si)是一个指向buffer_packet的指针 +; 中断结束后有一个状态返回,具体请STFW & RTFM ReadSector: push bp mov bp, sp pusha - mov si, BufferPacket ; ds:si 指向的是BufferPacket的首地址 + mov si, BufferPacket mov word [si + 0], 010h ; buffer_packet_size mov word [si + 2], cx ; sectors mov word [si + 4], bx ; buffer-offset @@ -155,8 +169,7 @@ ReadSector: popa pop bp ret - -.ReadFail +.ReadFail: mov dh, 2 call DispStr jmp $ ; 如果cf位置1,就意味着读入错误,这个时候建议直接开摆 diff --git a/boot/inc/fat12.inc b/boot/inc/fat12.inc new file mode 100644 index 0000000..86e854a --- /dev/null +++ b/boot/inc/fat12.inc @@ -0,0 +1,301 @@ + ; 下面是 FAT12 磁盘的头 + ; 正常情况下,boot是要对磁盘头的数据进行解析的 + ; 但是出于简单考虑,直接将磁盘头硬编码进来,如果要分析可就太麻烦了,汇编本来就看的头大,还搞那么多未知元 + ; 里面很多信息实际上在boot里用不上,请各位别对里面的参数纠结太多,用到了再查也不迟 + 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 80h ; 中断 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个字节 + + ; 文件系统信息存放完毕后后面的内容就可以自由调整了,撒花! + +; 原先Orange的代码是按照 主函数->常量->变量->子函数的顺序给出,逻辑很乱 +; 这次尝试用C语言风格按照 常量->变量->子函数->主函数的顺序给出,更加符合正常的编程逻辑 +;============================================================================ +;常量 +;================================================================================================ + +; 这部分请看手册 +RootDirSectors equ 14 +SectorNoOfRootDirectory equ 19 +SectorNoOfFAT1 equ 1 +DeltaSectorNo equ 31 +;================================================================================================ + +;============================================================================ +;变量 +;---------------------------------------------------------------------------- +LeftRootDirSectors dw RootDirSectors ; 还未搜索的根目录扇区数 +RootDirSectorNow dw SectorNoOfRootDirectory ; 目前正在搜索的根目录扇区 + +;============================================================================ +;字符串 +;---------------------------------------------------------------------------- +KernelFileName db "KERNEL BIN", 0 ; KERNEL.BIN 的文件名(为什么中间有空格请RTFM) +; 为简化代码, 下面每个字符串的长度均为 MessageLength +MessageLength equ 9 +BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0 +Message1 db "Ready. " ; 9字节, 不够则用空格补齐. 序号 1 +Message2 db "No KERNEL" ; 9字节, 不够则用空格补齐. 序号 2 +;============================================================================ +; 汇编并不像高级语言一样规范,寄存器忘保存,调用子函数后发现值变了可太痛苦了 +; 所以为了减少这份痛苦,这里的所有函数都保证函数除了返回值寄存器其余的主要寄存器都有保护现场 +; 保证调用之后不用担心寄存器值变了 +;---------------------------------------------------------------------------- +; 函数名: Panic +;---------------------------------------------------------------------------- +; 作用: +; 当遇到一些程序不可解决恢复的事,就建议直接开摆!输出“No KERNEL”后就地死循环开摆! +Panic: + mov dh, 2 + call DispStr + jmp $ + +;---------------------------------------------------------------------------- +; 函数名: DispStr +;---------------------------------------------------------------------------- +; 作用: +; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(从0开始) +DispStr: + pusha + push es + + mov ax, MessageLength + mul dh + add ax, BootMessage + mov bp, ax + mov ax, ds + mov es, ax ; ES:BP = 串地址 + mov cx, MessageLength ; CX = 串长度 + mov ax, 01301h ; AH = 13, AL = 01h + mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) + mov dl, 0 + int 10h ; RTFM + + pop es + popa + ret + +;---------------------------------------------------------------------------- +; 函数名: DispDot +;---------------------------------------------------------------------------- +; 作用: +; 打印一个点 +DispDot: + pusha + + mov ah, 0Eh ; `. 每读一个扇区就在 "Booting " 后面 + mov al, '.' ; | 打一个点, 形成这样的效果: + mov bl, 0Fh ; | Booting ...... + int 10h ; / + + popa + ret + +;---------------------------------------------------------------------------- +; 函数名: ReadSector +;---------------------------------------------------------------------------- +; 作用: +; 将磁盘的数据读入到内存中 +; ax: 从哪个扇区开始 +; cx: 读入多少个扇区 +; (es:bx): 读入的缓冲区的起始地址 +; +; 这里使用的是bios的扩展读功能,写成C语言的结构体是这样的 +; struct buffer_packet { +; short buffer_packet_size; /* struct's size(可以为 0x10 或 0x18)*/ +; short sectors; /* 读多少个 sectors */ +; char *buffer; /* buffer address */ +; long long start_sectors; /* 从哪个 sector 开始读 */ +; long long *l_buffer; /* 64 位的 buffer address 这个我们不管!!!*/ +; } buffer; +; 需要注意的是,buffer参数的高4位填充的是段地址(es),低4位填充的是偏移量(bx) +; +; int 13h扩展读功能要求 +; ah = 0x42 +; dl = 驱动号 +; (ds:si)是一个指向buffer的指针 +; 中断结束后有一个状态返回,具体请STFW & RTFM +ReadSector: + pusha + + push dword 0 + push word 0 + push word ax ; start_sectors + push word es + push word bx ; buffer + push word cx ; sectors + push word 10h ; buffer_packet_size + + mov si, sp ; si <- 指向buffer的地址 + mov dl, [BS_DrvNum] ; 驱动号 + mov ah, 42h ; 扩展读 + int 13h + jc Panic ; 如果cf位置1,就意味着读入错误,这个时候建议直接开摆 + + add sp, 10h + + popa + ret + +;---------------------------------------------------------------------------- +; 函数名: GetNextCluster +;---------------------------------------------------------------------------- +; 作用: +; ax存放的是当前的簇(cluster)号,根据当前的簇号在fat表里查找,找到下一个簇的簇号,并将返回值存放在ax +GetNextCluster: + pusha + mov bp, sp + + mov bx, 3 ; 一个FAT项长度为1.5字节 + mul bx + mov bx, 2 ; ax = floor(clus_number * 1.5) + div bx ; 这个时候ax里面放着的是FAT项基地址相对于FAT表开头的字节偏移量 + ; 如果clus_number为奇数,则dx为1,否则为0 + push dx ; 临时保存奇数标识信息 + mov dx, 0 ; 下面除法要用到 + mov bx, [BPB_BytsPerSec] + div bx ; dx:ax / BPB_BytsPerSec + ; ax <- 商 (基地址在FAT表的第几个扇区) + ; dx <- 余数 (基地址在扇区内的偏移) + mov bx, 0 ; bx <- 0 于是, es:bx = BaseOfKernelFile:0 + add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是FAT项所在的扇区号 + mov cx, 2 ; 读取FAT项所在的扇区, 一次读两个, 避免在边界 + call ReadSector ; 发生错误, 因为一个FAT项可能跨越两个扇区 + + mov bx, dx ; 将偏移量搬回bx + mov ax, [es:bx] + pop bx ; 取回奇数标识信息 + cmp bx, 0 ; 如果是第奇数个FAT项还得右移四位,所以这也是为什么FAT12这么辣鸡的原因, + jz .EvenCluster ; 诶……徒增那么多判断,看看隔壁FAT16,FAT32,exFAT,不会发生FAT项跨扇区的情况,也不需要判断第奇偶个 + shr ax, 4 ; 可能是微软(FAT是微软创建的)第一个亲儿子的原因,有它的历史局限性 +.EvenCluster: + and ax, 0FFFh ; 读完需要与一下,因为高位是未定义的 + mov word [bp + 14], ax ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + + popa + ret + +;---------------------------------------------------------------------------- +; 函数名: StringCmp +;---------------------------------------------------------------------------- +; 作用: +; 比较 ds:si 和 es:di 处的字符串(比较长度为11,仅为kernel.bin所用) +; 如果两个字符串相等ax返回1,否则ax返回0 +StringCmp: + pusha + mov bp, sp + + mov cx, 11 ; 比较长度为11 + cld ; 清位保险一下 +.STARTCMP: + lodsb ; ds:si -> al + cmp al, byte [es:di] + jnz .DIFFERENT + inc di + dec cx + cmp cx, 0 + jz .SAME + jmp .STARTCMP +.DIFFERENT: + mov word [bp + 14], 0 ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + jmp .ENDCMP +.SAME: + mov word [bp + 14], 1 ; 下一步就是ENDCMP了,就懒得jump了 +.ENDCMP: + popa + ret +;---------------------------------------------------------------------------- +; 函数名: LoadKernelFile +;---------------------------------------------------------------------------- +; 作用: +; 将kernel的elf文件加载到BaseOfKernelFile:OffsetOfKernelFile +LoadKernelFile: + pusha + push es + + mov ax, BaseOfKernelFile + mov es, ax ; es <- BaseOfKernelFile + + ; 清屏 + mov ax, 0600h ; AH = 6, AL = 0h + mov bx, 0700h ; 黑底白字(BL = 07h) + mov cx, 0 ; 左上角: (0, 0) + mov dx, 0184fh ; 右下角: (80, 50) + int 10h ; int 10h + + mov dh, 0 ; "Booting " + call DispStr ; 显示字符串 + + mov ah, 0 ; ┓ + mov dl, [BS_DrvNum] ; ┣ 硬盘复位 + int 13h ; ┛ + +; 下面在 A 盘的根目录寻找 KERNEL.BIN +.FindKernelInRootDir: + mov ax, [RootDirSectorNow] ; ax <- 现在正在搜索的扇区号 + mov bx, OffsetOfKernelFile ; es:bx = BaseOfKernelFile:OffsetOfKernelFile + mov cx, 1 + call ReadSector + + mov si, KernelFileName ; ds:si -> "KERNEL BIN" + mov di, OffsetOfKernelFile ; es:di -> BaseOfKernelFile:400h = BaseOfKernelFile*10h+400h + mov dx, 10h ; 32(目录项大小) * 16(dx) = 512(BPB_BytsPerSec) + +.CompareFilename: + call StringCmp + cmp ax, 1 + jz .KernelFound ; ax == 1 -> 比对成了 + dec dx + cmp dx, 0 + jz .GotoNextRootDirSector ; 该扇区的所有目录项都探索完了,去探索下一个扇区 + add di, 20h ; 32 -> 目录项大小 + jmp .CompareFilename + +.GotoNextRootDirSector: + inc word [RootDirSectorNow] ; 改变正在搜索的扇区号 + dec word [LeftRootDirSectors] ; ┓ + cmp word [LeftRootDirSectors], 0 ; ┣ 判断根目录区是不是已经读完 + jz Panic ; ┛ 如果读完表示没有找到 KERNEL.BIN,就直接开摆 + jmp .FindKernelInRootDir + +.KernelFound: ; 找到 KERNEL.BIN 后便来到这里继续 + add di, 01Ah ; 0x1a = 28 这个 28 在目录项里偏移量对应的数据是起始簇号(RTFM) + mov dx, word [es:di] ; 起始簇号占2字节,读入到dx里 + mov bx, OffsetOfKernelFile ; es:bx = BaseOfKernelFile:OffsetOfKernelFile + +.LoadKernel: + call DispDot + mov ax, dx ; ax <- 数据区簇号 + add ax, DeltaSectorNo ; 数据区的簇号需要加上一个偏移量才能得到真正的扇区号 + mov cx, 1 ; 一个簇就仅有一个扇区 + call ReadSector + mov ax, dx ; ax <- 数据区簇号(在之前ax = 数据区簇号+偏移量) + call GetNextCluster ; 根据数据区簇号获取文件下一个簇的簇号 + mov dx, ax ; dx <- 下一个簇的簇号 + cmp dx, 0FFFh ; 判断是否读完了(根据文档理论上dx只要在0xFF8~0xFFF都行,但是这里直接偷懒只判断0xFFF) + jz .LoadFinished + add bx, [BPB_BytsPerSec] ; 别忘了更新bx,否则你会发现文件发生复写的情况(指来回更新BaseOfKernelFile:OffsetOfKernelFile ~ BaseOfKernelFile:OffsetOfKernelFile+0x200) + jmp .LoadKernel +.LoadFinished: + mov dh, 1 ; "Ready." + call DispStr ; 显示字符串 + + pop es + popa + ret \ No newline at end of file diff --git a/boot/inc/load.inc b/boot/inc/load.inc new file mode 100644 index 0000000..243cb86 --- /dev/null +++ b/boot/inc/load.inc @@ -0,0 +1,23 @@ + +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; load.inc +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; Forrest Yu, 2005 +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址 +OffsetOfLoader equ 0400h ; LOADER.BIN 被加载到的位置 ---- 偏移地址 +LoaderSegPhyAddr equ 90000h ; LOADER.BIN 被加载到的位置 ---- 物理地址 (= Base * 10h) + +BaseOfKernelFile equ 08000h ; KERNEL.BIN 被加载到的位置 ---- 段地址 +OffsetOfKernelFile equ 0400h ; KERNEL.BIN 被加载到的位置 ---- 偏移地址 +KernelSegPhyAddr equ 80000h ; KERNEL.BIN 被加载到的位置 ---- 偏移地址 (= Base * 10h) + +; 注意:1、必须与 MAKEFILE 中参数 -Ttext 的值相等!! +; 2、这是个地址而非仅仅是个偏移,如果 -Ttext 的值为 0x400400,则它的值也应该是 0x400400。 +KernelEntryPointPhyAddr equ 030400h + +BaseOfStack equ OffsetOfLoader + +PageDirBase equ 200000h ; 页目录开始地址: 2M +PageTblBase equ 201000h ; 页表开始地址: 2M + 4K \ No newline at end of file diff --git a/boot/inc/pm.inc b/boot/inc/pm.inc new file mode 100644 index 0000000..94e1ebf --- /dev/null +++ b/boot/inc/pm.inc @@ -0,0 +1,324 @@ + +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; pm.inc +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; Forrest Yu, 2005 +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +; 描述符图示 + +; 图示一 +; +; ------ ┏━━┳━━┓高地址 +; ┃ 7 ┃ 段 ┃ +; ┣━━┫ ┃ +; 基 +; 字节 7 ┆ ┆ ┆ +; 址 +; ┣━━┫ ② ┃ +; ┃ 0 ┃ ┃ +; ------ ┣━━╋━━┫ +; ┃ 7 ┃ G ┃ +; ┣━━╉──┨ +; ┃ 6 ┃ D ┃ +; ┣━━╉──┨ +; ┃ 5 ┃ 0 ┃ +; ┣━━╉──┨ +; ┃ 4 ┃ AVL┃ +; 字节 6 ┣━━╉──┨ +; ┃ 3 ┃ ┃ +; ┣━━┫ 段 ┃ +; ┃ 2 ┃ 界 ┃ +; ┣━━┫ 限 ┃ +; ┃ 1 ┃ ┃ +; ┣━━┫ ② ┃ +; ┃ 0 ┃ ┃ +; ------ ┣━━╋━━┫ +; ┃ 7 ┃ P ┃ +; ┣━━╉──┨ +; ┃ 6 ┃ ┃ +; ┣━━┫ DPL┃ +; ┃ 5 ┃ ┃ +; ┣━━╉──┨ +; ┃ 4 ┃ S ┃ +; 字节 5 ┣━━╉──┨ +; ┃ 3 ┃ ┃ +; ┣━━┫ T ┃ +; ┃ 2 ┃ Y ┃ +; ┣━━┫ P ┃ +; ┃ 1 ┃ E ┃ +; ┣━━┫ ┃ +; ┃ 0 ┃ ┃ +; ------ ┣━━╋━━┫ +; ┃ 23 ┃ ┃ +; ┣━━┫ ┃ +; ┃ 22 ┃ ┃ +; ┣━━┫ 段 ┃ +; +; 字节 ┆ ┆ 基 ┆ +; 2, 3, 4 +; ┣━━┫ 址 ┃ +; ┃ 1 ┃ ① ┃ +; ┣━━┫ ┃ +; ┃ 0 ┃ ┃ +; ------ ┣━━╋━━┫ +; ┃ 15 ┃ ┃ +; ┣━━┫ ┃ +; ┃ 14 ┃ ┃ +; ┣━━┫ 段 ┃ +; +; 字节 0,1┆ ┆ 界 ┆ +; +; ┣━━┫ 限 ┃ +; ┃ 1 ┃ ① ┃ +; ┣━━┫ ┃ +; ┃ 0 ┃ ┃ +; ------ ┗━━┻━━┛低地址 +; + + +; 图示二 + +; 高地址………………………………………………………………………低地址 + +; | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +; |7654321076543210765432107654321076543210765432107654321076543210| <- 共 8 字节 +; |--------========--------========--------========--------========| +; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓ +; ┃31..24┃ (见下图) ┃ 段基址(23..0) ┃ 段界限(15..0)┃ +; ┃ ┃ ┃ ┃ ┃ +; ┃ 基址2┃③│②│ ①┃基址1b│ 基址1a ┃ 段界限1 ┃ +; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫ +; ┃ %6 ┃ %5 ┃ %4 ┃ %3 ┃ %2 ┃ %1 ┃ +; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛ +; │ \_________ +; │ \__________________ +; │ \________________________________________________ +; │ \ +; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓ +; ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃ +; ┣━━╋━━╋━━╋━━╋━━┻━━┻━━┻━━╋━━╋━━┻━━╋━━╋━━┻━━┻━━┻━━┫ +; ┃ G ┃ D ┃ 0 ┃ AVL┃ 段界限 2 (19..16) ┃ P ┃ DPL ┃ S ┃ TYPE ┃ +; ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫ +; ┃ ③: 属性 2 ┃ ②: 段界限 2 ┃ ①: 属性1 ┃ +; ┗━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┛ +; 高地址 低地址 +; +; + +; 说明: +; +; (1) P: 存在(Present)位。 +; P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中; +; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。 +; +; (2) DPL: 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。 +; +; (3) S: 说明描述符的类型。 +; 对于存储段描述符而言,S=1,以区别与系统段描述符和门描述符(S=0)。 +; +; (4) TYPE: 说明存储段描述符所描述的存储段的具体属性。 +; +; +; 数据段类型 类型值 说明 +; ---------------------------------- +; 0 只读 +; 1 只读、已访问 +; 2 读/写 +; 3 读/写、已访问 +; 4 只读、向下扩展 +; 5 只读、向下扩展、已访问 +; 6 读/写、向下扩展 +; 7 读/写、向下扩展、已访问 +; +; +; 类型值 说明 +; 代码段类型 ---------------------------------- +; 8 只执行 +; 9 只执行、已访问 +; A 执行/读 +; B 执行/读、已访问 +; C 只执行、一致码段 +; D 只执行、一致码段、已访问 +; E 执行/读、一致码段 +; F 执行/读、一致码段、已访问 +; +; +; 系统段类型 类型编码 说明 +; ---------------------------------- +; 0 <未定义> +; 1 可用286TSS +; 2 LDT +; 3 忙的286TSS +; 4 286调用门 +; 5 任务门 +; 6 286中断门 +; 7 286陷阱门 +; 8 未定义 +; 9 可用386TSS +; A <未定义> +; B 忙的386TSS +; C 386调用门 +; D <未定义> +; E 386中断门 +; F 386陷阱门 +; +; (5) G: 段界限粒度(Granularity)位。 +; G=0 表示界限粒度为字节; +; G=1 表示界限粒度为4K 字节。 +; 注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。 +; +; (6) D: D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同。 +; ⑴ 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数所默认的大小。 +; ① D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段; +; ② D=0 表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。 +; ⑵ 在向下扩展数据段的描述符中,D位决定段的上部边界。 +; ① D=1表示段的上部界限为4G; +; ② D=0表示段的上部界限为64K,这是为了与80286兼容。 +; ⑶ 在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。 +; ① D=1表示使用32位堆栈指针寄存器ESP; +; ② D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。 +; +; (7) AVL: 软件可利用位。80386对该位的使用未左规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。 +; + + +;---------------------------------------------------------------------------- +; 描述符类型值说明 +; 其中: +; DA_ : Descriptor Attribute +; D : 数据段 +; C : 代码段 +; S : 系统段 +; R : 只读 +; RW : 读写 +; A : 已访问 +; 其它 : 可按照字面意思理解 +;---------------------------------------------------------------------------- +DA_32 EQU 4000h ; 32 位段 +DA_LIMIT_4K EQU 8000h ; 段界限粒度为 4K 字节 + +DA_DPL0 EQU 00h ; DPL = 0 +DA_DPL1 EQU 20h ; DPL = 1 +DA_DPL2 EQU 40h ; DPL = 2 +DA_DPL3 EQU 60h ; DPL = 3 +;---------------------------------------------------------------------------- +; 存储段描述符类型值说明 +;---------------------------------------------------------------------------- +DA_DR EQU 90h ; 存在的只读数据段类型值 +DA_DRW EQU 92h ; 存在的可读写数据段属性值 +DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值 +DA_C EQU 98h ; 存在的只执行代码段属性值 +DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值 +DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值 +DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值 +;---------------------------------------------------------------------------- +; 系统段描述符类型值说明 +;---------------------------------------------------------------------------- +DA_LDT EQU 82h ; 局部描述符表段类型值 +DA_TaskGate EQU 85h ; 任务门类型值 +DA_386TSS EQU 89h ; 可用 386 任务状态段类型值 +DA_386CGate EQU 8Ch ; 386 调用门类型值 +DA_386IGate EQU 8Eh ; 386 中断门类型值 +DA_386TGate EQU 8Fh ; 386 陷阱门类型值 +;---------------------------------------------------------------------------- + + +; 选择子图示: +; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓ +; ┃ 15 ┃ 14 ┃ 13 ┃ 12 ┃ 11 ┃ 10 ┃ 9 ┃ 8 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃ +; ┣━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━╋━━╋━━┻━━┫ +; ┃ 描述符索引 ┃ TI ┃ RPL ┃ +; ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━┻━━━━━┛ +; +; RPL(Requested Privilege Level): 请求特权级,用于特权检查。 +; +; TI(Table Indicator): 引用描述符表指示位 +; TI=0 指示从全局描述符表GDT中读取描述符; +; TI=1 指示从局部描述符表LDT中读取描述符。 +; + +;---------------------------------------------------------------------------- +; 选择子类型值说明 +; 其中: +; SA_ : Selector Attribute + +SA_RPL0 EQU 0 ; ┓ +SA_RPL1 EQU 1 ; ┣ RPL +SA_RPL2 EQU 2 ; ┃ +SA_RPL3 EQU 3 ; ┛ + +SA_TIG EQU 0 ; ┓TI +SA_TIL EQU 4 ; ┛ +;---------------------------------------------------------------------------- + + +;---------------------------------------------------------------------------- +; 分页机制使用的常量说明 +;---------------------------------------------------------------------------- +PG_P EQU 1 ; 页存在属性位 +PG_RWR EQU 0 ; R/W 属性位值, 读/执行 +PG_RWW EQU 2 ; R/W 属性位值, 读/写/执行 +PG_USS EQU 0 ; U/S 属性位值, 系统级 +PG_USU EQU 4 ; U/S 属性位值, 用户级 +;---------------------------------------------------------------------------- + + + + +; ========================================= +; FLAGS - Intel 8086 Family Flags Register +; ========================================= +; +; |11|10|F|E|D|C|B|A|9|8|7|6|5|4|3|2|1|0| +; | | | | | | | | | | | | | | | | | '--- CF……Carry Flag +; | | | | | | | | | | | | | | | | '--- 1 +; | | | | | | | | | | | | | | | '--- PF……Parity Flag +; | | | | | | | | | | | | | | '--- 0 +; | | | | | | | | | | | | | '--- AF……Auxiliary Flag +; | | | | | | | | | | | | '--- 0 +; | | | | | | | | | | | '--- ZF……Zero Flag +; | | | | | | | | | | '--- SF……Sign Flag +; | | | | | | | | | '--- TF……Trap Flag (Single Step) +; | | | | | | | | '--- IF……Interrupt Flag +; | | | | | | | '--- DF……Direction Flag +; | | | | | | '--- OF……Overflow flag +; | | | | '----- IOPL……I/O Privilege Level (286+ only) +; | | | '----- NT……Nested Task Flag (286+ only) +; | | '----- 0 +; | '----- RF……Resume Flag (386+ only) +; '------ VM……Virtual Mode Flag (386+ only) +; +; 注: see PUSHF POPF STI CLI STD CLD +; + + +; 宏 ------------------------------------------------------------------------------------------------------ +; +; 描述符 +; usage: Descriptor Base, Limit, Attr +; Base: dd +; Limit: dd (low 20 bits available) +; Attr: dw (lower 4 bits of higher byte are always 0) +%macro Descriptor 3 + dw %2 & 0FFFFh ; 段界限 1 (2 字节) + dw %1 & 0FFFFh ; 段基址 1 (2 字节) + db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节) + dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节) + db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节) +%endmacro ; 共 8 字节 +; +; 门 +; usage: Gate Selector, Offset, DCount, Attr +; Selector: dw +; Offset: dd +; DCount: db +; Attr: db +%macro Gate 4 + dw (%2 & 0FFFFh) ; 偏移 1 (2 字节) + dw %1 ; 选择子 (2 字节) + dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节) + dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节) +%endmacro ; 共 8 字节 +; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/boot/linker.ld b/boot/linker.ld new file mode 100644 index 0000000..f09c2e2 --- /dev/null +++ b/boot/linker.ld @@ -0,0 +1,30 @@ +OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") +OUTPUT_ARCH(i386) +ENTRY(_start) +BASE_ADDRESS = 0x400; +LOADER_SEG_PHYADDR = 0x90000; + +SECTIONS +{ + . = BASE_ADDRESS; + .text.s16 : { + obj/boot/loader.o(.text.s16) + } + + . = ALIGN(32); + . += LOADER_SEG_PHYADDR; + text_lma = . - LOADER_SEG_PHYADDR; + .text ALIGN(32): AT(text_lma) { + *(.text .stub .text.* .gnu.linkonce.t.* .rodata .rodata.*) + } + + . = ALIGN(32); + data_lma = . - LOADER_SEG_PHYADDR; + .data ALIGN(32): AT(data_lma) { + *(*) + } + + /DISCARD/ : { + *(.eh_frame .note.GNU-stack) + } +} diff --git a/boot/loader.asm b/boot/loader.asm new file mode 100644 index 0000000..f7230b9 --- /dev/null +++ b/boot/loader.asm @@ -0,0 +1,220 @@ + +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; loader.asm +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +; Forrest Yu, 2005 +; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +[SECTION .text.s16] +ALIGN 16 +[BITS 16] + +%include "./boot/inc/load.inc" + +global _start +_start: + jmp Main ; Start + +%include "./boot/inc/fat12.inc" +%include "./boot/inc/pm.inc" + +ALIGN 4 +; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------ +; 段基址 段界限 属性 +LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 +LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR | DA_32 | DA_LIMIT_4K ; 0 ~ 4G +LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K ; 0 ~ 4G +LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3 ; 显存首地址 + +; 准备lgdt必要的数据结构 -------------------------------------------------------------------------------------------------------------------------------------------- +GdtLen equ $ - LABEL_GDT +GdtPtr dw GdtLen - 1 ; gdt界限 + dd LoaderSegPhyAddr + LABEL_GDT ; gdt基地址 + +; GDT 选择子 ---------------------------------------------------------------------------------- +SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT +SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT +SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3 + +Main: ; <--- 从这里开始 ************* + mov ax, cs + mov ds, ax + mov es, ax + mov ss, ax + mov sp, BaseOfStack + + mov cx, 02000h + mov ah, 01h + int 10h ; 隐藏光标 + + call LoadKernelFile + +; 下面准备跳入保护模式 ------------------------------------------- + +; 加载 GDTR + lgdt [GdtPtr] + +; 关中断 + cli + +; 打开地址线A20 + in al, 92h + or al, 00000010b + out 92h, al + +; 准备切换到保护模式 + mov eax, cr0 + or eax, 1 + mov cr0, eax + +; 真正进入保护模式 + jmp dword SelectorFlatC:LABEL_PM_START + +; 从此以后的代码在保护模式下执行 ---------------------------------------------------- +; 32 位代码段. 由实模式跳入 --------------------------------------------------------- +[SECTION .text] +ALIGN 32 +[BITS 32] + +extern load_kernel + +LABEL_PM_START: + mov ax, SelectorVideo + mov gs, ax + mov ax, SelectorFlatRW + mov ds, ax + mov es, ax + mov fs, ax + mov ss, ax + mov esp, TopOfStack + + call SetupPaging; 启动分页机制,不过在这个实验大家可以不用管,就当是一个能够扩展访存范围的牛逼玩意 + jmp SelectorFlatC:load_kernel + + ;*************************************************************** + ; 内存看上去是这样的: + ; ┃ ┃ + ; ┃ . ┃ + ; ┃ . ┃ + ; ┃ . ┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; ┃■■■■■■Page Tables■■■■■■┃ + ; ┃■■■■■(大小由LOADER决定)■■■■┃ + ; 00101000h ┃■■■■■■■■■■■■■■■■■■┃ PageTblBase + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; 00100000h ┃■■■■Page Directory Table■■■■┃ PageDirBase <- 1M + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ + ; F0000h ┃□□□□□□□System ROM□□□□□□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ + ; E0000h ┃□□□□Expansion of system ROM □□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ + ; C0000h ┃□□□Reserved for ROM expansion□□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ B8000h ← gs + ; A0000h ┃□□□Display adapter reserved□□□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ + ; 9FC00h ┃□□extended BIOS data area (EBDA)□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; 90000h ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; 80000h ┃■■■■■■■KERNEL.BIN■■■■■■┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; 30000h ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr) + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃ ┃ + ; 7E00h ┃ F R E E ┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■■■■■■■■■■■┃ + ; 7C00h ┃■■■■■■BOOT SECTOR■■■■■■┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃ ┃ + ; 500h ┃ F R E E ┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□□□□□□□□□□□□□□□┃ + ; 400h ┃□□□□ROM BIOS parameter area □□┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇┃ + ; 0h ┃◇◇◇◇◇◇Int Vectors◇◇◇◇◇◇┃ + ; ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss + ; + ; + ; ┏━━━┓ ┏━━━┓ + ; ┃■■■┃ 我们使用 ┃□□□┃ 不能使用的内存 + ; ┗━━━┛ ┗━━━┛ + ; ┏━━━┓ ┏━━━┓ + ; ┃ ┃ 未使用空间 ┃◇◇◇┃ 可以覆盖的内存 + ; ┗━━━┛ ┗━━━┛ + ; + ; 注:KERNEL 的位置实际上是很灵活的,可以通过同时改变 LOAD.INC 中的 KernelEntryPointPhyAddr 和 MAKEFILE 中参数 -Ttext 的值来改变。 + ; 比如,如果把 KernelEntryPointPhyAddr 和 -Ttext 的值都改为 0x400400,则 KERNEL 就会被加载到内存 0x400000(4M) 处,入口在 0x400400。 + ; + +; 启动分页机制 -------------------------------------------------------------- +SetupPaging: + ; 根据内存大小计算应初始化多少PDE以及多少页表 + xor edx, edx + mov eax, 8000000h ; qemu虚拟机默认内存128MB,这里简化实现省去了实模式代码里中断向BIOS询问可用内存大小的步骤 + mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小 + div ebx + mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数 + test edx, edx + jz .no_remainder + inc ecx ; 如果余数不为 0 就需增加一个页表 +.no_remainder: + push ecx ; 暂存页表个数 + + ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞. + + ; 首先初始化页目录 + mov ax, SelectorFlatRW + mov es, ax + mov edi, PageDirBase ; 此段首地址为 PageDirBase + xor eax, eax + mov eax, PageTblBase | PG_P | PG_USU | PG_RWW +.1: + stosd + add eax, 4096 ; 为了简化, 所有页表在内存中是连续的. + loop .1 + + ; 再初始化所有页表 + pop eax ; 页表个数 + mov ebx, 1024 ; 每个页表 1024 个 PTE + mul ebx + mov ecx, eax ; PTE个数 = 页表个数 * 1024 + mov edi, PageTblBase ; 此段首地址为 PageTblBase + xor eax, eax + mov eax, PG_P | PG_USU | PG_RWW +.2: + stosd + add eax, 4096 ; 每一页指向 4K 的空间 + loop .2 + + mov eax, PageDirBase + mov cr3, eax + mov eax, cr0 + or eax, 80000000h + mov cr0, eax + jmp short .3 +.3: + nop + + ret +; 分页机制启动完毕 ---------------------------------------------------------- + + +; SECTION .data 之开始 --------------------------------------------------------------------------------------------- +[SECTION .data] +ALIGN 32 +[BITS 32] +; 堆栈就在数据段的末尾 +StackSpace: times 1000h db 0 +TopOfStack equ $ ; 栈顶 diff --git a/boot/loadkernel.c b/boot/loadkernel.c new file mode 100644 index 0000000..86c9145 --- /dev/null +++ b/boot/loadkernel.c @@ -0,0 +1,164 @@ +/* + * 类型 + */ +typedef int i32; +typedef unsigned int u32; +typedef short i16; +typedef unsigned short u16; +typedef char i8; +typedef unsigned char u8; + +// 通常描述一个对象的大小,会根据机器的型号变化类型 +typedef u32 size_t; +// elf文件格式会用 +typedef u32 Elf32_Addr; +typedef u16 Elf32_Half; +typedef u32 Elf32_Off; +typedef i32 Elf32_Sword; +typedef u32 Elf32_Word; + +/* + * elf相关 + */ +#define KERNEL_ELF 0x80400 + +#define EI_NIDENT 16 +typedef struct elf32_hdr { + unsigned char e_ident[EI_NIDENT]; + Elf32_Half e_type; + Elf32_Half e_machine; + Elf32_Word e_version; + Elf32_Addr e_entry; /* Entry point */ + Elf32_Off e_phoff; + Elf32_Off e_shoff; + Elf32_Word e_flags; + Elf32_Half e_ehsize; + Elf32_Half e_phentsize; + Elf32_Half e_phnum; + Elf32_Half e_shentsize; + Elf32_Half e_shnum; + Elf32_Half e_shstrndx; +} Elf32_Ehdr; + +#define PT_NULL 0 +#define PT_LOAD 1 +#define PT_DYNAMIC 2 +#define PT_INTERP 3 +#define PT_NOTE 4 +#define PT_SHLIB 5 +#define PT_PHDR 6 + +typedef struct elf32_phdr { + Elf32_Word p_type; + Elf32_Off p_offset; + Elf32_Addr p_vaddr; + Elf32_Addr p_paddr; + Elf32_Word p_filesz; + Elf32_Word p_memsz; + Elf32_Word p_flags; + Elf32_Word p_align; +} Elf32_Phdr; + +/* + * 显示相关 + */ +#define TERMINAL_COLUMN 80 +#define TERMINAL_ROW 25 + +#define TERMINAL_POS(row, column) ((u16)(row) * TERMINAL_COLUMN + (column)) +/* + * 终端默认色,黑底白字 + */ +#define DEFAULT_COLOR 0x0f00 + +/* + * 这个函数将content数据(2字节)写到终端第disp_pos个字符 + * 第0个字符在0行0列,第1个字符在0行1列,第80个字符在1行0列,以此类推 + * 用的是内联汇编,等价于mov word [gs:disp_pos * 2], content + */ +inline static void +write_to_terminal(u16 disp_pos, u16 content) +{ + asm( + "mov %1, %%gs:(%0)" ::"r"(disp_pos * 2), "r"(content) + : "memory"); +} + +/* + * 清屏 + */ +static void +clear_screen() +{ + for (int i = 0; i < TERMINAL_ROW; i++) + for (int j = 0; j < TERMINAL_COLUMN; j++) + write_to_terminal(TERMINAL_POS(i, j), + DEFAULT_COLOR | ' '); +} + +static void * +memset(void *v, int c, size_t n) +{ + char *p; + int m; + + p = v; + m = n; + while (--m >= 0) + *p++ = c; + + return v; +} + +static void * +memcpy(void *dst, const void *src, size_t n) +{ + const char *s; + char *d; + + s = src; + d = dst; + + if (s < d && s + n > d) { + s += n; + d += n; + while (n-- > 0) + *--d = *--s; + } else { + while (n-- > 0) + *d++ = *s++; + } + + return dst; +} + +/* + * 初始化函数,加载kernel.bin的elf文件并跳过去。 + */ +void +load_kernel() +{ + clear_screen(); + for (char *s = "----start loading kernel elf----", *st = s; *s; s++) + write_to_terminal(s - st, DEFAULT_COLOR | *s); + + Elf32_Ehdr *kernel_ehdr = (Elf32_Ehdr *)KERNEL_ELF; + + Elf32_Phdr *kernel_phdr = (void *)kernel_ehdr + kernel_ehdr->e_phoff; + for (u32 i = 0; i < kernel_ehdr->e_phnum; i++, kernel_phdr++) + { + if (kernel_phdr->p_type != PT_LOAD) + continue; + // 将elf的文件数据复制到指定位置 + memcpy( + (void *)kernel_phdr->p_vaddr, + (void *)kernel_ehdr + kernel_phdr->p_offset, + kernel_phdr->p_filesz); + // 将后面的字节清零(p_memsz >= p_filesz) + memset( + (void *)kernel_phdr->p_vaddr + kernel_phdr->p_filesz, + 0, + kernel_phdr->p_memsz - kernel_phdr->p_filesz); + } + ((void (*)(void))(kernel_ehdr->e_entry))(); +} \ No newline at end of file diff --git a/i386-32bit.xml b/i386-32bit.xml deleted file mode 100644 index 872fcea..0000000 --- a/i386-32bit.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/inc/cmatrix.h b/inc/cmatrix.h new file mode 100644 index 0000000..14b559b --- /dev/null +++ b/inc/cmatrix.h @@ -0,0 +1,14 @@ +#ifndef MINIOS_CMATRIX_H +#define MINIOS_CMATRIX_H + +#include "type.h" +struct color_char { + u8 foreground;//输出字符的前景色(0~15) + u8 background;//输出字符的背景色(0~15) + u8 print_char;//输出字符的ASCII码(0~127) + u16 print_pos;//输出字符在终端中的位置 +}; + +void cmatrix_start(); + +#endif /* MINIOS_CMATRIX_H */ \ No newline at end of file diff --git a/inc/protect.h b/inc/protect.h new file mode 100644 index 0000000..df1c734 --- /dev/null +++ b/inc/protect.h @@ -0,0 +1,19 @@ +#ifndef MINIOS_PROTECT_H +#define MINIOS_PROTECT_H + +#include "type.h" + +/* 存储段描述符/系统段描述符 */ +typedef struct s_descriptor { /* 共 8 个字节 */ + u16 limit_low; /* Limit */ + u16 base_low; /* Base */ + u8 base_mid; /* Base */ + u8 attr1; /* P(1) DPL(2) DT(1) TYPE(4) */ + u8 limit_high_attr2; /* G(1) D(1) 0(1) AVL(1) LimitHigh(4) */ + u8 base_high; /* Base */ +}DESCRIPTOR; + +/* GDT 和 IDT 中描述符的个数 */ +#define GDT_SIZE 128 + +#endif /* MINIOS_PROTECT_H */ diff --git a/inc/string.h b/inc/string.h new file mode 100644 index 0000000..3e71713 --- /dev/null +++ b/inc/string.h @@ -0,0 +1,9 @@ +#ifndef MINIOS_STRING_H +#define MINIOS_STRING_H + +#include "type.h" + +void * memset(void *v, int c, size_t n); +void * memcpy(void *dst, const void *src, size_t n); + +#endif /* MINIOS_STRING_H */ \ No newline at end of file diff --git a/inc/terminal.h b/inc/terminal.h new file mode 100644 index 0000000..57a9abd --- /dev/null +++ b/inc/terminal.h @@ -0,0 +1,43 @@ +#ifndef MINIOS_TERMINAL_H +#define MINIOS_TERMINAL_H + +#include "type.h" + +/* + * 终端大小参数 + */ +#define TERMINAL_COLUMN 80 +#define TERMINAL_ROW 25 + +#define TERMINAL_POS(row, column) ((u16)(row) * TERMINAL_COLUMN + (column)) +/* + * 颜色码 + */ +#define BLACK 0x0 +#define BLUE 0x1 +#define GREEN 0x2 +#define CYAN 0x3 +#define RED 0x4 +#define FUCHUSIA 0x5 +#define BROWN 0x6 +#define SILVER 0x7 +#define GRAY 0x8 +#define LIGHT_BLUE 0x9 +#define LIGHT_GREEN 0xa +#define LIGHT_CYAN 0xb +#define LIGHT_RED 0xc +#define LIGHT_FUCHSIA 0xd +#define YELLOW 0xe +#define WHITE 0xf + +#define FOREGROUND(color) ((u16)(color & 0xf) << 8) +#define BACKGROUND(color) ((u16)(color & 0xf) << 12) +/* + * 终端默认色,黑底白字 + */ +#define DEFAULT_COLOR FOREGROUND(WHITE) | BACKGROUND(BLACK) + +void clear_screen(); +void kprintf(u16 disp_pos, const char* format, ...); + +#endif /* MINIOS_TERMINAL_H */ \ No newline at end of file diff --git a/inc/type.h b/inc/type.h new file mode 100644 index 0000000..dd5b060 --- /dev/null +++ b/inc/type.h @@ -0,0 +1,53 @@ +#ifndef MINIOS_TYPE_H +#define MINIOS_TYPE_H + +#define NULL 0 + +typedef int i32; +typedef unsigned int u32; +typedef short i16; +typedef unsigned short u16; +typedef char i8; +typedef unsigned char u8; + +// 通常描述一个对象的大小,会根据机器的型号变化类型 +typedef u32 size_t; +// signed size_t 通常描述系统调用返回值,会根据机器的型号变化类型 +typedef i32 ssize_t; +// 通常描述偏移量,会根据机器的型号变化类型 +typedef i32 off_t; +// 通常描述物理地址 +typedef u32 phyaddr_t; + +#define MIN(_a, _b) \ +({ \ + typeof(_a) __a = (_a); \ + typeof(_b) __b = (_b); \ + __a <= __b ? __a : __b; \ +}) + +#define MAX(_a, _b) \ +({ \ + typeof(_a) __a = (_a); \ + typeof(_b) __b = (_b); \ + __a >= __b ? __a : __b; \ +}) + +#define ROUNDDOWN(a, n) \ +({ \ + u32 __a = (u32) (a); \ + (typeof(a)) (__a - __a % n); \ +}) + +#define ROUNDUP(a, n) \ +({ \ + u32 __n = (u32) (n); \ + (typeof(a)) (ROUNDDOWN((u32) (a) + __n - 1, __n)); \ +}) + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) + +// 求出在结构体中的某个成员的偏移量 +#define offsetof(type, member) ((size_t) (&((type*)0)->member)) + +#endif /* MINIOS_TYPE_H */ diff --git a/kern/Makefrag b/kern/Makefrag new file mode 100644 index 0000000..b2f9f01 --- /dev/null +++ b/kern/Makefrag @@ -0,0 +1,42 @@ +# +# makefile的kernel部分, +# 将会导入到根目录的makefile文件 +# + +OBJDIRS += kern + +KERN_ENTRY := 0x30400 + +# kernel的所有源码文件,如果要添加什么新文件就只需要在这里添加即可,其余makefile都不需要动 +KERN_SRCFEILS:= kern/kernel.asm \ + kern/start.c \ + kern/cmatrix.c \ + kern/kprintf.asm\ + lib/terminal.c \ + lib/string.c + +# 根据KERN_SRCFEILS获取所有需要的可重定位文件 +KERN_OBJFILES := $(patsubst %.c, $(OBJDIR)/%.o, $(KERN_SRCFEILS)) +KERN_OBJFILES := $(patsubst %.asm, $(OBJDIR)/%.o, $(KERN_OBJFILES)) +KERN_OBJFILES := $(patsubst $(OBJDIR)/lib/%, $(OBJDIR)/kern/%, $(KERN_OBJFILES)) + +# 根据源文件pattern判断编译的方式 +$(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/kern/%.o: lib/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/kern/%.o: kern/%.asm $(OBJDIR)/.vars.CFLAGS + @echo + as obj $< + @mkdir -p $(@D) + @$(AS) -f elf -o $@ $< + +# 将所有可重定位文件链接成kernel.bin +$(OBJDIR)/kern/kernel.bin: $(KERN_OBJFILES) + @echo + ld $@ + @$(LD) $(LDFLAGS) -Ttext $(KERN_ENTRY) -o $@ $^ diff --git a/kern/cmatrix.c b/kern/cmatrix.c new file mode 100644 index 0000000..90b6205 --- /dev/null +++ b/kern/cmatrix.c @@ -0,0 +1,203 @@ +#include "type.h" +#include "cmatrix.h" +#include "terminal.h" + +static size_t random_seed = 114; +static size_t +rand() { + return ( ((random_seed = random_seed * 214013L + + 2531011L) >> 16) & 0x7fff ); +} + +struct cmatrix_node { + u8 row; + u8 character; + struct cmatrix_node *pre; + struct cmatrix_node *nxt; +}; + +struct cmatrix_info { + u8 column; + u8 max_length; + u8 now_length; + struct cmatrix_node *head; + struct cmatrix_node *tail; +}; + +typedef struct cmatrix_node cm_node; +typedef struct cmatrix_info cm_info; + +#define NODE_POOL_SIZE ((TERMINAL_ROW) * (TERMINAL_COLUMN)) +static cm_node node_pool[NODE_POOL_SIZE]; + +static cm_node* +alloc_node() +{ + for (int i = 0; i < NODE_POOL_SIZE; i++) + if (node_pool[i].character == 0) + return &node_pool[i]; + return NULL; +} + +static void +free_node(cm_node *node_ptr) +{ + node_ptr -> row = 0; + node_ptr -> character = 0; + if (node_ptr -> pre != NULL) { + node_ptr -> pre -> nxt = NULL; + node_ptr -> pre = NULL; + } + if (node_ptr -> nxt != NULL) { + node_ptr -> nxt -> pre = NULL; + node_ptr -> nxt = NULL; + } +} + +#define INFO_POOL_SIZE (TERMINAL_COLUMN) +static cm_info info_pool[INFO_POOL_SIZE]; + +static const u8 char_pool[] = { + 'a','b','c','d','e','f','g','h','i','j','k','l','m', + 'n','o','p','q','r','s','t','u','v','w','x','y','z', + 'A','B','C','D','E','F','G','H','I','J','K','L','M', + 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + '!','@','#','$','^','&','*','(',')','{','}','[',']', + '|','?','/',':',';','`','~','>','<',',','.','%','-', + '_','+','=','0','1','2','3','4','5','6','7','8','9', +}; + +static void +init_info(cm_info *info_ptr) +{ + info_ptr -> max_length = rand() % (TERMINAL_ROW - 1) + 1; + info_ptr -> head = info_ptr -> tail = NULL; +} + +static void +expand_info(cm_info *info_ptr) +{ + u8 character = char_pool[rand() % ARRAY_SIZE(char_pool)]; + + if (info_ptr -> head == NULL) { + cm_node node = { + .row = 0, + .character = character, + .pre = NULL, + .nxt = NULL, + }; + cm_node *node_ptr = alloc_node(); + *node_ptr = node; + // 修改info + info_ptr -> head = node_ptr; + info_ptr -> tail = node_ptr; + info_ptr -> now_length++; + // 打印字符 +#ifdef TESTS + struct color_char black_green = { + .background = BLACK, + .foreground = GREEN, + .print_char = info_ptr -> head -> character, + .print_pos = TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column) + }; + kprintf(0, "%s", black_green); +#else + kprintf(TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column), + "%b%c", GREEN, info_ptr -> head -> character); +#endif + + } else if (info_ptr -> head -> row < TERMINAL_ROW - 1) { + cm_node node = { + .row = info_ptr -> head -> row + 1, + .character = character, + .pre = NULL, + .nxt = info_ptr -> head, + }; + cm_node *node_ptr = alloc_node(); + *node_ptr = node; + // 打印字符 +#ifdef TESTS + struct color_char black_green = { + .background = BLACK, + .foreground = GREEN, + .print_char = info_ptr -> head -> character, + .print_pos = TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column) + }; + kprintf(0, "%s", black_green); +#else + kprintf(TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column), + "%f%c", GREEN, info_ptr -> head -> character); +#endif + // 修改info + info_ptr -> head -> pre = node_ptr; + info_ptr -> head = node_ptr; + info_ptr -> now_length++; + // 打印字符 +#ifdef TESTS + struct color_char green_white = { + .background = GREEN, + .foreground = WHITE, + .print_char = info_ptr -> head -> character, + .print_pos = TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column) + }; + kprintf(0, "%s", green_white); +#else + kprintf(TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column), + "%b%c", GREEN, info_ptr -> head -> character); +#endif + } else { + // 打印字符 +#ifdef TESTS + struct color_char black_green = { + .background = BLACK, + .foreground = GREEN, + .print_char = info_ptr -> head -> character, + .print_pos = TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column) + }; + kprintf(0, "%s", black_green); +#else + kprintf(TERMINAL_POS(info_ptr -> head -> row, + info_ptr -> column), + "%f%c", GREEN, info_ptr -> head -> character); +#endif + } + + if (info_ptr -> now_length > info_ptr -> max_length + || info_ptr -> head -> row == TERMINAL_ROW - 1) { + cm_node *node_ptr = info_ptr -> tail; + // 打印字符 + kprintf(TERMINAL_POS(info_ptr -> tail -> row, info_ptr -> column), + "%c", ' '); + // 修改info + info_ptr -> tail = info_ptr -> tail -> pre; + info_ptr -> now_length--; + // 释放内存 + free_node(node_ptr); + } +} + +void +cmatrix_start() +{ + clear_screen(); + for (int i = 0; i < TERMINAL_COLUMN; i++) + info_pool[i].column = i; + while(1) { + for (int i = 0; i < TERMINAL_COLUMN; i++) { + cm_info *info_ptr = &info_pool[i]; + if (info_ptr -> now_length == 0) + init_info(info_ptr); + expand_info(info_ptr); + } + for (int i = 0; i < 1e7; i++) { + // do nothing + } + } +} \ No newline at end of file diff --git a/kern/kernel.asm b/kern/kernel.asm new file mode 100644 index 0000000..6731acb --- /dev/null +++ b/kern/kernel.asm @@ -0,0 +1,73 @@ +SELECTOR_KERNEL_CS equ 8 + +; 导入函数 +extern cstart + +; 导入全局变量 +extern gdt_ptr + +[SECTION .bss] +StackSpace resb 4096 +StackTop: ; 栈顶 + +[section .text] ; 代码在此 + +global _start ; 导出 _start + +_start: + ; 此时内存看上去是这样的(更详细的内存情况在 LOADER.ASM 中有说明): + ; ┃ ┃ + ; ┃ ... ┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■Page Tables■■■■■■┃ + ; ┃■■■■■(大小由LOADER决定)■■■■┃ PageTblBase + ; 00101000h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■Page Directory Table■■■■┃ PageDirBase = 1M + ; 00100000h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃□□□□ Hardware Reserved □□□□┃ B8000h ← gs + ; 9FC00h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp + ; 90000h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■KERNEL.BIN■■■■■■┃ + ; 80000h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr) + ; 30000h ┣━━━━━━━━━━━━━━━━━━┫ + ; ┋ ... ┋ + ; ┋ ┋ + ; 0h ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss + ; + ; + ; GDT 以及相应的描述符是这样的: + ; + ; Descriptors Selectors + ; ┏━━━━━━━━━━━━━━━━━━┓ + ; ┃ Dummy Descriptor ┃ + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃ DESC_FLAT_C (0~4G) ┃ 8h = cs + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃ DESC_FLAT_RW (0~4G) ┃ 10h = ds, es, fs, ss + ; ┣━━━━━━━━━━━━━━━━━━┫ + ; ┃ DESC_VIDEO ┃ 1Bh = gs + ; ┗━━━━━━━━━━━━━━━━━━┛ + ; + ; 注意! 在使用 C 代码的时候一定要保证 ds, es, ss 这几个段寄存器的值是一样的 + ; 因为编译器有可能编译出使用它们的代码, 而编译器默认它们是一样的. 比如串拷贝操作会用到 ds 和 es. + ; + ; + + + ; 把 esp 从 LOADER 挪到 KERNEL + mov esp, StackTop ; 堆栈在 bss 段中 + + sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr + call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT + lgdt [gdt_ptr] ; 使用新的GDT + + + jmp SELECTOR_KERNEL_CS:csinit +csinit: ; “这个跳转指令强制使用刚刚初始化的结构”——<> P90. + + push 0 + popfd ; Pop top of stack into EFLAGS + + hlt diff --git a/kern/kprintf.asm b/kern/kprintf.asm new file mode 100644 index 0000000..9ffa055 --- /dev/null +++ b/kern/kprintf.asm @@ -0,0 +1,18 @@ +[SECTION .text] + +[BITS 32] + +global kprintf +;=============================================== +; void kprintf(u16 disp_pos, const char *format, ...) +; 参数说明: +; disp_pos: 开始打印的位置,0为0行0列,1为0行1列,80位1行0列 +; format: 需要格式化输出的字符串,默认输出的字符颜色为黑底白字 +; %c: 输出下一个参数的字符信息(保证参数范围在0~127),输出完打印的位置往下移动一位 +; %b: 更改之后输出的字符的背景色(保证参数范围在0~15) +; %f: 更改之后输出的字符的前景色(保证参数范围在0~15) +; %s(提高内容): 参考inc/terminal.h,传进来的是一个结构体,结构体参数足够明确不复赘述, +; 输出是独立的,输出完打印的位置不会往下移动一位,不会影响接下来%c的输出的颜色 +; 其余字符:按照字符输出(保证字符里不会有%,\n等奇奇怪怪的字符,都是常见字符,%后面必会跟上述三个参数之一),输出完打印的位置往下移动一位 +kprintf: + jmp $ \ No newline at end of file diff --git a/kern/start.c b/kern/start.c new file mode 100644 index 0000000..89df120 --- /dev/null +++ b/kern/start.c @@ -0,0 +1,56 @@ +#include "type.h" +#include "protect.h" +#include "string.h" +#include "cmatrix.h" +#include "terminal.h" + +u8 gdt_ptr[6]; /* 0~15:Limit 16~47:Base */ +DESCRIPTOR gdt[GDT_SIZE]; + +void cstart() +{ + /* 将 LOADER 中的 GDT 复制到新的 GDT 中 */ + memcpy(&gdt, /* New GDT */ + (void *)(*((u32 *)(&gdt_ptr[2]))), /* Base of Old GDT */ + *((u16 *)(&gdt_ptr[0])) + 1 /* Limit of Old GDT */ + ); + /* gdt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。*/ + u16 *p_gdt_limit = (u16 *)(&gdt_ptr[0]); + u32 *p_gdt_base = (u32 *)(&gdt_ptr[2]); + *p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1; + *p_gdt_base = (u32)&gdt; + // 在终端的第1行依次打出N(黑底白字),W(黑底蓝字),P(白底蓝字),U(白底黑字) + kprintf(TERMINAL_POS(1, 0), + "N%fW%bP%fU", LIGHT_BLUE, WHITE, BLACK); + // 在终端的第2行依次输出白底绿紫相间的字符 + for (char *s = "even old new york once Amsterdam", + *st = s ; *s ; s += 4) { + kprintf(TERMINAL_POS(2, s - st), + "%b%f%c%f%c%f%c%f%c", + WHITE, GREEN, *(s + 0), + FUCHUSIA, *(s + 1), GREEN, + *(s + 2), FUCHUSIA, *(s + 3)); + } +#ifdef TESTS + //在终端第3,4行分别输出两句话,而且两行的格式相同 + //第3行为绿底黄字,第4行为黄底绿字 + for (char *s1 = "never gonna give you up ", *st1 = s1, + *s2 = "never gonna let you down" ; *s1 ; s1 += 2, s2 += 2) { + struct color_char c1 = { + .background = YELLOW, + .foreground = GREEN, + .print_char = *(s2 + 0), + .print_pos = TERMINAL_POS(4, s1 - st1 + 0), + }; + struct color_char c2 = { + .background = YELLOW, + .foreground = GREEN, + .print_char = *(s2 + 1), + .print_pos = TERMINAL_POS(4, s1 - st1 + 1), + }; + kprintf(TERMINAL_POS(3, s1 - st1), + "%b%f%c%s%c%s", + GREEN, YELLOW, *(s1 + 0), c1, *(s1 + 1), c2); + } +#endif +} diff --git a/lab3-finish.png b/lab3-finish.png new file mode 100644 index 0000000..db0287d Binary files /dev/null and b/lab3-finish.png differ diff --git a/lib/string.c b/lib/string.c new file mode 100644 index 0000000..d8a8233 --- /dev/null +++ b/lib/string.c @@ -0,0 +1,37 @@ +#include "string.h" + +void * +memset(void *v, int c, size_t n) +{ + char *p; + int m; + + p = v; + m = n; + while (--m >= 0) + *p++ = c; + + return v; +} + +void * +memcpy(void *dst, const void *src, size_t n) +{ + const char *s; + char *d; + + s = src; + d = dst; + + if (s < d && s + n > d) { + s += n; + d += n; + while (n-- > 0) + *--d = *--s; + } else { + while (n-- > 0) + *d++ = *s++; + } + + return dst; +} \ No newline at end of file diff --git a/lib/terminal.c b/lib/terminal.c new file mode 100644 index 0000000..3228258 --- /dev/null +++ b/lib/terminal.c @@ -0,0 +1,18 @@ +#include "terminal.h" +#include "cmatrix.h" + +inline static void +write_to_terminal(u16 disp_pos, u16 content) +{ + asm( + "mov %1, %%gs:(%0)" ::"r"(disp_pos * 2), "r"(content) + : "memory"); +} +void +clear_screen() +{ + u16 content = DEFAULT_COLOR | ' '; + for (int i = 0; i < TERMINAL_ROW; i++) + for (int j = 0; j < TERMINAL_COLUMN; j++) + write_to_terminal(TERMINAL_POS(i, j), content); +} \ No newline at end of file diff --git a/loader.asm b/loader.asm deleted file mode 100644 index 005934d..0000000 --- a/loader.asm +++ /dev/null @@ -1,395 +0,0 @@ -org 0400h - jmp MAIN -;---------------------------------------------------------------------------- -; @param bp+8: selector -; @param bp+6: string addr -; @param bp+4: length -;---------------------------------------------------------------------------- -; Display string to current cursor -DispStr: - push bp - mov bp, sp - pusha - push es - mov ax, 0300h - mov bh, 0 - int 10h - mov ax, [bp + 6] - mov cx, [bp + 4] - mov es, [bp + 8] - mov bp, ax - mov ax, 01301h ; AH = 13, AL = 01h - mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) - mov dl, 0 - int 10h - - pop es - popa - pop bp - ret -;---------------------------------------------------------------------------- -; @param bp + 4: the word to print as uint_16t -;---------------------------------------------------------------------------- -; print a word as decimal to current cursor -DispInt: - push bp - mov bp, sp - pusha - mov di, 0 - mov ax, [bp + 4] -.loop1: - mov dx, 0 - mov bx, 10 - div bx ; {dx, ax} / bx(=10) -> ax ... dx - push dx - inc di - test ax, ax - jnz .loop1 - -.loop2: - pop dx - mov ax, dx - mov ah, 0Eh - add al, '0' - mov bl, 0Fh - int 10H - dec di - test di, di - jnz .loop2 - - popa - pop bp - ret -;---------------------------------------------------------------------------- -; no param -;---------------------------------------------------------------------------- -; give you a new line -DispNewline: - push bp - mov bp, sp - pusha - mov ax, 0300h - mov bh, 0 - int 10h - add dh, 1 - mov dl, 0 - - mov ax, 0200h - int 10h - - popa - pop bp - ret - -;---------------------------------------------------------------------------- -; @param bp + 4: low byte holds the ascii to print -;---------------------------------------------------------------------------- -; putchar to current cursor pos -DispPutchar: - push bp - mov bp, sp - pusha - ; get cursor pos - mov ax, 0300h - mov bh, 0 - int 10h - ; putchar - mov ax, [bp + 4] - mov ah, 0Eh ; no need to set al, ax holds the param - mov bl, 0Fh - int 10H - - popa - pop bp - ret -;---------------------------------------------------------------------------- -; 函数名: StringCmp -;---------------------------------------------------------------------------- -; 作用: -; 比较 ds:si 和 es:di 处的字符串(比较长度为11,仅为loader.bin所用) -; 如果两个字符串相等ax返回1,否则ax返回0 -StringCmp: - push bp - mov bp, sp - pusha - - mov cx, 11 ; 比较长度为11 - cld ; 清位保险一下 -.STARTCMP: - lodsb ; ds:si -> al - cmp al, byte [es:di] - jnz .DIFFERENT - inc di - dec cx - cmp cx, 0 - jz .SAME - jmp .STARTCMP -.DIFFERENT: - mov word [bp - 2], 0 ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 - jmp .ENDCMP -.SAME: - mov word [bp - 2], 1 ; 下一步就是ENDCMP了,就懒得jump了 -.ENDCMP: - popa - pop bp - ret - -;---------------------------------------------------------------------------- -; 函数名: ReadSector -;---------------------------------------------------------------------------- -; 作用: -; 将磁盘的数据读入到内存中 -; ax: 从哪个扇区开始 -; cx: 读入多少个扇区 -; (es:bx): 读入的缓冲区的起始地址 -; -; 中断调用传入的参数规范请参考本节实验指导书的实验参考LBA部分 -ReadSector: - push bp - mov bp, sp - pusha - - mov si, BufferPacket ; ds:si 指向的是BufferPacket的首地址 - mov word [si + 0], 010h ; buffer_packet_size - mov word [si + 2], cx ; sectors - mov word [si + 4], bx ; buffer-offset - mov word [si + 6], es ; buffer-segment - mov word [si + 8], ax ; start_sectors - - mov dl, BS_DrvNum ; 驱动号 - mov ah, 42h ; 扩展读 - int 13h - jc .ReadFail ; 读取失败,简单考虑就默认bios坏了 - - popa - pop bp - ret -.ReadFail: - jmp $ ; 如果cf位置1,就意味着读入错误,这个时候建议直接开摆 - -;------------------------------------------------------------------------------ -; 函数名: GetNextCluster -; data => BaseOfSectorBuf : 0 -;------------------------------------------------------------------------------ -; 作用: -; ax存放的是当前的簇(cluster)号,根据当前的簇号在fat表里查找,找到下一个簇的簇号,并将返回值存放在ax -GetNextCluster: - push bp - mov bp, sp - pusha - - mov bx, 3 ; 一个FAT项长度为1.5字节 - mul bx - mov bx, 2 ; ax = floor(clus_number * 1.5) - div bx ; 这个时候ax里面放着的是FAT项基地址相对于FAT表开头的字节偏移量 - ; 如果clus_number为奇数,则dx为1,否则为0 - push dx ; 临时保存奇数标识信息 - mov dx, 0 ; 下面除法要用到 - mov bx, BPB_BytsPerSec - div bx ; dx:ax / BPB_BytsPerSec - ; ax <- 商 (基地址在FAT表的第几个扇区) - ; dx <- 余数 (基地址在扇区内的偏移) - mov bx, 0 ; bx <- 0 于是, es:bx = BaseOfSectorBuf:0 - add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是FAT项所在的扇区号 - mov cx, 2 ; 读取FAT项所在的扇区, 一次读两个, 避免在边界 - call ReadSector ; 发生错误, 因为一个FAT项可能跨越两个扇区 - - mov bx, dx ; 将偏移量搬回bx - mov ax, [es:bx] - pop bx ; 取回奇数标识信息 - cmp bx, 0 ; 如果是第奇数个FAT项还得右移四位 - jz .EvenCluster ; 可能是微软(FAT是微软创建的)第一个亲儿子的原因,有它的历史局限性 - shr ax, 4 ; 当时的磁盘很脆弱,经常容易写坏,所以需要两张FAT表备份,而且人们能够制作的存储设备的容量很小 -.EvenCluster: - and ax, 0FFFh ; 读完需要与一下,因为高位是未定义的,防止ax值有误 - mov word [bp - 2], ax ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 - - popa - pop bp - ret - -;------------------------------------------------------------------------------ -; @param bp + 4: Filename string addr -; @retval ax: first cluster no -;------------------------------------------------------------------------------ -; Find File by name, return first cluster no. -; data => BaseOfSectorBuf:OffsetOfSectorBuf -FuncFindFile: - push bp - mov bp, sp - pusha - - mov word [RootDirSectorNow], SectorNoOfRootDirectory - mov word [LeftRootDirSectors], RootDirSectors -.FindLoaderInRootDir: - mov ax, [RootDirSectorNow] ; ax <- 现在正在搜索的扇区号 - mov bx, OffsetOfSectorBuf ; es:bx = BaseOfSectorBuf:OffsetOfSectorBuf - mov cx, 1 - call ReadSector - - mov si, [bp + 4] ; ds:si - mov di, OffsetOfSectorBuf ; es:di -> BaseOfSectorBuf:400h = BaseOfSectorBuf*10h+400h - mov dx, 10h ; 32(目录项大小) * 16(dx) = 512(BPB_BytsPerSec) - -.CompareFilename: - call StringCmp - cmp ax, 1 - jz .LoaderFound ; ax == 1 -> 比对成了 - dec dx - cmp dx, 0 - jz .GotoNextRootDirSector ; 该扇区的所有目录项都探索完了,去探索下一个扇区 - add di, 20h ; 32 -> 目录项大小 - jmp .CompareFilename - -.GotoNextRootDirSector: - inc word [RootDirSectorNow] ; 改变正在搜索的扇区号 - dec word [LeftRootDirSectors] ; ┓ - cmp word [LeftRootDirSectors], 0 ; ┣ 判断根目录区是不是已经读完 - jz .NoLoader ; ┛ 如果读完表示没有找到 LOADER.BIN,就直接开摆 - jmp .FindLoaderInRootDir - -.NoLoader: - call DispNewline - mov ax, cs - push ax - mov ax, NotFoundString - push ax - mov ax, _endNotFoundString - NotFoundString - push ax - call DispStr - add sp, 6 - jmp $ - -.LoaderFound: ; 找到 LOADER.BIN 后便来到这里继续 - add di, 01Ah ; 0x1a = 26 这个 26 在目录项里偏移量对应的数据是起始簇号(RTFM) - mov dx, word [es:di] ; 起始簇号占2字节,读入到dx里 - - mov word [bp - 2], dx ; set ax([bp-2]) as retval, popa will set that - popa - pop bp - ret - -;------------------------------------------------------------------------------ -; @param bp + 4: first cluster number -;------------------------------------------------------------------------------ -; Find File by name, return first cluster no. -; data => BaseOfSectorBuf:OffsetOfSectorBuf -FuncPrintClusterNumbers: - push bp - mov bp, sp - pusha - mov ax, [bp + 4] -.loop: - mov ax, dx - push ax - call DispInt ; ax now holds the current cluster number - add sp, 2 - push ' ' - call DispPutchar - add sp, 2 - - call GetNextCluster ; 根据数据区簇号获取文件下一个簇的簇号 - mov dx, ax ; dx <- 下一个簇的簇号 - cmp dx, 0FFFh ; 判断是否读完了(根据文档理论上dx只要在0xFF8~0xFFF都行,但是这里直接偷懒只判断0xFFF) - jnz .loop - - popa - pop bp - ret - -MAIN: - mov ax, cs ; cs <- 0 - mov ds, ax ; ds <- 0 - mov ss, ax ; ss <- 0 - mov ax, BaseOfSectorBuf - mov es, ax ; es <- BaseOfSectorBuf -; Display "This is {name}'s boot" - call DispNewline - - push cs - push StudentString - push _endStudentString - StudentString - call DispStr - add sp, 6 ; pop for 3 times - -; Read loader.bin's cluster number - push LoaderFileName - call FuncFindFile - add sp, 2 - - mov dx, ax ; temporarily save read result to dx -; Print first cluster number - call DispNewline - - push cs - push clusternoString - push _endclusternoString - clusternoString - call DispStr - add sp, 6 ; pop for 3 times - - push dx ; dx holds the read result - call FuncPrintClusterNumbers - add sp, 2 - -; Read aA1.txt's cluster number - push AA1FileName - call FuncFindFile - add sp, 2 - - mov dx, ax ; temporarily save read result to dx -; Print first cluster number - call DispNewline - - push cs - push clusternoString - push _endclusternoString - clusternoString - call DispStr - add sp, 6 - - push dx ; dx holds the read result - call FuncPrintClusterNumbers - add sp, 2 - jmp $ - -;============================================================================== -; CONSTANTS -;============================================================================== -BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长) -; according to osdev, memory from 0x0000:0x7e00 to 0x7000:0xFFFF is free to use -BaseOfSectorBuf equ 07000h ; diff from where we are here -OffsetOfSectorBuf equ 0400h ; the first 400h(=1024d) bytes left for fat1 read buf -RootDirSectors equ 14 ; 19~32 -SectorNoOfRootDirectory equ 19 ; Root Sector starts from 19 -SectorNoOfFAT1 equ 1 -DeltaSectorNo equ 31 -BPB_BytsPerSec equ 512 ; 每扇区字节数 -BPB_SecPerClus equ 1 ; 每簇多少扇区 -BPB_RsvdSecCnt equ 1 ; Boot 记录占用多少扇区 -BPB_NumFATs equ 2 ; 共有多少 FAT 表 -BPB_RootEntCnt equ 224 ; 根目录文件数最大值 -BPB_TotSec16 equ 2880 ; 逻辑扇区总数 -BPB_Media equ 0xF0 ; 媒体描述符 -BPB_FATSz16 equ 9 ; 每FAT扇区数 -BPB_SecPerTrk equ 18 ; 每磁道扇区数 -BPB_NumHeads equ 2 ; 磁头数(面数) -BPB_HiddSec equ 0 ; 隐藏扇区数 -BPB_TotSec32 equ 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数 -BS_DrvNum equ 80h ; 中断 13 的驱动器号 -BS_Reserved1 equ 0 ; 未使用 -BS_BootSig equ 29h ; 扩展引导标记 (29h) -BS_VolID equ 0 ; 卷序列号 -;============================================================================== -; dw means initialized DATA -;============================================================================== -LeftRootDirSectors dw RootDirSectors ; 还未搜索的根目录扇区数 -RootDirSectorNow dw SectorNoOfRootDirectory ; 目前正在搜索的根目录扇区 -BufferPacket times 010h db 0 ; ReadSector函数会用到的,用于向int 13h中断的一个缓冲区 - -LoaderFileName db "LOADER BIN", 0 ; 8:3, fill with whitespace -AA1FileName db "AA1 TXT", 0 ; the 0 at end is for StrCmp -StudentString db "This is LiMaoliang's boot" ; size = 25 -_endStudentString -clusternoString db "cluster no:" -_endclusternoString -NotFoundString db "not found" -_endNotFoundString \ No newline at end of file diff --git a/mergedep.pl b/mergedep.pl new file mode 100644 index 0000000..1730d53 --- /dev/null +++ b/mergedep.pl @@ -0,0 +1,86 @@ +#!/usr/bin/perl +# Copyright 2003 Bryan Ford +# Distributed under the GNU General Public License. +# +# Usage: mergedep [ ...] +# +# This script merges the contents of all specified +# on the command line into the single file , +# which may or may not previously exist. +# Dependencies in the will override +# any existing dependencies for the same targets in . +# The are deleted after is updated. +# +# The are typically generated by GCC with the -MD option, +# and the is typically included from a Makefile, +# as shown here for GNU 'make': +# +# .deps: $(wildcard *.d) +# perl mergedep $@ $^ +# -include .deps +# +# This script properly handles multiple dependencies per , +# including dependencies having no target, +# so it is compatible with GCC3's -MP option. +# + +sub readdeps { + my $filename = shift; + + open(DEPFILE, $filename) or return 0; + while () { + if (/([^:]*):([^\\:]*)([\\]?)$/) { + my $target = $1; + my $deplines = $2; + my $slash = $3; + while ($slash ne '') { + $_ = ; + defined($_) or die + "Unterminated dependency in $filename"; + /(^[ \t][^\\]*)([\\]?)$/ or die + "Bad continuation line in $filename"; + $deplines = "$deplines\\\n$1"; + $slash = $2; + } + #print "DEPENDENCY [[$target]]: [[$deplines]]\n"; + $dephash{$target} = $deplines; + } elsif (/^[#]?[ \t]*$/) { + # ignore blank lines and comments + } else { + die "Bad dependency line in $filename: $_"; + } + } + close DEPFILE; + return 1; +} + + +if ($#ARGV < 0) { + print "Usage: mergedep [ ..]\n"; + exit(1); +} + +%dephash = (); + +# Read the main dependency file +$maindeps = $ARGV[0]; +readdeps($maindeps); + +# Read and merge in the new dependency files +foreach $i (1 .. $#ARGV) { + readdeps($ARGV[$i]) or die "Can't open $ARGV[$i]"; +} + +# Update the main dependency file +open(DEPFILE, ">$maindeps.tmp") or die "Can't open output file $maindeps.tmp"; +foreach $target (keys %dephash) { + print DEPFILE "$target:$dephash{$target}"; +} +close DEPFILE; +rename("$maindeps.tmp", "$maindeps") or die "Can't overwrite $maindeps"; + +# Finally, delete the new dependency files +foreach $i (1 .. $#ARGV) { + unlink($ARGV[$i]) or print "Error removing $ARGV[$i]\n"; +} + diff --git a/target.xml b/target.xml deleted file mode 100644 index 71daae8..0000000 --- a/target.xml +++ /dev/null @@ -1 +0,0 @@ -i8086 diff --git a/实验3-v0.2.md b/实验3-v0.2.md new file mode 100644 index 0000000..24dc490 --- /dev/null +++ b/实验3-v0.2.md @@ -0,0 +1,293 @@ +
+ 实验三 进入保护模式 +
+ +
+ 谷建华 +
+ +
+ 2022-09-29 v0.2 +
+ +#### 实验目的 + +1. 学习x86保护模式编程,以及如何从实模式进入到保护模式. +2. 学习elf文件的组成和格式、加载elf文件格式的kernel. +3. 学习汇编与C之间的相互调用. +4. 学习二进制文件与elf文件之间的区别. + +#### 实验预习内容 + +1. 保护模式之前需要做的准备工作. +2. ELF文件格式. +3. 应用程序二进制接口. + +#### 实验内容以及步骤 + +**2022年试点班需要完成1、2、3三个实验,提高实验想做就做不强求** + +1. 编译构建硬盘启动盘,并挂载到虚拟机上,证实执行流交给了kernel + (1) 修改`kernel.asm`,使之能够在虚拟机终端中显示`kernel`字符串(注意,这个时候bios中断**不能**使用了). + (2) 通过`make`编译,生成`a.img`硬盘镜像. + (3) 通过`make run`运行虚拟机并挂载`a.img`作为硬盘启动盘,观察并记录现象. + +2. 通过修改`initkernel.c`的代码,解析`kernel.bin`elf文件并结合elf文档阅读,回答下面问题: + (1) 凭什么知道`kernel.bin`是一个elf文件? + (2) 凭什么知道`kernel.bin`运行的环境的指令集为i386? + (3) `kernel.bin`一共被加载了几个程序段?加载的段在内存中是从什么地址开始?加载的长度有多长?每个段的读/写/可执行标志是怎么设置的? + +3. 通过修改`kprintf.asm`,完成kprintf的函数的汇编实现,函数调用的参数如下: + (1) 第0个参数为从终端中的第几个字符开始输出,第0个字符是终端0行0列,第1个字符是0行1列,第80个字符是1行0列,以此类推. + (2) 第1个参数为需要格式化输出的字符串,传进去的参数实际是这个字符串的首地址. + (3) 再之后的参数根据第1个参数的字符串决定,每次调用kprintf函数后首先默认输出黑底白字的字符,接下来几条是需要实现的. + + + `%c`在终端输出参数的对应的ASCII字符值(参数保证范围在0~127之间),在输出字符后输出的位置往下移动一位. + + `%f`更改之后输出的字符的前景色为参数的对应值(参数保证范围在0~15之间,颜色码对应值) + + `%b`更改之后输出的字符的背景色为参数的对应值(参数保证范围在0~15之间,颜色码对应值) + + 其余字符则按照其ASCII码输出(保证字符串不会出现%、换行符、退格符等乱七八糟的字符),在输出字符后输出的位置往下移动一位. + + 在`start.c`中存放一组测试样例,效果已经写明可以大家可以用于测试. + + (4) 在`start.c`中接入`cmatrix.c`中的启动函数`cmatrix_start`,如果编写正确,会在终端中看到以下效果 + + ![](./lab3-finish.png) + + (5) (自我提高内容,自己想做就做,不用写进报告,**禁止内卷**)增添一个新参数`%s`,对应的参数是一个结构体,结构体的格式如下: + + ```C + struct color_char{ + u8 foreground;//输出字符的前景色(0~15) + u8 background;//输出字符的背景色(0~15) + u8 print_char;//输出字符的ASCII码(0~127) + u16 print_pos;//输出字符在终端中的位置 + }; + ``` + 这种输出方式不影响接下来`%c`输出的前景色、背景色和输出在终端中的位置,相当于是一个独立的输出方式.检验你程序的正确性可以调用`make tests`命令生成专属的测试kernel. + +4. 简单的`nm`工具制作(自我提高内容,自己想做就做,不用写进报告,**禁止内卷**) + (1) 通过分析elf文件的文件头,找到elf文件中的符号表,输出每个符号对应的符号名、符号地址、大小和类型. + (2) 这个操作可以在我们的内核中写,也可以自己独立写一个C/Cpp程序,如何检验自己的程序是否正确,可以通过`nm`和`readelf -s`检查. + +5. 创建FAT32格式的硬盘启动盘(自我提高内容,自己想做就做,不用写进报告,**禁止内卷**) + (1) 之前硬盘启动盘的文件系统格式为FAT12,这次需要将FAT12格式变成FAT32格式,这需要熟读官方文档,了解FAT32的文件格式以及处理方式. + (2) 修改boot/loader代码实现正确加载. + (3) 编译制作硬盘启动盘,启动虚拟机并挂载硬盘启动盘,观察并记录现象. +#### 实验总结 + +1. 在loader阶段都完成了哪些主要功能?X86系统是如何进入保护模式的?在进入保护模式之前需要完成哪些准备工作? +2. C语言和汇编语言是如何互相调用的?参数如何传递? + +#### 实验参考 + +##### 进入保护模式 + +在进入了loader后,终于可以不受512字节的代码限制.进入loader之后我们需要干的一件事是:**进入保护模式**. + +为什么要进入保护模式?这个时候这个问题就自然而然问出来了,这不是实模式待着好好地嘛,去啥保护模式.正常情况是这样的,对于我们写的程序来说实模式貌似挺够我们用的,但是如果稍微写个大一点的程序呢?这个时候一个噩梦般的事情发生了:段偏移的寻址方式太弱了,虽说1MB的寻址能力挺强的,但是对于一个固定段,它的偏移寻址能力只有64KB大,只需要128个boot程序般大的程序文件会超过偏移寻址能力,这对程序员来说是极为头疼的,写程序逻辑本身就很麻烦了,还要关心寻址,这程序谁爱写谁写. + +而保护模式解决了这个寻址问题,它扩展了段偏移寻址模式的定义,将偏移范围支持到4GB大,刚好是一个int能够表达的范围,这不爽死XD,所以进入保护模式还是很有必要的. + +保护模式是怎么扩展段偏移寻址模式的定义?这帮硬件工程师商量来商量去最后给出了一种方式`gdt(global descriptor table)`全局描述符.全局描述符抽象来看是一个数据结构,这个数据结构不能说是整齐规整,也可以说是东拼西凑,具体可以到Orange教科书中查询相关的数据结构细节.与实模式的段地址不同,保护模式的“段地址”就是gdt的基地址,而gdt能够表示偏移寻址的范围最多能到4GB,这样就能够支持4GB的寻址. + +回忆实模式,它的基址是一个地址值,很容易用一个寄存器进行存储,但是保护模式的段地址是一个数据结构的基地址,而且由于段寄存器的历史遗留问题仍然只有16位,不能填入32位数据,所以硬件工程师就搞出一条命令`lgdt`,由于我们在一个固定缓冲区按顺序存储一些gdt,亦叫gdt表(数组),只需要通过`lgdt`指令告诉硬件gdt表的起始地址和长度就能让硬件提前分析gdt表,这样我们只需要将段寄存器改成gdt相对于gdt表的偏移量(亦称选择子)就可以轻松表示.(改成这种方式还能够控制权限,不过本篇不谈) + +在进入保护模式之前,根据硬件厂商的手册,我们需要做三件事 + +1. 加载gdt表 +2. 打开地址线A20 +3. cr0第0位置位 + +这三件事做完之后,通过一个32位长指令的长跳转跳转到`SelectorFlatC:LABEL_PM_START`后,我们执行环境从16位变成了32位,从实模式跳到了保护模式.其中`SelectorFlatC`是希望cs变成的gdt选择子,`LABEL_PM_START`是指希望跳到的地址(不受段偏移限制了). + +在进入了保护模式后,就可以忘掉落后的段偏移访存方式了,能够更加舒服地写汇编程序了.但惊喜不止于此,我们还能够通过C语言实现我们想要的功能,想必看完那么多汇编大家肯定不想再读汇编了,苦日子终于熬到头了,可以用C了,C比汇编可好读太多了. + +##### 汇编与C之间的交互 + +在进入了C语言的部分之后就需要面临一个问题,如何进行汇编与C之间的交互?这个问题放平时可能想都不会想,大一开始的C语言课程可能根本不会让你意识到程序背后本质可以抽象成一条条汇编指令的执行,就如大家写的汇编程序一样,所以C语言代码本身可以具象为汇编代码,C语言中的函数本身就是一条label标签,这样就可以通过jmp和call进行函数跳转/调用将执行流交给C语言程序. + +但是这么说依然很抽象,你依然会很难理解一个在C语言中的函数调用具体发生了什么,一个带参数的函数跳转是怎么执行的,中间经历了什么? + +1. 导入/导出符号 + +如何让汇编代码与C代码进行相互调用,首先需要了解什么是符号,符号说人话就是函数名,全局变量名等.我们在编写汇编代码的时候会写一些标签,这些标签成了我们跳转的目标,也可以成为我们访存的目标,从某种意义上,符号就是这些标签. + +先解决第一个问题,如何在汇编中调用C函数/全局变量,对于汇编来说,这个符号是外部的,所以需要引入这个符号,引入到代码中,这样编译器就知道这个就是外部的符号,在跳转/访存的时候就知道这个符号是从哪里来的了.虽然在汇编文件编译的时候不知道确切地址,在链接的时候就能够知道了. + +```c +;nasm汇编程序 +extern foo ; 这个时候通过引入foo符号实现函数的跳转 +call foo ; 通过调用foo函数实现打印 + +//C程序 +#include +void foo() +{ + printf("114514 1919810"); +} +``` + +然后解决第二个问题,如何在C中调用汇编中的函数/全局变量,对于汇编来说,这个符号不仅是汇编文件用,外部也要用,所以汇编文件就需要将符号导出,告诉外界,它们想要的符号在汇编文件里,这样在链接的时候其他文件也知道这个符号在哪. + +```c +;nasm汇编程序 +global foo ; 导出foo符号 +foo: + ... ; 这里是foo函数的实现 + +global foofoo ; 导出foofoo符号 +foofoo dd 0 ; 这个是个全局变量 + +//C程序 +void foo(); //这里类似汇编里面的extern,如果下面没有foo函数的实现就将foo这个函数的符号导入进来 +extern int foofoo; //将foofoo这个全局符号导入进来 + +void bar() +{ + foo(); //这样就能够调用foo函数 + foofoo=1;//这样就能够使用foofoo变量 +} +``` + +这样汇编与C语言代码之间的相互调用就完成了.如果你好奇原理是什么,可以看第3节符号表. + +2. 参数传递 + +刚才解决的是C语言函数与汇编函数相互调用的问题,但是假设有这么个情况,这个C语言函数是一个带输入参数的函数,比如`int foo(int a, int b)`,这个时候如果光调用`call`指令可不好使,`a,b`参数就被吞了,下面的`foo`函数拿着混乱的参数大搞破坏,这肯定是我们所不希望的. + +那么对于`foo`函数,它还有两个参数`a,b`,那么如何将`a,b`“告诉”子函数,对于i386指令集规定了参数传递方式:对于所有参数,按照从右往左的顺序压栈,通过栈传递中间参数. + +对于上面这个例子来说,nasm汇编在调用C函数之前需要先将`b`压栈,然后将`a`压栈,最后才是调用`call`指令跳转到`foo`函数的标签处,C函数同样遵循这个规范,所以会去栈里寻找`a,b`两个变量的值,离栈顶越近的元素在函数定义的时候参数越靠左. + +这种从右往左的压栈方式其实很好,比如`printf`,我们不知道要输入多少个参数,但是会根据其中的字符串中的信息`%d,%s`判断需要多少个参数,然后在栈中找就可以将传入的参数拿出.在这次的实验中大家需要实现一个类似`printf`的函数,来深刻理解这种传参方式. + +通过压栈就能解决参数传递的问题,这种传递方式本质叫**应用程序二进制接口**,在第4节中有介绍. + +3. 符号表 + +可能大家都忘完了大一学的大计基和C程序,这个时候就要需要回忆一下一个C语言程序的编译过程`预处理、编译、汇编、链接`.相信大家在大一的时候把这一过程当文科一样背,现在可能基本记不清这几步具体发生了什么.前面三个步骤还能勉强理解理解,这个链接可能是真的难以理解. + +假设有若干个可重定位文件(.o文件),那么可以使用GNU的工具`ld`对这些可重定位文件进行链接成一个可执行文件. + +```c +//1.c程序内容 +void foo() +{ + int bar=114514; +} +int main() +{ + foo(); +} +//2.c程序内容 +void foo() +{ + int bar=1919810; +} +``` + +将两个程序的内容用gcc(附上-c参数)命令编译成两个可重定位文件,然后尝试将两个可重定位文件用ld命令链接,然后你就会发现一个神奇的现象. + +```shell +$ gcc -c 1.c && gcc -c 2.c +$ ld 1.o 2.o -o test +ld: 2.o: in function `foo': +2.c:(.text+0x0): multiple definition of `foo'; 1.o:1.c:(.text+0x0): first defined here +ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 +``` + +你会发现不让你链接,原因是foo被两次定义了.那么原因是什么?它凭什么知道foo被两次定义了?我们作为写程序的人肯定知道被两次定义了,但是机器怎么知道?这就涉及到**符号表**了. + +每一个可重定位文件里面存放着一张符号表,它存放所有符号信息.那么如何获取符号信息?GNU里面有一个工具`nm`能够获取对应的符号信息. + +```shell +$ nm 1.o +0000000000000000 T foo +0000000000000012 T main +$ nm 2.o +0000000000000000 T foo +``` + +看到这个的时候,哦,确实有两个foo的符号,所以在链接的时候会发生符号冲突.符号表是用于链接时定位的,在可重定位文件里,可能某个跳转语句的目的地址还是未知的,在链接成可执行文件时才能具体填入地址. + +在大一上C程序的时候大家可能不知道有`nm`命令,所以对链接的学习是迷迷糊糊的(再加上考试是不会考这个的,这个考了不挂一片人),所以可能仍不会`extern static`等相关关键词的语义.在做这节实验的时候可以尝试结合`nm`复习这些关键词的语义,这会对你对链接的理解有很大帮助. + +4. 应用程序二进制接口(Application Binary Interface) + +ABI本质上是一种约定,它约束外部函数调用的参数规范,每种指令集都有它自己的一套调用规范,类似64-x86,risc-V指令集首先用的是寄存器传参再是栈传参,而32位的i386指令集用的是栈传参.所以只要按照ABI规范写就一定能够正确调用外部函数. + +目前我们的OS用的是32位i386指令集,采取的传参方式是栈传参,就是在调用函数之前将所有参数从右往左依次压入栈中,最后才是函数跳转,不管是调用者还是被调用者都遵循ABI,这样就可以实现正确的函数调用. + +但是实际上有那么多种ABI,我们肯定背不下那么多ABI规范,但是有一种最笨也是最有效的方式就是:读汇编,读汇编,读汇编.因为C语言程序本质上也可以抽象成一个汇编程序,而汇编程序是最基础的,它每条指令的执行会按部就班地执行,而且每句话都是实打实地,不会骗你半点,当你觉得你实现的汇编很对但是链接进去执行错误的时候最好的方式就是反汇编/gdb. + +那么如何反汇编呢?GNU也给了我们一个工具`objdump`,通过调用`objdump -d kernel.bin | less`命令就可以将kernel.bin这个可执行elf文件(也可以是可重定位文件)反汇编,然后就可以愉悦地查看汇编代码了,通过查看实际的汇编代码是很好的,它完全反应了程序指令运行行为,对着它研究ABI准没错. + +当学会了ABI,大家可能就觉得C语言可能也那么神秘,你也可以独立从一个C语言翻译成一段汇编代码,只是需要读文档+花费逝量时间即可. + +##### 二进制文件和ELF文件 + +在进入了保护模式之后剩下loader要做的事就是加载kernel,毕竟loader是一个32位与16位代码混合的二进制文件,有实模式寻址的约束,不太适合扩展32位代码当内核,不如新启一个程序文件作为kernel. + +而这次加载kernel文件并不能像加载loader文件一样直接加载到指定位置然后跳过去,可以通过`file`命令查看两个文件类型: + +```shell +$ file loader.bin | tr ',' '\n' +loader.bin: + code offset 0x1c7+3 + OEM-ID "ForrestY" + root entries 224 + sectors 2880 (volumes <=32 MB) + sectors/FAT 9 + sectors/track 18 + serial number 0x0 + label: "OrangeS0.02" + FAT (12 bit) +$ file kernel.bin | tr ',' '\n' +kernel.bin: ELF 32-bit LSB executable + Intel 80386 + version 1 (SYSV) + statically linked + with debug_info + not stripped +``` + +loader.bin本质上是一个二进制文件,不过带有引导信息(沿用了boot的引导信息所以会被识别成一个文件系统),所以可以通过跳转跳到文件开头就可以进行loader的执行. + +而kernel.bin本质上是一个elf可执行文件,什么是elf可执行文件? + +##### ELF文件格式简介 + +试想一个简单的场景,你写的代码有代码数据和普通数据,这两部分数据是不对等的,对于代码数据你希望它不会被改动,所以对这部分数据不应该附上写权限,普通数据应该附上写权限.很明显这两部分数据不应该混在一块,而是应该分成两段存放.而且每一段都有它希望被加载到的地址,整个一个elf文件的入口地址也要指定……这些东西是需要一个数据结构进行管理,维护每一个段所需求的信息,然后在文件开头维护每个段的信息以及整个程序的信息等,这个数据结构就叫ELF文件格式.如果你比较熟悉内核,熟悉execve系统调用,那么[这里](https://www.bilibili.com/video/BV1wL4y1L72C/)会让你对ELF文件格式有更深刻的理解(如果不熟悉别听,绝对听晕). + +ELF维护了若干个程序段,每个程序段存放了相对应的数据,通过对数据的迁移到指定地址,再跳到入口地址就能实现elf文件的加载执行. + +在知道ELF是啥之后,怎么解析ELF文件,将ELF文件的程序数据迁移就是个问题,讲理论概念谁不会讲,但是在具体做的时候经常会怀疑人生,理论突然就不起用了,那么这个时候该怎么办?**读文档**,对于ELF文件格式,linux里面有写一个专门关于它的文档,可以通过调用`man elf`指令进行查看ELF里面的细节,可以结合`initkernel.c`代码与文档一起阅读,分析其中的具体含义(不要惧怕阅读英语,你们从小学开始学的英语不是仅用来考试的).需要注意的是`man elf`中是不会给出参数的具体值的,需要阅读`/usr/include/elf.h`头文件获取具体的值. + +当然解析这些玩意肯定折磨人,不过我们万能的GNU又给我们提供了一个工具`readelf`,当你想分析一个elf文件格式的时候,通过调用`readelf -a`获取所有信息,`readelf -l`获取程序头信息,当你觉得你的C程序分析错的时候,用`readelf`能够及时比对. + +##### 加载kernel + +虽然说起来ELF格式很复杂,但是由于kernel没啥权限需求,实际上加载的时候的代码没几行,实际上就遍历所有程序头,然后根据每个程序头的要求将文件数据加载到指定的位置,最后用一个跳转跳入kernel的入口地址. + +在进入了kernel了之后,我们真正有意思的内核就来了,大家会逐渐实现一些有趣的机制,并用这些有趣的机制实现很多有趣的玩意. + +##### 显存 + +进入保护模式后,实模式下能够使用的BIOS中断就不能用了,那么如何往终端打印字符就成了一个难事,不过好在天无绝人之路,在内存的[0xB8000, 0xC0000)处给了我们一个能够往那黑漆漆的终端写字符的途径,我们的终端只有25行*80列,一共2000个字符,每个字符由两字节表示,相当于终端显示的是显存[0xB8000, 0xB8000 + 4000)这部分的内容,这两字节低字节表示ASCII字符内容,高字节字符分成两部分,低4位为前景色,高四位为背景色,颜色码与实验一中的颜色码一致(具体可以到Orange书中7.2.1节中查看). + +在保护模式中,显存选择子对应的段就是显存的内存.所以在loader中可以通过gs段寄存器对显存进行存取字符,这个可比中断高效多了. + +```nasm +; 段描述符 +LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3 ; 显存首地址 + +; 段选择子 +SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3 + +; 将选择子加载到gs段寄存器 +mov gs, SelectorVideo +mov ah, 00fh; 黑底白字 +mov al, 041h; 'A' +mov word [gs:0 * 2] ax; 往0行0列写入一个黑底白字的字符A +mov word [gs:1 * 2] ax; 往0行1列写入一个黑底白字的字符A +mov word [gs:80 * 2] ax; 往1行0列写入一个黑底白字的字符A +``` \ No newline at end of file diff --git a/实验二-v0.3.md b/实验二-v0.3.md deleted file mode 100644 index 395dd5f..0000000 --- a/实验二-v0.3.md +++ /dev/null @@ -1,149 +0,0 @@ -
- 实验二 加载loader -
- -
- 谷建华 -
- -
- 2022-09-20 v0.3 -
- -#### 实验目的 - -1. 学习从boot加载loader的过程. -2. 学习FAT12文件系统. - -#### 实验预习内容 - -1. FAT12的基本格式. -2. BIOS读硬盘扇区的调用的使用方法. -3. 硬盘LBA的地址编码. - -#### 实验内容以及步骤 - -**2022年试点班需要做1、2两个实验,第3个实验想做就做不强求** - -1. 编译构建硬盘启动盘,并挂载到虚拟机上,观察并证实执行流交给了loader - (1) 修改`loader.asm`,使之能够在虚拟机终端中显示`This is {学生自己的名字的拼音}’s boot`. - (2) 编译`boot.asm`和`loader.asm`,生成相应的二进制文件. - (3) 通过`dd`和`mkfs`命令制作一个文件镜像盘,挂载到`/mnt`文件夹,将loader二进制文件写入文件镜像后将boot写入第0个扇区. - (4) 将文件镜像盘作为硬盘启动盘挂载到虚拟机,运行虚拟机观察并记录现象. -2. 观察并记录文件所使用的簇号 - (1) 修改loader文件,能够像boot一样寻找loader.bin. - (2) 编写打印功能将簇号打印在终端里. - (3) 制作硬盘启动盘,运行虚拟机观察并记录现象. - (4) 使用`dd`命令创建一个大小为4KB,名字为`aA1.txt`的文件并写入`a.img`,修改loader寻找`aA1.txt`并打印簇号,运行虚拟机观察并记录现象(此项可能较难,好好利用gdb调试和`xxd a.img | less`查看镜像数据). -3. 修复损坏的镜像文件(自我提高内容,自己想做就做,不用写进报告,**禁止内卷**) - (1) 我们准备了一个未知的损坏了的镜像文件,其文件系统为FAT12.它的第一张FAT表受到了部分损坏,根目录区也被未知数据覆盖,但万幸的是其余部分均未损坏,你需要根据根据剩下的信息恢复出文件系统中的数据. - (2) 此实验很磨时间,很难,做的同学需要好好对照FAT的官方文档进行学习,需要编写一个C程序仔细分析其中的关键信息并一步步恢复,验证你恢复是否成功可以将它用`mount`挂载并查看里面的数据,里面有我们准备的彩蛋. -4. 结合参考代码,请尝试自己重写boot代码,完成系统引导和加载loader的实验. -#### 实验总结 - -1. 在boot阶段都完成了哪些主要功能?这些功能是如何实现的?如何在引导盘中查找文件loader.bin的? -2. 为什么FAT12在寻找下一个簇的时候要连续读两个扇区,请通过画图的方式画出边界的几种情况. -3. 如果把引导盘的格式换成FAT32,查找文件loader.bin的过程是什么? - -#### 实验参考 - -##### 从boot到loader - -在上一节成功制作了硬盘启动盘后,实际上512字节远远不能满足我们的需求,这么点随便写点啥就超出去了,在进入真正的内核kernel之前,要做的东西可不少,加载解析内核文件,进入保护模式,向bios询问设备信息(比如剩余内存,设备树)等等,所以需要一个程序loader作为中间缓冲,它不受文件大小512字节限制. - -在有了loader之后boot的任务一下子就轻松了很多了,它只需要加载loader,并将执行流交给loader即可,仅完成一项工作512字节还是能够应付的. - -假设我们有了loader程序,我们怎么进行加载?要知道在第一个实验里我们的硬盘就只有512字节大,往哪里放loader?一个简单的想法就是直接将loader直接接在boot后面,但是这种方式可扩展性极低,无法存放多个文件,文件信息无法很好的管理.为了解决了这个问题需要一个在磁盘上的持久化的数据结构管理文件信息,上世纪七八十年代微软这帮工程师整出了FAT文件系统,FAT12是第一代,如果有兴趣可以在[这里](https://www.bilibili.com/video/BV1oZ4y1t7ce)听听FAT的历史由来. - -利用FAT文件系统可以有效管理文件,boot作为FAT文件系统的一部分放进了磁盘第0个扇区,然后根据第0个扇区的文件系统信息通过bios中断(分CHS和LBA两种模式,预实验简化了读入方式为LBA)读取磁盘其余扇区,完成loader的加载. - -##### 系统镜像文件的创建 - -在编写了boot和loader两个文件后就需要准备一个镜像文件存放,其中boot写入镜像文件的第0个扇区,loader放进文件系统中存放,根据boot里面的信息规范需要创建一个1.44MB大的镜像,通过`dd`命令创建一个1.44MB大的镜像文件. - -```shell -# if (input file) 读取的文件,这里读取的文件是zero抽象文件,这个文件会输出无尽的0 -# of (output file) 输出的文件,这里输出的地方是a.img镜像文件 -# bs 一次读写的数据字节数 -# count 重复多少次读写 -dd if=/dev/zero of=a.img bs=512 count=2880 -# 在命令结束后可以用stat检查a.img的大小确认 -``` - -在创建好镜像文件后,它现在还不是一个FAT文件系统,这个可以通过`file`命令检查,创建文件系统linux里面有一个命令叫`mkfs`(make filesystem). - -```shell -mkfs.vfat a.img -``` - -`vfat`指定文件系统类型,尽管FAT系列有很多,但是根据官方文档,`mkfs`根据镜像文件的大小判断该是什么类型,这里会默认生成`FAT12`文件系统,可以通过`file`命令检查. - -在创建完文件系统后就该考虑怎么把loader放进去了,镜像文件可以通过`mount`命令进行挂载,挂载到一个文件夹之后,所有对镜像文件的操作可以抽象成对该文件夹的操作.所以可以通过`cp`命令将loader写入镜像文件. - -```shell -# mount要求sudo权限 -sudo mount a.img /mnt -o loop #挂载到根目录的mnt文件夹,-o loop可以暂时不去理会 -sudo cp ./loader.bin /mnt #将loader拷贝过去后文件镜像里就写入了loader的数据 -sudo umount /mnt #解除挂载,可能会失败,如果没成功需要多执行几次 -``` - -将loader写入文件镜像后最后一步就要将boot写入第0个扇区. - -```shell -# conv=notrunc 如果这句命令不加上的话a.img又变回512字节大小了 -# no truncate 不会在写文件前让a.img强制置回0字节 -dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc -``` - -这样`a.img`就可以当做硬盘启动盘,放进qemu虚拟机模拟运行了. - -```shell -qemu-system-i386 \ --boot order=c \ --drive file=a.img,format=raw -``` - -##### 磁盘扇区的读写 - -在boot加载loader的过程中读取磁盘是靠着13h中断命令,通过中断让bios将磁盘中的数据读到内存中,读取方式有CHS(C:柱面,H:磁头,S:扇区)和LBA(Logical Block Addressing)两种.Orange教材用的是软盘CHS读取方式,读取一个扇区需要经过复杂的运算(柱面、磁道、扇区都要计算).这次使用了更简单的LBA读取方式,它只需要读入的逻辑扇区号就可以直接将磁盘上的文件读入,只需要传入一个结构体指针就可以进行读取. - -LBA读入方式使用的是bios的int 13h扩展读功能,扩展读功能的标识号ah=0x42,LBA最核心的是要往里面传入一个结构体参数,写成C语言的struct类似长这样: - -```c -struct buffer_packet { - short buffer_packet_size; - short sectors; - char *buffer; - long long start_sectors; - long long *l_buffer; -}; -``` - -+ `buffer_packet_size`:描述这个结构体的字节数(值为0x10或0x18,区别在于最后一个`l_buffer`是否启用).2字节 -+ `sectors`:从`start_sectors`开始需要连续读入的扇区数.2字节 -+ `buffer`:如果结构体大小为0x10,这个参数启用,描述接受磁盘数据的缓冲区地址.4字节,其高2字节为段寄存器值,其低2字节为偏移量 -+ `start_sectors`:磁盘中第几个逻辑扇区开始读入.8字节 -+ `l_buffer`:如果结构体大小为0x18,这个参数启用,描述接受磁盘数据的缓冲区地址.8字节. - -调用int 13h中断时的寄存器参数设置如下: - -+ ah=0x42 -+ dl=驱动号(模拟器里为硬盘镜像,驱动号为0x80) -+ ds:si=指向`buffer_packet`结构体的指针地址 - -通过调用int 13h将数据从持久化设备(磁盘)中读入到内存中,这样可以完成数据的加载. -##### 修复文件系统 - -修复文件系统说简单也简单,说难也难,如果不熟悉文件系统的话学习成本会巨高.在之前的实验,我们使用的是文件系统功能的子集,而修复文件系统需要知道它的全集,要不然从哪里修复都不知道,博客基本上是无法把所有东西讲清楚,所以**RTFM**是最有效的方法. - -当你脑中有一个设计知道怎么去做的时候,你又会发现第二个难题:你的实现能力跟不上了.在你们大学的前几年,可能说自己做了几个项目,但是实际情况上是用高级语言调用几个API就可以完成,难度在于建模,以及怎么跟数据打交道,并没有对底层有过太多的交互. - -大部分情况下我们都是在做计算型的编程,只要运算出来输出就完事了,而这次你需要独自面对系统,python这种高级语言可能会给你帮倒忙,因为它不太适合处理这种情况.你所能做的就只有拿着C/Cpp,以及一堆由glibc(比如printf,puts这些常用函数)包装好的API与内核进行交互. - -虽然同样是API,glibc的API相当的底层,需要`man`命令好好阅读相关函数调用的细节,不要惧怕英文.靠自己查博客绝对会把自己查晕,实现半天发现函数干的跟自己想的不一样 ~~(我曾经相信csdn)~~. - -在你们实现中会遇到另一个可怕的事情就是:struct结构体,如果你直接实现你会惊喜地发现可能结构体跟你想的不那么一致,~~不是语言不行,是你不懂语言~~. - -在实现时推荐大家使用一种防御性编程`assert(expr)`,就是当expr表达式的值为False时程序会及时奔溃并告诉你奔溃的位置.如果不想让自己的程序出太多离谱的错还要花大量时间尝试,就应该使用这种方式,在各种你觉得可能会出问题的地方加上一个`assert`. - -如果你通过一步步翻文档,一步步独立调错终于把镜像文件给恢复正确,你就能够看到我们精心准备的**彩蛋**,这给你的成就感会相当的大,如果会读手册,并有一定的实现能力你会发现你能干更多事,那些看上去很牛逼的玩意实际上你也能够实现. \ No newline at end of file