30天自制操作系统吧 关注:1,381贴子:4,751
  • 16回复贴,共1

如何实现同时支持从软盘、硬盘启动

只看楼主收藏回复

注意:这个帖子属于边写边记,之后应该会整理成文章发到其他平台。引导代码基于我自己写的bootloader,和30天的不一样
首先,我之前看过的很多书和文章都直接默认从0号软盘引导,因此引导扇区的代码也是写死了直接从扇区0读取loader。多亏了chatgpt,我找到了IBM的BIOS文档
文档中记录了INT 19中断功能就是关于Bootstrap Loader的。其中规定了:CS=0000H,IP=7C00H(也就是引导扇区的地址),DL=读取的(磁盘)驱动器号。虽然这是BIOS提供给程序用来从指定驱动器重新引导的,但是我相信初次启动时应该也是相同的。
实践出真知,使用QEMU+gdb调试,在0x7c00处打断点。参数指定从0号软盘启动时,DX=0x00,从0号硬盘启动时,DX=0x80。软盘驱动器号范围是0x00~0x7F,硬盘则是0x80~0xFF,所以基本就可以确定引导程序所在的磁盘号存在DL寄存器中。
如果直接存下DL作为后面加载程序的驱动器号,应该就可以实现软盘硬盘通用的引导程序了


