了解引导过程如何工作以及NetBSD如何处理引导过程的有用手册页如下:
fdisk(8):包含有关MBR如何工作、NetBSD如何处理它以及NetBSD采取的不同引导阶段的说明。
引导(8):关于引导过程如何工作的概述。特别是最后几个阶段(比如由/boot程序完成的阶段)以及如何修改引导参数。
在期望具有MBR分区的磁盘的系统上(请参见fdisk(8)),disklabel将在类型为169(NetBSD)的MBR标签的前8k和物理磁盘的前8k中查找标签,并在请求时更新标签。在其他系统上,磁盘标签将只查看磁盘的开头。写入标签的偏移量也取决于系统。
这些手册页介绍了NetBSD的引导程序。与FreeBSD(上面的链接)类似,NetBSD也将其划分为不同的程序:
第一个程序是一个非常小的程序,必须适合MBR(请记住,我们谈论的是i386)。它是/usr/mdec/mbr。然后,它将加载并将控制权传递给第二个程序,即bootxx。
因此,第二个程序是bootxx,位于/usr/mdec/bootxx_FSTYPE.TODO:它安装在哪里?第三个是一个大得多的程序(这里是52kb)/usr/mdec/boot,并复制到/。TODO:这是加载/boot.cfg文件的程序吗?
不过,NetBSD对此的理解略有不同,它从代码区占用了一些空间来填充引导信息。以下是NetBSD对MBR的详细理解:
/**MBR引导扇区。*这由磁盘扇区0中的MBR(主引导记录)和MBR分区的扇区0中的PBR(分区引导记录)使用。*/struct MBR_SECTOR{/*跳转到引导代码的指令。*//*通常为0xE9nnnn或0xEBnn90*/uint8_t MBR_jmpboot[3];/*OEM名称和版本*/uint8_t MBR_oemname[8];Union{/*BIOS参数块*/struct MBR_bpbFAT12 bpb12;struct MBR_bpbFAT16 bpb16;struct MBR_bpbFAT32 bpb32;}。/*MBR_BOOTSEL MAGIC*/uint16_t MBR_BOOTSEL_MAGIC;/*MBR分区表*/struct MBR_PARTITION MBR_PARTS[MBR_PART_COUNT];/*MBR MAGIC(0xaa55)*/uint16_t MBR_MAGIC;}__PACKED;
您会意识到,在结构上,留给bootstrap程序的空间比我们刚才预测的要小得多。这是因为在结构的顶部,就在MBR_BOOTCODE参数之前声明了其他字段。在对MBR进行编程时,这些字段的区别实际上是无关紧要的,它们实际上用于引导程序。我们了解,通过查看结构顶部的注释,该结构也可以用于MBR和PBR。这些奇怪的区域对于我们稍后将要看到的PBR非常重要。
它有一些参数,比如将有效分区更改为引导之前的超时,最后是mbrbs_nametab数组,该数组允许命名最多四个分区(MBR_PART_COUNT=4)、9个字符宽度(MBR_BS_PARTNAMESIZE=8,+1表示空字符)。这些常量在同一文件中定义。
状态(位7设置,即0x40,表示激活或可引导。一些旧的MBR支持0x80。
这就是所有魔力开始的地方。mbr.S程序是稍后汇编并存储在磁盘的前512字节的汇编代码。MBR读取并执行它。该程序找到NetBSD分区,读取其中的第一个扇区(保存下一个引导程序),然后执行它。
第一个程序位于src/sys/arch/i386/stand/mbr/mbr.S。这是稍后将在/usr/mdec/mbr和/usr/mdec/mbr生成二进制程序和变体的代码,但我们现在将学习最基本的版本。即是说,我们认为:
BOOTSEL是未定义的:我们对研究让用户更改要从哪个驱动器加载的代码不感兴趣。暗示第一个硬盘。
BOOT_EXTENDED未定义:我们对研究允许从扩展分区引导的代码不感兴趣。
COM_PORT未定义:我们对研究允许我们从串行线启动的代码不感兴趣。
定义了NO_BANNER:我们对研究向用户显示消息的代码不感兴趣。
NO_CHS未定义:因此所有读取都处于CHS模式。尽管它使事情变得更加困难,但是default/usr/mdec/mbr并不具备这一点(我相信它保持了向后兼容性)。
有关第一个程序及其不同风格的信息,建议查看MBR(8)手册页。
您可能希望从mbr.S源代码编译MBR程序。为此,首先使用顶级build.sh脚本编译工具:
它将首先编译一组构建NetBSD所需的工具。然后,cd到MBR Makefile所在的目录:
在我的例子中,有必要设置MACHINE_GNU_ARCH=i486,因为Make查找的是以i386为前缀的二进制文件。
这将创建mbr.o中间文件和mbrinal文件,这是可以在/usr/mdec/mbr中找到的最终文件。
MBR通常不会更新。如果用户想要更改MBR程序,则需要手动完成(sysinst也会这样做)。但是有一个非常有用的fdisk(8)实用程序仅用于更新分区表。
回到mbr.S程序,第一行有与程序一起使用的常量。稍后,我们开始节目(刚进入(开始))。entry()只不过是一个宏,在src/sys/arch/i386/include/asm.h中声明。
因此,我们看到entry()只是一个用于插入一些GNU Assembler指令的宏,包括一个指定程序入口点(Start)的标签。
让我们先解释一下MBR代码。BIOS将MBR的前512个字节读入地址0x7c00并执行。但以下代码实际上将这512个字节复制到别处(地址0x8800)并跳转到该地址。为什么?这是因为稍后将在0x7c00[3]中加载引导程序的第二阶段。如果您想了解更多关于MBR部门的信息,以及操作系统是如何处理这一问题和BIOS的,请参阅[BLU2010]。
地址0x8800没有出现在当前程序中的任何地方,但我们知道它,因为所有地址都以引用0x8800为起点,因为这是当前二进制文件将链接到的加载地址。如果您经常了解MBR代码是如何编译的,请看下面的代码片段。确保加载地址已传递给链接器。
LOADADDR=0x8800AFLAGS.mbr.S=${${ACTIVE_CC}==";clang";:?-no-integrated-as:}AFLAGS.gpt.S=${${ACTIVE_CC}==";CLANG";:?-无集成-AS:}${prog}:${objs}${_MKTARGET_LINK}${cc}-o${prog}.tmp${LDFLAGS}-wl,-ttext,${LOADDR}${objs}@set--$$(${NM}-t d${prog}.tmp|grep';\<;MBR_SPACE\&。\ECHO";#${prog}";${OBJCOPY}-O二进制${prog}.tmp${prog}rm-f${prog}.tmp中有$$1个可用字节。
因此,回到我们的ASM程序,它实际上是从下面的代码进入(开始)开始的。这段代码的作用是复制所有内容。
条目(START)xor%ax、%ax mov%ax、%ss movw$BOOTADDR、%sp mov%ax、%es mov%ax、%ds movw$MBr、%di mov$BOOTADDR+(MBR-START)、%si PUSH%AX/*对于%cs的Lret*/PUSH%di MOVW$(BSS_START-MBR)、%CX REP MOVsb/*重定位代码*/mor
这里使用的汇编程序语法来自GNU汇编程序,因为这是NetBSD项目使用的汇编程序。因此,目标寄存器位于操作的右侧。(http://stackoverflow.com/questions/2397528/mov-src-dest-or-mov-dest-src)。
4.下一行将%sp(堆栈指针)设置为地址0x7c00。BOOTADDR是在当前文件的第77行中使用此值定义的常量。根据[BLU2010](第15页),第#34;(.)。BIOS喜欢总是将引导扇区加载到地址0x7c00(.)";,因此我们必须告诉MBR程序我们在哪里:-)。然后,地址0x7c00至0x7e00(512字节)被保留用于该第一个程序。请记住,堆栈是向下增长的([BLU2010],第17页),因此到低于0x7c00的地址,从此不会触及我们的代码。
现代操作系统通常将所有这些寄存器指向同一位置,从而有效地禁止了它们的使用。这就是这里正在发生的事情(待办事项:确认)(http://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture#Segment_Registers)。
7.将MBR程序地址移动到%di[7][8]。MBR程序就是我们正在分析的当前块编写的。
movw移动一个16位整数(可以是地址),即一个字,还有movb、movl和movq操作码。它们分别移动BYTE、DWORD和QWORD。(http://www.programmingforums.org/post119574-5.html)。
一个字节代表8位;一个字代表16位;一个DWORD32位和一个QWORD64位(http://en.wikipedia.org/wiki/Word_%28computer_architecture%29).。
8.$BOOTADDR+(MBR-START)是加载到主存储器中时MBR程序的地址。将此地址移动到%si。记住:它不仅仅是0x7c00+(MBR-0),而是类似于0x7c00+(MBR-0x8800),因为这段代码是用加载地址0x8800编译的(正如我们刚才解释的),所以更改了当前程序中所有地址的偏移量。
11.bss_start在文件末尾定义,包含此程序末尾的地址[9][10]。因此,%cx将具有MBR&34;程序和当前程序结束之间的字节数,并将其用作计数器。
bss_off=0bss_start=.#定义bss(name,size)name=bss_start+bss_off;bss_off=bss_off+size bss(ptn_list,256*4)/*long[]:引导扇区号*/bss(dump_eax_buff,16)bss(bss_end,0)。
我们看到";bss";是紧跟在引导程序之后启动的位置,包含以下组件:
13.我们只设置了上面的%di,%si和%cx来调用此操作。我们调用movsb[11],它将复制地址%sito%di开始的所有字节(每次迭代都会递增),%cx倍[12][13]。也就是说,它会将所有程序(从MBR标签正下方到程序末尾)复制到当前块之后的地址(就在MBR标签或程序之前)。
14.在步骤11中,我们解释了bss_start具有当前程序结束的地址。bss_end在文件末尾定义。
我们可以看到,bss()是一个用于定义与bss_start相关的变量的宏。因此,bss_end是这个区域的末端,称为bss(TODO:它代表什么?它的目的是什么?)。最后,我们意识到,如果有一个未完成的块,%ch将保存512字节块的数量+1。即向上舍入的512字节块的数量。因此,举几个例子:
bss_end-bss_start+511=511->;511/512=0.998->;%ch=1。
bss_end-bss_start+511=512->;512/512=1.000->;%ch=1。
bss_end-bss_start+511=513->;513/512=1.001->;%ch=2
让我们记住,%ch存储%cx的8个最高有效位,即%cx=%ch*256+%cl。
16.将紧接在当前节目之后的存储区清零,即,将BSS清零。stosw将%ax(零)中的数量复制到从%di开始的内存区域。如果与rep操作码一起使用(这正是我们的情况),它使用%cx来了解需要重复[14]的次数,但请记住,由于stosw移动字(16位),%cx将有一半的字节需要写入。例如:假设我们要写入字节0x0 100次。如果我们要使用stosw(而不是stosb),%ax将为0x0,%cx将仅为50。
这解释了步骤14中的代码。";bss";大小是1040字节,(bss_end-bss_start+511)/512是3。但是因为它存储在%ch和%cx=%ch*256+%cl中,所以%cx=3*256+%cl,即%cx=768+%cl。它将是一个小于";bss&34;大小的数字,但是如果stosw复制的是字而不是字节,它将遍历双倍字节[15],因此将该区域清零。
17.最后,跳转到MBR地址,我们在步骤10将其值推送到堆栈。
我们将所有剩余的MBR代码移到地址0x8800,将BSS&34;部分(紧跟在当前地址之后)置零,并跳转到MBR代码所在的位置。现在,让我们在MBR所在的地方重新开始!在源代码中,它的编码恰好在我们刚刚研究的代码下面。根据[MBRX86],这正是典型的引导代码首先要做的。
我们将自己移到一边,因为我们将在地址0x7c00加载下一个程序pbr.s。有些人可能会想:为什么不直接执行这个程序并将pbr.s复制到其他地方呢?我们稍后将看到,在某些情况下可以直接加载pbr.Scan,而不需要像mbr.S这样的前一个程序,因此BIOS会将其加载到0x7c00。我们还需要在那里下载,因为它有所有的地址链接。
/**检查BIOS传递的驱动器号是否正常。一些BIOS可能不会*这样做并传递垃圾。*/mbr:cmpb$MAXDRV,%dl/*依赖于MINDRV为0x80*/jle 1f movb$MINDRV,%dl/*垃圾输入,启动盘0*/1:PUSH%dx/*保存驱动器号*/PUSH%DX/*两次-for err_msg循环*/。
关于这段代码,重要的一点是它使用了本地标签,它们只是由数字组成,跳转到它们是使用后缀f(向前)或b(向后)。http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_asmstabs
我们应该注意%dl寄存器。这是加载MBR的驱动器号,根据[MBRX86],这是BIOS传递给MBR的唯一重要编号。软盘驱动器为0x0、0x1等,硬盘驱动器为0x80、0x81等[18]。
1.如果首先开始比较%dl和$MAXDRV(0x8f),则可能的最大值(TODO:REFERENCES?)。如果数字小于或等于0x8f,则跳到下一个1标签。如果不是,将0x80(第一个硬盘)强制赋值为%dl。
2.将%dx的值压入堆栈两次。记住%dl存储%dx寄存器的最低位。
当前代码的其余部分是关于串口和打印给用户的消息,我们对此并不感兴趣。
在这段代码的末尾,我们的重要寄存器看起来是一样的,但是堆栈发生了变化。
它只检查%dl寄存器。这是BIOS存储我们从其引导的驱动器号(硬盘、软盘等)的位置。如果它是无效值,则强制0x80(第一个硬盘)。
/**浏览选择器(名称)表,打印用过的条目。**注册用法:*%ax temp*%bx namettab[]引导选择器菜单*%ecx base of';Extended';PARTITION*%edX下一个扩展分区*%si消息PTR(ETC)*%此分区的EDI扇区号*%BP parttab[]MBR分区表*/bootsel_menu:movw$nametab,%bx#ifdef boot_Extended xorl%ecx,%ecx/*扩展分区的BASE*/NEXT_EXTENDED:xorl%edX,%edX/*用于下一个扩展分区*/#endif Lea parttab-nametab(%bx)。%al/*分区类型*/#ifdef no_CHS movl 8(%bp),%edi/*分区扇区号*/#ifdef boot_Extended CMPB$MBR_PTYPE_EXT,%al/*扩展分区*/je 1f CMPB$MBR_PTYPE_EXT_LBA,%al/*扩展LBA分区*/je 1f CMPB$MBR_PTYPE_EXT_LNX,%al/*Linux扩展。%edi/*添加扩展的PTN base*/#endif test%al,%al/*未定义分区*/je 4f CMPB$0x80,(%bp)/*检查活动分区*/jne 3f/*如果没有.。*/#定义活动(4*((KEY_ACTIVE-KEY_DISK1)&;0xff))#ifdef NO_CHS movl%edi,PTN_LIST+ACTIVE/*保存活动PTN的位置*/#ELSE MOV%BP,PTN_LIST+ACTIVE#undef ACTIVE3:#ifdef BOOTSEL CMPB$0,(%bx)/*查看提示*/JZ 4f/*。:';<;<;8|';1';+count*/shl$2,%si/*const+count*4*/#定义const(4*(';:';<;<;8)+';1';-((KEY_PTN1-KEY_DISK1)&;0xff)#ifdef no_chs movl%edi,ptn_list-const(%si)/*要读取的扇区*/#Else mov%bp,ptn_list-const(%si)/*分区信息*/#endif#undef const mov%bx,%si call message_crlf/*PROMPT*/#endif4:add$0x10,%bp add$TABENTRYSIZE,%。
乍一看,这段程序似乎非常令人困惑,但事实并非如此:有些块我们只是打算忽略,因为我们在本节开始时认为有些宏是未定义或定义的。
1.第一行将nametab字符串的地址移动到%bxregister。代码顶部的注释显示nametab[]store";bootselector Menu";。让我们来看看姓名标签的定义。
此.ill指令将重复字节0x0,其大小为0x01,n次[19](其中n是MBR_PART_COUNT*(MBR_BS_PARTNAMESIZE+1))。MBR_PART_COUNT和MBR_BS_PARTNAMESIZE在src/sys/sys/bootlock.h中定义。通过BootIntroduction一节我们已经知道,namettab指的是最多4个分区的8个字符宽度的名称,即.full指令保留36个字节。
Fill指令的工作方式类似于循环,方式如下:它的语法是.Fill Count、Size、Value。即重复具有SIZE_SIZE、COUNT次的字节值。有关更多信息,请访问http://web.mit.edu/gnu/doc/html/as_7.html。
有关namettab和bootsel内容如何与MBR相关的更多信息,请查看MBR结构。
根据MBR(8),bootsel部分(包括namettab字符串)允许一些人选择从哪个分区引导,但是,为了理解该程序的基础知识,我们假设bootsel被禁用,从而生成一个简单得多的MBR程序。
我们还将在下一步使用parttab。它的定义就在下面。
我们看到空字节被重复64次(十六进制0x40),正好在幻数[20]之前的分区表的大小。
2.下一行以LEA操作码开始。要理解这一行,就足以理解Lea指令与Mov指令相似,但又有一些重要且众多的不同之处。对于此示例,只需知道,在mov地址、寄存器的形式中,当mov将源操作数的存储器地址的内容移动到目标时,lea本身也会移动地址。[21][22]。
此页面提供了有关创建LEA的原因以及如何使用它来循环访问数组条目(获取其地址)的更多详细信息:http://stackoverflow.com/questions/1658294/whats-the-purpose-of-the-lea-instruction。
它还使用相对地址模式。寻址模式在第35页的[BARTLETT2009]中有很好的说明。翻译,下面的表达式:
(请注意,减法是在编译时完成的。因此,在执行代码时,它将类似于x(%bx),其中x=parttab-nametab。)。
为什么程序员要编写如此复杂的行来将parttab的地址存储在%bp中?如果用户遍历其他分区,则这是循环的第一行。稍后,%bx被更改,程序执行从上面的标签NEXT_EXTENDED返回。由于我们对此不感兴趣,因此在我们的示例中,当前指令等同于:
在继续下一步之前,请注意,从当前块开头的注释来看,%bx存储nametab[]--boot选择器菜单的地址,%bp存储parttab[]--mbrartition菜单的地址。
3.与前面的指令一样,此指令也使用位移来指定地址。4(%bp)部分表示";采用%bp和4字节的地址,并将结果地址的内容存储在%bp";中。因为%bp具有分区表的地址,所以%bp+4将指向存储第一个分区的分区类型的字节(请参见Partition Structure表和MBR_SECTOR)。
4.由于no_chs为un.。
..