IP属地:浙江1楼2024-10-26 23:05回复
    补充一下我的设计思路,证明我还活着
    boot部分在改成直接使用dl作为设备号后已经可以成功加载loader了(注意需要用的dx寄存器的时候先push dx免得丢失设备号了)
    首先loader要支持软盘用的标准读和大于chs寻址范围的硬盘扩展读。
    boot程序应该作为硬盘的第0个扇区,为了支持MBR,这个扇区0x1be位置开始的64个字节保存了分区表
    loader程序从第1个扇区开始,遍历分区表找到唯一的哪一个有可引导标准的分区,再根据磁盘的参数选择chs或lba模式读取对应分区的第0个扇区。
    为了程序简单,我选择仅支持fat文件系统(fat系都向下兼容),内核文件必定保存在根目录的第0个数据扇区。如果检测到了fat的bpb结构,则根据信息定位到第0个数据扇区,再循环读取文件目录项,找到名为KERNEL的文件(fat短目录项文件名都是大写)并读取。
    按照这样的流程,kernel文件的大小就可以变化,并且可以变化可被文件管理系统修改


    IP属地:浙江来自Android客户端2楼2024-10-28 17:55
    回复
      ...厉害呀,还在研究。现在不是有UEFI固件嘛,听说支持RAMDISK,加载到内存后再启动,只要主板支持UEFI,名字对就能启动


      IP属地:广西3楼2024-11-01 19:46
      收起回复
        再吱个声证明我没弃坑,这半个月主要是去造轮子了,写了个程序用来读写虚拟磁盘包括建立分区表和FAT32格式化等。
        现在在调试新写的引导程序了,目前已经可以读出fat32分区根目录了,离胜利不远了


        IP属地:浙江来自Android客户端4楼2024-11-19 13:15
        回复
          完了,遭遇滑铁卢了。0x13中断在写入到0x18e00之后bios就自己卡在那了,然而内核大小远大于这个数
          传统引导想要绕过去基本上只能上grub了,这一个月算是白干了


          IP属地:浙江来自Android客户端5楼2024-11-19 21:00
          回复
            今天终于搞定基于grub2的multiboot2引导了
            因为grub自带的grub-install只能直接安装到设备上,虚拟硬盘需要挂载非常麻烦,所以我花了好几天研究grub和grub-install的代码,最后发现直接遵循grub的默认设置就省去grub-install其中的大部分环节了。
            介绍一下,对于传统(相对于GPT分区)的MBR(Master Boot Record,主引导记录)分区设计的逻辑如下:
            BIOS依然是检查逻辑0扇区结尾的0x55,0xaa标志来判断是否能引导。按照MBR的思路,逻辑0扇区是一个特殊的引导程序,这个引导程序负责检查位于该扇区偏移0x1be处的分区表,这个分区表最多能记录4个分区(这也是为什么MBR分区只能由4个主分区的原因),然后根据某种规则从其中选取一个有“可引导”标记的分区,将这一分区的0扇区(被称为VBR分区,Volume Boot Record)加载到内存(一般为了保证兼容性还是在0x7c00),最后在跳转过去,由VBR负责加载该分区的系统。
            但是由于MBR和VBR都只有一个扇区也就是512字节那么大,所以稍微不那么简单的程序就已经放不下了,而且由于16位模式寻址范围最大只有1MB,对于大于1MB(考虑到BIOS本身和其他的部分,实际上1MB都没有)或者加载地址大于1MB的内核文件就无法加载了,而切换到保护模式又没法调用BIOS功能,势必要自己实现硬盘驱动和文件系统,复杂度就上升了数十倍。所以GRUB有两种解决方案。一种是把包含了这些功能的grub的core.img写入一段没有被划给任何分区的空间,再由定制的mbr程序(boot.S,编译后为boot.img)加载到内存;另一种则是core.img本身作为一个文件存放再某个文件系统内,再由mbr加载。
            这时候就会碰到一个问题,mbr怎么知道grub-core再哪个位置呢?core.img由grub-mkimage命令生成,实际上由多个部分组成,diskboot(对于磁盘是diskboot,其他设备会不同)、decompresser(用于解压grub-core,grub-core为了缩小体积可以选择压缩)和grub-core(其中还可选集成各种grub mod用于扩展功能),diskboot也是一个扇区大小,再调用grub-install时会自动设置boot.img中保存的diskboot位置,diskboot的尾部有一个blocklist,因为grub-core可能被分割成多个部分保存,blocklist把grub-core的各个部分以block结构组合起来用于读取grub-core。diskboot默认保存在mbr后的一个扇区。看过grub后,感觉grub本身就像一个严格控制过体积的迷你内核
            所以grub2启动过程时这样的:MBR(boot.S)->diskboot(diskboot.S)->(decompresser->)grub-core->真正的系统内核。
            今天先写到这,明天继续写实操部分


            IP属地:浙江6楼2024-11-25 00:07
            收起回复
              其实正常情况下,grub kernel启动后就需要读取grub.cfg配置文件来进行引导(如果找不到就会进入grub命令行),但是grub kernel也不知道grub.cfg在哪个设备哪个分区,grub时通过设置(grub中的)环境变量来指定的,环境变量和core.img集成的module等信息都存放在grub kernel的开头位置,由于这一部分是可能被压缩的,所以手动修改会比较麻烦。
              grub-install会自行检测设备信息等并配置这些信息,但是由于使用grub-install需要先挂载虚拟硬盘会很麻烦,所以我决定绕开grub-install。在grub-mkimage中有个参数--prefix,设置这个参数就可以将虚拟硬盘中grub的位置写入core.img,例如"(hd0,msdos1)/boot/grub"(注意不能有空格)就指的是第0个硬盘(mbr分区表)的第1个分区下/boot/grub/目录。另外,grub-mkimage在生成core.img时默认会以core.img连续且位于逻辑扇区1的情况配置blocklist
              接下来安装grub的步骤就简单了,先把boot.img的前440字节复制到mbr扇区(避免覆盖分区表),然后注意在分区时留出1MB空间,把整个core.img复制到逻辑扇区1开始的位置。最后记得把grub.cfg复制到虚拟硬盘的对应位置就好,我用的是自己写的工具imagetool可以不用挂载硬盘直接操作虚拟硬盘
              我使用的命令参数如下:
              grub-mkimage -O i386-pc -o core.img --prefix "(hd0,msdos1)/boot/grub/" minicmd normal gzio gcry_crc verifiers terminal priority_queue gettext extcmd datetime crypto bufio boot biosdisk part_gpt part_msdos fat ext2 fshelp net multiboot2 all_video gfxterm
              最后三个mod是我自己根据需要加的,multiboot2是为了按照multiboo2协议引导内核,all_video和gfxterm是为了可以切换到图形模式,其他的都是grub-install的默认配置,一些mod之间有依赖关系,如果缺少了grub就不能正常启动,会进入grub-rescue模式
              按照这个思路,我写了个简易的脚本来安装grub到虚拟硬盘


              IP属地:浙江7楼2024-11-25 11:13
              回复
                然后就是multiboot2,之所以采用multiboot2而不是multiboot是因为multiboot2对于header的位置限制的不那么死,只要在文件的前32768字节就行。
                Multiboot2 Header 的基本字段
                字段 |大小| 值 | 说明
                magic | 4 | 0xE85250D6 | 标识符,用于表明该文件兼容 Multiboot2 协议。
                architecture | 4 | -------------- | 描述目标架构:0:32 位 x86,4:64 位 x86。
                header_length | 4 | -------------- | Multiboot2 Header 的总长度(包括所有 tag)。
                checksum | 4 | -------------- | -(magic + architecture + header_length) 的结果,使得这三个字段的和为零。
                Multiboot2 Header要求对齐8字节(Tag也是)
                Multiboot2有如下种类的Header Tag,具体说明见Multiboo2 Specification:

                我自己是这么配置的:


                info_tag指定了我需要内存信息、帧缓冲区地址和VBE模式信息,framebuffer_tag指定了我需要引导程序切换到1024x768x32bit的显示模式。
                到这里已经进入内核了,但是我们还需要重新配置GDT和IDT等信息来满足内核需要


                IP属地:浙江8楼2024-11-25 11:29
                回复
                  目前已经成功进入内核初始化程序了,就是一到init_apic就会重启,还没深入调试。今天要赶DDL,明天继续


                  IP属地:浙江9楼2024-11-25 14:56
                  收起回复
                    果然是VRAM内存映射的问题
                    接下来就是配置GDT,读取引导提供的信息然后再跳转进内核

                    multiboot2header这里修改成了宏定义,防止出现在.symtab部分导致读错文件头

                    gdt加载则是直接复制原来的方式进行。
                    然后就是把ebx和eax压入栈作为multiboot2_loader函数的参数调用,按照multiboot2协议的规定,eax存着魔数0x36d76289,ebx则是boot info的起始地址。
                    接下来进入C写的部分:
                    最开始的4个字节是总大小,后4个字节用于对其8字节。再往后就是同样对齐8字节的info tag,具体每个info tag的定义见Multiboot2 Specification

                    原先的引导程序给内核提供的信息有从BIOS获取的内存信息和显示模式信息,对应的info tag是basic memory infomation和framebuffer inf

                    memory map结构和BIOS获取的结构一样,所以直接复制就好,需要注意的是这里的结构也是对齐过8字节的。framebuffer info也是直接复制就行了,这里的VideoInfo是一个全局变量,所以不用等内核初始化就可以直接赋值

                    然后是循环尾部的处理,由于p是uint32_t类型的指针,所以size要先除以4再加
                    结束之后就可以直接跳转到原来的内核入口处启动了


                    IP属地:浙江10楼2024-11-26 21:47
                    回复