探本溯源——深入领略Linux内核绝美风光之系统启动篇(二)

在前文结尾处我们提到内核映像的加载是由专用的bootloader比如LILO或是GRUB来实现的,而在x86架构下Linux内核通常使用其中之一的GRUB,它通过执行initrd文件来识别内核映像所在的文件系统进而执行加载,然而有一个需要注意的问题是,并非所有的物理地址空间对内核而言都是可用的,比如其中的某个物理地址范围可能被映射为I/O设备的共享内存,也可能其中的一个物理页框存放着BIOS数据,综合上述原因,GRUB必须建立一个物理地址映射来将内核加载至可用的物理内存中,而建立映射的这一过程则是根据协议完成的,不仅如此,实模式下的内核代码能够占用的内存空间,以及初始化过程中用来建立堆栈的物理内存大小都需要遵守相关的启动协议,有关LINUX/x86启动协议的详细信息可参考Documentation\x86目录下的boot.txt文件,该文件还详细解释了header.S中所定义的全局变量hdr的各个字段的含义,而其重要性单从源文件中各个字段后的注释规模便可见一斑。

物理内存布局
在深入剖析源代码之前,我们有必要首先明确启动阶段物理内存的分布情况,因为只有明白了执行过程背后所依据的一些基本事实才能对源代码有更为深刻的认识,而其中的一些事实源于软硬件的高速发展,另外一些则是为了满足兼容性的要求。

总的来说,内核映像由三部分构成,它们分别是:

  • 一个512字节的启动扇区——Kernel boot sector,也即在前文中所剖析的Linux内核自带的bootloader,但由于现在使用的bootloader为GRUB,为了避免产生歧义,所以冠以另外一个名称。该boot sector占用操作系统映像所在逻辑分区的第一个扇区,注意从bootsect的偏移0x1f1处开始存放hdr变量。
  • 实模式下的内核安装部分——Kernel setup,连续占用若干个扇区大小的内存空间,主要用于检测硬件环境并执行一系列的初始化,为保护模式下内核代码的运行完成一些前期的准备工作。
  • 保护模式下的内核代码,GRUB将其加载至从地址0x10 0000开始的物理内存中。

这里要注意的是实模式下的Kernel boot sector和Kernel setup虽然从逻辑上被分成两部分,但它们的分布是连续的,并且这两部分都处于第一个1MB的物理内存空间中,而其上所提到的保护模式下的内核代码的初始地址为0x10 0000,表示这部分代码从物理内存的第二个1MB开始安装,因此内核映像的实模式及保护模式这两个部分的分界线即物理地址0x10 0000。这里要特别注意的一点是,GRUB不可能单独在实模式下完成上述加载任务,因为在实模式下CPU只能寻址第一个1MB范围内的物理内存空间(原因请参见处理器体系结构及寻址模式一文)。有意思的是,在GRUB的次引导过程执行时将会临时性地切换到保护模式,完成内核映像的加载后再次回到实模式。因此虽然引导加载过程有两种模式的来回切换操作,但对于内核映像来说却是透明的,当控制权转交给内核后,它仍然首先从实模式下开始运行,但此时并不意味着超出第一个1MB范围的物理内存中没有任何内容,只是这些内容占用的内存空间无法被内核当前执行的指令所寻址,这使得保护模式下的内核代码不会被随意修改,也算是对保护模式中“保护”一词的另类诠释。

以下是GRUB将内核映像加载至内存空间后的分布图:


图1

上图同样位于Documentation\x86目录下的boot.txt中。在该文件中还展示了由zImage内核映像所使用的传统内存映射模型,但我们并不过多关注这类历史遗留问题,在后续的代码剖析过程中均以现阶段所使用的一些模型作为基准。在前文结尾处曾提到过BIOS例程将内核放入低地址0x0001 0000(小内核映像zImage)或者从高地址0x0010 0000(大内核映像bzImage)开始的RAM中,而现阶段所使用的Linux内核均编译为bzImage,因此保护模式下的内核代码被安装在从高地址0x0010 0000开始的RAM中。正如上图所示,我们也可以看到在完成POST以及一系列的初始化工作后,BIOS将bootloader加载至物理地址0000 7c00处。而令人困惑的是,内核的启动扇区bootsect的起始地址并未被严格限制,这其实是由于Linux内核允许使用多种bootloader所导致的,比如前文所提到的LILO以及GRUB,在现实情况中存在更多不同类型的bootloader,不同的bootloader可能将实模式的起始地址加载至不同的位置,然而在x86架构下的GRUB设置的起始地址正是0x9 0000

另外我们也可以看到实模式下的内核代码以及该代码所创建的堆栈的大小至多为8KB,之所以需要创建堆栈是为了提供C语言的运行环境,因为代码的执行过程需要使用栈来保存局部变量,函数调用则需要借助栈来传递参数以及保存返回地址,其中还有可能涉及到动态内存的分配,以及将内存分配给内核命令行。这样一来,实模式下的内核代码需要使用的内存空间将会达到8KB*2=16KB,而该段的起始地址为0x9 0000,因此这段内存空间的结尾处的地址至多将会达到0xA 0000。而这是不允许,因为现代机器中的许多BIOS例程需要使用从起始地址0x9 A000开始的额外内存空间,该内存空间即扩展的BIOS数据区(Extended BIOS Data Area,EBDA)。若内核被GRUB安装至较高的内存空间,那么执行代码将被BIOS修改。事实上实模式下的Linux内核正是基于这一原则所设计的,我们将在后文的源代码剖析过程中看到其具体的实现细节。

我们在前文提到过,代码背后所依据的某些事实是为了满足历史遗留问题所提出的兼容性要求,这一点可从上图中物理地址范围0xA 0000~0x10 0000的内存占用情况得知。追溯至上世纪80年代,当时IBM所推出的第一台PC机可供寻址的物理内存总共为1MB。而这1MB中的低640KB供DOS以及应用程序使用,而高端的384KB则被留作它用,其中低端的640KB被称为常规内存,高端的384KB则被称为保留内存,这两种不同的内存类型通过物理地址0xA 0000得以分隔,此后这个分界线便被确定下来并沿用至今。

简单总结一下,在x86体系结构中,RAM的第一个1MB内存空间包含如下两个“独特”的地方:

  • 物理地址0x0000~0x1000以及0x9 A000~0xA 0000所占内存由BIOS使用,存放加电自检(Power-On Self-Test,POST)期间检查到的系统硬件配置。有些类型的BIOS甚至在系统初始化之后依然将数据写入该内存。
  • 从0xA 0000~0x10 0000范围内的物理内存通常保存BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是IBM兼容PC上从640KB到1MB之间的著名的洞——图1中的I/O memory hole:物理地址存在但被保留,不能由操作系统使用。

内核将上述地址范围的物理内存所占页框标记为保留,它们连同内核代码以及已初始化或未初始化的内核数据一起,在整个机器的执行周期中常驻内存,而绝不能被动态分配或由内核调度程序交换到磁盘上。


图2

上图形象地显示了常驻物理内存中各个区域的使用情况,需要注意的一点是保护模式下的内核代码所占页框的总数依赖于对应的配置方案,因此上图中与之对应的区域只是用符号简单的加以表示。而实模式下的内核代码仅在启动期间执行硬件检测及一系列初始化操作之后便不再被使用,因此当实模式下的内核代码跳转到保护模式之后,这部分内存空间即可由内核代码用于其他用途。

实模式下内核的初始化变量——安装头(setup header)
该安装头为从Kernel boot sector中偏移0x1f1处开始的hdr变量,主要存放初始化期间将会使用到的一些数据。我将把该变量中各个字段的含义集中罗列在这里,在后文中讲到内核执行初始化过程中使用到这些数据时不再单独详细描述。另外要注意的是有些字段的存在同样属于历史遗留性问题,对于这些内容我们直接一带而过。下表是这些字段的概要说明:

偏移量/大小字段名含义
0x1F1/1setup_sects表示Kernel setup中的代码所占用的物理存储空间
0x1F2/2root_flags
如果被设置,则根以可读形式挂载
0x1F4/4syssize
32位代码的大小,该值以16字节的段为单位
0x1F8/2ram_size
不再使用——仅由bootsect.S文件使用(已被废弃
0x1FA/2vid_mode
视频模式控制
0x1FC/2root_dev
默认根设备号
0x1FE/2boot_flag
魔数,其值为常量"0xAA55",标识有效引导记录结尾的标签
0x200/2jump
跳转指令
0x202/4header
魔数标签,其值为"HdrS"
0x206/2version
启动协议版本支持
0x208/4realmode_swtch
引导加载程序钩子
0x20C/2start_sys_seg
加载低地址段(已被废弃
0x20E/2kernel_version
指向内核版本字符串的指针
0x210/1type_of_loader
引导加载程序标识符
0x211/1loadflags
引导协议选项标志
0x212/2setup_move_size
移动至高位内存所需的大小(已被废弃
0x214/4code32_start
引导加载程序钩子
0x218/4ramdisk_image
initrd加载地址
0x21C/4ramdisk_size
initrd的尺寸
0x220/4bootsect_kludge
不再使用——仅由bootsect.S使用(已被废弃
0x224/2heap_end_ptr
在setup尾部之后的空闲内存
0x226/1ext_loader_ver
扩展的引导加载程序版本
0x227/1ext_loader_type
扩展的引导加载的ID
0x228/4cmd_line_ptr
指向内核命令行的32位指针
0x22C/4ramdisk_max
initrd可用的最高地址
0x230/4kernel_alignment
对内核要求的物理地址对齐
0x234/1relocatable_kernel
内核是否可重定位
0x235/1min_alignment
最小对齐,其值要求为2的平方
0x236/2pad3
不再使用(已被废弃
0x238/4cmdline_size
内核命令行的最大尺寸
0x23C/4hardware_subarch
硬件子架构
0x240/8hardware_subarch_data
特定子架构的数据
0x248/4payload_offset
内核负载偏移
0x24C/4payload_length
内核负载的长度
0x250/8setup_data
指向存放setup_data结构体的链表的64位物理指针
0x258/8pref_address
首选的加载地址
0x260/4init_size
初始化期间的线性地址要求
注:上表中由红色标识的字段已被废弃,由绿色标识的字段不再建议使用,而是统一在命令行中设置

下面单独列出其中某些重要字段更详细的解释,注意所有字段都以小端法存放,并且其中一些字段存放由引导加载程序从内核中读出的信息(即类型为可读),另外一些字段则由引导加载程序填充(类型为可写),其他的字段则由引导加载程序做适当的修改(类型为可修改)。

  • setup_sects——占用1个字节,类型为可读,表示Kernel setup所占用的物理内存大小,且以一个512字节的扇区为单位。为了保持后向兼容,如果该字段被赋值为0,那么实际的值是4。实模式代码由boot sector(总是占用一个512字节的扇区大小)加上Kernel setup代码组成。
  • syssize——占用4个字节,类型为可读,该字段表示保护模式代码的尺寸,大小以16字节的小段为单元。但对于现在的Linux内核来说,在进行引导配置时仅使用其中的两个字节——两个高位字节不再可用,因此如果LOAD_HIGH标志被置位那么该字段不能被认为是内核的大小。
  • jump——占用2个字节,类型为可读。该字段包含x86架构下的跳转指令,0xEB(跳转指令的字节码)后跟一个相对于地址0x202的有符号偏移,这个字段能被用来决定安装头的大小。
  • header——占用4个字节,类型为可读,包含魔数"Hdrs"(0x53726448)。如果该魔数没有在偏移0x202处设置,那么启动协议的版本被认为是旧的,因此装载一个老的内核。但我们在源文件header.S中清晰地看到了该标签,因此总是加载bzImage内核映像。并且header字段之后的version字段包含协议的版本,例如若version字段被设置为0x0204则代表使用2.04版本的协议,在源文件中该字段被设置为0x020a,因此表示使用最新的2.10版本的协议。
  • kernel_version——占用2个字节,类型为可读,如果该字段被设置为非零值,则表示一个指向以NULL结尾的含有内核版本号的字符串的指针。这能够被用来向用户展示内核版本。字段值应小于0x200*setup_sects。
  • type_of_loader——占用1个字节,类型为可写,该字段与ext_loader_type以及ext_loader_ver字段联合起来表示所使用的bootloader的类型及其版本号,由于x86架构下始终使用的是GRUB,因此这里我们不再继续深究,具体细节可参考Documentation\x86目录下的boot.txt文件。
  • loadflags——占用1个字节,类型为可修改,这个字段是一个位掩码(bitmask)。第0位(只读):LOADED_HIGH,如果该位置0,那么保护模式下的代码被加载至0x1 0000处,若复位则加载至0x10 0000。第5位(可写):QUIET_FLAG,如果置0则打印早期信息,如果复位则禁止早期信息。第6位(可写):KEEP_SEGMENTS,如果该位置0那么在32位入口点处重新加载段寄存器,如果复位那么不会重新加载。第7位(可写):CAN_USE_HEAP,将该位置1指示字段heap_end_ptr中的值有效,如果这个位被清除,那么一些Kernel setup代码将无法执行。其中最重要的是第0位与第7位。
  • code32_start——占用4个字节,类型为可修改。表示在保护模式中的跳转地址,其值默认为内核的加载地址,同时能够用来被bootloader决定合适的加载地址。修改这个字段是出于以下两个目的:①作为引导加载程序的钩子(a boot loader hook),②如果没有安装钩子的bootloader将一个可重定位的内核加载至非标准的地址,那么bootloader将会修改这个字段以指向加载地址。
  • ramdisk_image/ramdisk_size——均占用4个字节且类型为可写。这两个字段主要指示initrd的32位线性地址及其尺寸,若不使用initrd则这两位均为0。此外还有一个名为ramdisk_max的字段,同样占用4个字节但类型为可读,表示initrd可用物理内存的最大地址。我们在前文中提到过,initrd主要是由GRUB的次引导程序载入内存,实现一些模块的加载及文件系统的安装。
  • heap_end_ptr——占用2个字节,类型为可写。将这个字段设置为Kernel setup中堆栈结尾处距离实模式代码起始部分的偏移减去0x200后的值。
  • cmd_line_ptr——占用4个字节,类型为可写。将这个字段设置为内核命令行的线性地址。内核命令行能够被定位至Kernel setup中堆的结尾处至物理地址0xA 0000之间的任何位置,正如实模式代码自身一样,内核命令行同样不一定需要被放置在同一个64KB段中。即使引导加载程序不支持命令行,也需要填充这个字段,在这种情形下可以将其指向一个空字符串。但如果这个字段被置为0,那么内核将假设引导加载程序不支持2.02以上版本的协议(当前所使用的是2.10版本的协议)。
  • kernel_alignment——占用4个字节,类型为可读可修改。若relocatable_kernel字段被设置为真,那么这个字段是由内核要求的对齐单元。一个可重定位内核当被加载至对齐方式与当前字段不兼容的地址处,那么在内核的初始化过程中将会被重新对齐。在允许更小对齐的情况下,这个字段可以由引导加载程序修改。
  • relocatable_kernel——占用1个字节,类型为可读。如果这个字段非零,内核的保护模式部分能够被加载至满足kernel_alignment字段的任意地址处。完成加载之后bootloader将会设置code32_start字段以指向被加载的代码,或是bootloader钩子。
  • min_alignment——占用1个字节,类型为可读。这个字段如果非零那么作为2的幂指示最小对齐要求。如果引导加载程序使用了这个字段,它也应该更新kernel_alignment字段,更新方式为:kernel_alignment=1<<min_alignment。
  • cmdline_size——占用4个字节,类型为可读。这个字段表示不考虑结束符0在内的命令行的最大尺寸。这移位这命令行最多能够包含cmdline_size个字符。
  • hardware_subarch/hardware_subarch_data——分别占用4个及8个字节,且类型均为可写,这个字段允许bootloader通知内核现在所处的硬件环境。
  • payload_offset——占用4个字节,类型为可写。如果非零那么这个字段包含从保护模式代码的起始地址到负载(payload)的偏移量。负载应该被压缩,压缩和非压缩的数据都应该使用标准的魔数来决定。当前所支持的压缩格式分别是:gzip(魔数为1F 88或1F 9E),bzip2(魔数为42 5A),LZMA(魔数为5D 00)以及XZ(魔数为FD 37)。非压缩负载的格式至今总是ELF(魔数为7F 45 4C 46)。下一个字段payload_length指示负载的长度。
  • setup_data——占用8个字节,类型为可写。这个字段是一个指向节点为setup_data结构体且以NULL结尾的链表的64位物理指针。它被用来定义可扩展的启动参数传递机制。
  • pref_address——占用8个字节,类型为可读。这个字段如果非零,则其值为内核首选的加载地址。一个可重定位的bootloader应该尽可能试图将内核加载至此处。一个不可重定位的内核则无条件移动其自身并从该地址处开始运行。
  • init_size——占用4个字节,类型为可读。这个字段指示了在内核能够检测内存映射之前它所需要的线性连续内存的总量,这段连续内存起始于内核运行时的开始地址。它能够被用来帮助可重定位的引导加载程序为内核选择一个安全的加载地址。

建立堆栈——准备C语言的运行环境
在前文中提到过,GRUB在完成一系列工作之后通过执行一个长跳转指令进入内核的入口点,该入口点位于从实模式内核起始的偏移量0x200处。这意味着如果实模式内核代码在地址0x9 0000处,内核的入口点则为0000:9020。在起始处,ds/es/ss寄存器应该指向实模式内核代码的开始处,即如果代码被加载至0x9 0000处时这些寄存器的值被置为0x9000(注意这一点很重要!),栈指针寄存器sp一般指向堆的顶部,并且中断被禁用。此外为了防范错误,在有些引导加载程序中将把fs/gs/ds/es/ss寄存器均设为相同的值。通常引导加载程序的典型设置方式如下所示:

  1. /*段基址由特定的引导加载程序而定,在x86架构下始终使用GRUB*/  
  2. /*因此seg被设置为0x9000*/  
  3. seg = base_ptr >> 4;    
  4.   
  5. /*禁用中断*/  
  6. cli();  
  7.   
  8. /*设置实模式内核栈 */  
  9. _SS = seg;  
  10. _SP = heap_end;  
  11.   
  12. /*将DS/ES/FS/GS寄存器设为段基址值*/  
  13. _DS = _ES = _FS = _GS = seg;  
  14.   
  15. /*执行长跳转将控制权转交内核*/  
  16. /*从header.S的_start全局标号处开始执行*/  
  17. jmp_far(seg+0x20, 0);  
/*段基址由特定的引导加载程序而定,在x86架构下始终使用GRUB*/
/*因此seg被设置为0x9000*/
seg = base_ptr >> 4;  

/*禁用中断*/
cli();

/*设置实模式内核栈 */
_SS = seg;
_SP = heap_end;

/*将DS/ES/FS/GS寄存器设为段基址值*/
_DS = _ES = _FS = _GS = seg;

/*执行长跳转将控制权转交内核*/
/*从header.S的_start全局标号处开始执行*/
jmp_far(seg+0x20, 0);

以下是紧跟全局标号_start的头两个字节的内容:

  1.     .globl    _start  
  2. _start:  
  3.         # Explicitly enter this as bytes, or the assembler  
  4.         # tries to generate a 3-byte jump here, which causes  
  5.         # everything else to push off to the wrong offset.  
  6.   
  7.         /*跳转指令,对应于安装头变量中的jump字段*/  
  8.         .byte    0xeb        # short (2-byte) jump  
  9.         .byte    start_of_setup-1f  
  10.   
  11. 1:  /*标号1*/  
  12.   
  13.     # Part 2 of the header, from the old setup.S  
    .globl    _start
_start:
        # Explicitly enter this as bytes, or the assembler
        # tries to generate a 3-byte jump here, which causes
        # everything else to push off to the wrong offset.

        /*跳转指令,对应于安装头变量中的jump字段*/
        .byte    0xeb        # short (2-byte) jump
        .byte    start_of_setup-1f

1:  /*标号1*/

    # Part 2 of the header, from the old setup.S

这里.byte 0xeb与.byte start_of_setup-1f是汇编指令jmp start_of_setup-1f的硬编码形式,其中的跳转为短转移,因此start_of_setup-1f所在的字节表示偏移量。因为汇编指令经过汇编器的“翻译”之后所形成的均为如上形式的字节码,而CPU本质上并不对数据和指令严格区分,因此可以通过对数据进行精心构造,使其表面上看起来是被处理的数据,但本质上却是可以被用来执行的指令。说到这里还想扯句题外话,我们经常说一个可执行文件被感染了,通常就是指该文件的内部构造被修改了,因为对任何文件都可以进行写操作,所以该可执行文件自然可以由某个不怀好意的进程增加一些新的数据,而这些数据正是被精心构造好的指令,在该文件此后的执行过程中发生的原本不可能存在的一系列操作都是这些数据的“功劳”,而这种方式正是通常所说的“区段注入”,这些被感染的文件自然也就成了所谓的病毒或是木马,另外在某些形式的缓冲区溢出攻击中也利用了这一点。其实这一特点还会催生很多有意思的话题,比如可执行文件对自身进行修改,在执行过程中进行自我进化——有些《黑客帝国》的味道 :-)。

继续回到内核的剖析上来,我们在之前说过上面这两个字节的硬编码执行的是jmp跳转指令的功能,而如果直接写jmp start_of_setup-1f形式的汇编指令,汇编器最终也能生成执行相同功能的字节码,那为什么不直接写汇编指令而偏要费那么大劲,去精心构造这两个字节的数据呢?其实上面的注释已经给出了详细的解答,因为汇编器生成的字节码最终将会占用3个字节,这使得后续的字段都被“推移”到错误的偏移处,所以我们只能通过硬编码的形式来实现跳转。另外在GAS汇编中还有一个重要的知识点,那就是在start_of_setup-1f中的1f并不表示十六进制的数据0x1f,其中的1表示一个标号,紧跟着的f表示向前的(forward),而如果要向后面的标号1跳转,则应该写成1b(b—backward)。.byte start_of_setup-1f这个字节的值表示两个标号——即start_of_setup与1之间的偏移量,由汇编器在汇编过程中自动填充。因为在汇编中的分支及循环语句只能通过jmp及其各种不同的变体来实现,因而跳转指令所跳转到的位置必须赋予一个标号,如果每个标号都取一个具有特定意义的名称将会very painful,因而GNU assembler中的这一特性对于汇编爱好者来说无疑very absorbing。我们在源文件中可以看到其中的大部分标号都仅仅只是一些没有含义的数字,足以说明维护Linux内核代码的这些hackers是十足的懒人 :-)。

接着跳转到start_of_setup标号处:

  1.     .section ".entrytext""ax"  
  2. start_of_setup:  
  3. #ifdef SAFE_RESET_DISK_CONTROLLER   
  4. # Reset the disk controller.   
  5.     movw    $0x0000, %ax        # Reset disk controller  
  6.     movb    $0x80, %dl      # All disks  
  7.     int $0x13  
  8. #endif   
  9.   
  10. # Force %es = %ds  /*强制将ds寄存器的内容赋值给es寄存器*/   
  11.     movw    %ds, %ax  
  12.     movw    %ax, %es  /*注意此时ax寄存器的内容与ds寄存器相同*/  
  13.     cld  /*清除方向标志,使用在串传送指令中,表示在完成传送后将di寄存器自动增加*/  
	.section ".entrytext", "ax"
start_of_setup:
#ifdef SAFE_RESET_DISK_CONTROLLER
# Reset the disk controller.
	movw	$0x0000, %ax		# Reset disk controller
	movb	$0x80, %dl		# All disks
	int	$0x13
#endif

# Force %es = %ds  /*强制将ds寄存器的内容赋值给es寄存器*/
	movw	%ds, %ax
	movw	%ax, %es  /*注意此时ax寄存器的内容与ds寄存器相同*/
	cld  /*清除方向标志,使用在串传送指令中,表示在完成传送后将di寄存器自动增加*/

在start_of_setup标号之后紧跟着的又是一个历史遗留的产物。在上述代码中我们看到如果预定义了宏SAFE_RESET_DISK_CONTROLLER,那么将调用BIOS中断例程0x13重置磁盘控制器。而这仅仅只是针对老式硬盘的代码,目前的硬盘并不需要执行这些指令,留着它仅仅是为了兼容老式硬盘,因此内核文件中并未预定义这个宏。

  1. # Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,   
  2. # which happened to work by accident for the old code.  Recalculate the stack   
  3. # pointer if %ss is invalid.  Otherwise leave it alone, LOADLIN sets up the   
  4. # stack behind its own code, so we can't blindly put it directly past the heap.   
  5.         /*注意之前已将ds寄存器中的值赋予ax寄存器*/  
  6.     movw    %ss, %dx  /*将ss寄存器赋予dx寄存器*/  
  7.     cmpw    %ax, %dx    # %ds == %ss?  /*比较ds寄存器与ss寄存器是否相等*/  
  8.     movw    %sp, %dx  /*将栈指针寄存器sp的值赋给dx寄存器*/  
  9.           
  10.         /*若ds寄存器的值与ss相等则跳转至标号2处,即说明sp寄存器已被合理设置*/  
  11.         je  2f      # -> assume %sp is reasonably set  
  12.   
  13.         /*反之则说明ss寄存器无效,建立一个新的栈*/  
  14.     # Invalid %ss, make up a new stack   
  15.     movw    $_end, %dx  /*将Kernel setup的结束地址装入dx寄存器*/  
  16.     testb   $CAN_USE_HEAP, loadflags  /*位测试操作的结果为真,不发生跳转*/  
  17.     jz  1f  
  18.     movw    heap_end_ptr, %dx  /*heap_end_ptr = _end+STACK_SIZE-512*/  
  19. 1:  addw    $STACK_SIZE, %dx  /*将heap_end_ptr的值加上STACK_SIZE,即栈的大小*/  
  20.     jnc 2f  
  21.     xorw    %dx, %dx    # Prevent wraparound  
# Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,
# which happened to work by accident for the old code.  Recalculate the stack
# pointer if %ss is invalid.  Otherwise leave it alone, LOADLIN sets up the
# stack behind its own code, so we can't blindly put it directly past the heap.
        /*注意之前已将ds寄存器中的值赋予ax寄存器*/
	movw	%ss, %dx  /*将ss寄存器赋予dx寄存器*/
	cmpw	%ax, %dx	# %ds == %ss?  /*比较ds寄存器与ss寄存器是否相等*/
	movw	%sp, %dx  /*将栈指针寄存器sp的值赋给dx寄存器*/
        
        /*若ds寄存器的值与ss相等则跳转至标号2处,即说明sp寄存器已被合理设置*/
        je	2f		# -> assume %sp is reasonably set

        /*反之则说明ss寄存器无效,建立一个新的栈*/
	# Invalid %ss, make up a new stack
	movw	$_end, %dx  /*将Kernel setup的结束地址装入dx寄存器*/
	testb	$CAN_USE_HEAP, loadflags  /*位测试操作的结果为真,不发生跳转*/
	jz	1f
	movw	heap_end_ptr, %dx  /*heap_end_ptr = _end+STACK_SIZE-512*/
1:	addw	$STACK_SIZE, %dx  /*将heap_end_ptr的值加上STACK_SIZE,即栈的大小*/
	jnc	2f
	xorw	%dx, %dx	# Prevent wraparound

首先解释一下注释——有些旧版本的引导加载程序LILO在装载完内核并将控制权转交给内核后,寄存器ss与ds并不相等。并且此时ss寄存器无效,因而需要重新计算栈指针,建立新栈的过程是由上述代码中跳转指令je  2f与标号2之间所执行的指令完成的。建立新栈时首先执行movw  $_end, %dx指令将_end的值赋给dx寄存器,这里_end是在汇编时由汇编器自动填充的,它的值正是Kernel setup与实模式内核代码起始地址的偏移量,由图1中我们可以看出其值最大为0x8000,在arch\x86\boot目录中的链接脚本setup.ld也验证了这一点:

  1. . = ASSERT(_end <= 0x8000, "Setup too big!");  /*第59行*/  
	. = ASSERT(_end <= 0x8000, "Setup too big!");  /*第59行*/

上述语句断言当_end的值大于0x8000时,执行链接时将会报错,提示“Kernel setup太大”。接着执行testb $CAN_USE_HEAP, loadflags指令,测试在loadflags字段中是否已经置位CAN_USE_HEAP所指示的位,这两个操作数在源文件中的定义如下:

  1.       
  2. loadflags:  
  3. LOADED_HIGH = 1         # If set, the kernel is loaded high  
  4. CAN_USE_HEAP    = 0x80      # If set, the loader also has set  
  5.                     # heap_end_ptr to tell how much   
  6.                     # space behind setup.S can be used for   
  7.                     # heap purposes.   
  8.                     # Only the loader knows what is free   
  9.         .byte   LOADED_HIGH  /*被设置为LOADED_HIGH*/  
	
loadflags:
LOADED_HIGH	= 1			# If set, the kernel is loaded high
CAN_USE_HEAP	= 0x80		# If set, the loader also has set
					# heap_end_ptr to tell how much
					# space behind setup.S can be used for
					# heap purposes.
					# Only the loader knows what is free
		.byte	LOADED_HIGH  /*被设置为LOADED_HIGH*/

我们发现loadflags字段的值被设置为LOADED_HIGH,因此由注释看出保护模式下的内核将被加载至起始地址0x10 0000处。然而根据1=0000 0001b可知这个字段的第7位并未被置1,但是其注释同样指出只有在第7位置1时才表示内核会使用堆,那么loadflags字段中的这一位究竟是否会被置1?答案是肯定的,因为Linux内核从启动协议版本号2.01开始,至今一直都支持实模式下的堆,所以根据注释可以猜测正是bootloader在加载内核时将这一位设置成1。于是testb $CAN_USE_HEAP, loadflags指令最后的运算结果为1,从而不发生跳转,随后紧接着执行movw heap_end_ptr, %dx指令,这条指令将heap_end_ptr的值填充dx寄存器,其中heap_end_ptr的值为:

  1. heap_end_ptr:   .word   _end+STACK_SIZE-512  
  2.                     # (Header version 0x0201 or later)   
  3.                     # space from here (exclusive) down to   
  4.                     # end of setup code can be used by setup   
  5.                     # for local heap purposes.  
heap_end_ptr:	.word	_end+STACK_SIZE-512
					# (Header version 0x0201 or later)
					# space from here (exclusive) down to
					# end of setup code can be used by setup
					# for local heap purposes.

heap_end_ptr被设置为_end+STACK_SIZE+512,其中STACK_SIZE的值在arch\x86\boot\Boot.h文件中被定义如下:

  1. #define STACK_SIZE  512 /* Minimum number of bytes for stack */  
#define STACK_SIZE	512	/* Minimum number of bytes for stack */

可以发现heap_end_ptr的值其实就等价于_end,而_end的值为Kernel setup的结束地址。接着执行标号1后的指令addw $STACK_SIZE, %dx,其中STACK_SIZE表示整个栈的大小,在实模式下,512字节的内存被分配给堆和栈同时使用完全足够。接着执行jnc  2f指令,由于标志位没有发生进位,因此直接跳转至标号2处,代码如下:

  1. 2:  # Now %dx should point to the end of our stack space  
  2.     andw    $~3, %dx    # dword align (might as well...)  
  3.     jnz 3f  /*测试条件为真,执行跳转*/  
  4.     movw    $0xfffc, %dx    # Make sure we're not zero  
  5.   
  6. 3:  movw    %ax, %ss  /* 实际执行 movw %ds, %ss */  
  7.     movzwl  %dx, %esp   # Clear upper half of %esp  
  8.           
  9.         /*此时允许中断,该指令与GRUB中的cli对应*/  
  10.         sti            # Now we should have a working stack  
2:	# Now %dx should point to the end of our stack space
	andw	$~3, %dx	# dword align (might as well...)
	jnz	3f  /*测试条件为真,执行跳转*/
	movw	$0xfffc, %dx	# Make sure we're not zero

3:	movw	%ax, %ss  /* 实际执行 movw %ds, %ss */
	movzwl	%dx, %esp	# Clear upper half of %esp
        
        /*此时允许中断,该指令与GRUB中的cli对应*/
        sti			# Now we should have a working stack

首先执行andw $~3, %dx指令将dx寄存器中的最低两位清零,即将栈底地址执行双字对齐操作,使得加载数据的效率更高。之后执行jnz  3f指令,由于上一条指令执行后的结果非零,因此跳转。在标号3后首先执行movw %ax, %ss指令,将ax寄存器赋值给ss,这里注意我们在一开始跳转至start_of_setup标号处执行时,曾将ax寄存器用来暂存ds寄存器的值,而此后的执行过程中该寄存器的值一直都未发生改变,因此这条指令实际是将ds寄存器的值赋值给了ss寄存器。紧接着执行movzwl  %dx, %esp将栈底地址赋值给esp寄存器从而完成堆栈的建立,将栈指针寄存器esp赋值为栈底地址表明初始时刻栈为空。其后的sti指令则打开中断,执行该指令是因为在将控制权转交给实模式下的内核代码之前,GRUB执行了cli()禁用中断的操作,因此完成堆栈的建立之后需要再次打开中断。上图所执行的一系列指令最终可由下图形象的显示:


图3

完成堆栈的建立操作后,还需对cs:eip执行相关的修正操作,具体指令如下所示:

  1. # We will have entered with %cs = %ds+0x20, normalize %cs so   
  2. # it is on par with the other segments.   
  3.     pushw   %ds  
  4.     pushw   $6f  
  5.     lretw  
  6. 6:  
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
	pushw	%ds
	pushw	$6f
	lretw
6:

前两条pushw指令分别将ds寄存器的值以及标号6处的偏移量进行压栈,因为我们已经正确建立了堆栈,因此上述两条指令可以正常工作。此后执行lretw长跳转指令,它将先前压入堆栈的操作数——即ds寄存器以及标号6对应的偏移量分别弹出至cs寄存器和eip寄存器,此后从标号6处继续开始执行。之所以要执行这三条指令,是因为GRUB将内核装载至从地址0x9 0000开始的物理内存后,执行一条长跳转指令jmp_far(seg+0x20, 0)跳过了512字节大小的bootsect,该指令将cs寄存器的值设置为0x9020,而其他的一系列段寄存器ds/es/ss/fs/gs的值均指向起始地址0x9 0000处,因此在执行上述三条指令后将所有的段寄存器的值均设置为0x9000——即执行了前文所说的修正操作。

接着执行标号6之后的指令:

  1. # Check signature at end of setup   
  2.     cmpl    $0x5a5aaa55, setup_sig  
  3.     jne setup_bad  /*测试条件为假,不执行跳转*/  
  4.   
  5. # Zero the bss   
  6.     movw    $__bss_start, %di  /*将bss段的起始地址加载至di寄存器中*/  
  7.     movw    $_end+3, %cx  /*将地址_end+3加载至cx寄存器中*/  
  8.     xorl    %eax, %eax  /*将eax寄存器清零*/  
  9.     subw    %di, %cx  /*将cx寄存器的值减去di寄存器的值,并将结果放入cx寄存器*/  
  10.     shrw    $2, %cx  /*将cx寄存器中的值右移两位,即将cx的值除以4,并将结果存入cx寄存器*/  
  11.     rep; stosl  /*执行串指令操作stosl,将eax中的值保存到es:edi指向的内存中,并且将edi自增4*/  
  12.   
  13. # Jump to C code (should not return)   
  14.     calll   main  /*跳转到main函数中*/  
# Check signature at end of setup
	cmpl	$0x5a5aaa55, setup_sig
	jne	setup_bad  /*测试条件为假,不执行跳转*/

# Zero the bss
	movw	$__bss_start, %di  /*将bss段的起始地址加载至di寄存器中*/
	movw	$_end+3, %cx  /*将地址_end+3加载至cx寄存器中*/
	xorl	%eax, %eax  /*将eax寄存器清零*/
	subw	%di, %cx  /*将cx寄存器的值减去di寄存器的值,并将结果放入cx寄存器*/
	shrw	$2, %cx  /*将cx寄存器中的值右移两位,即将cx的值除以4,并将结果存入cx寄存器*/
	rep; stosl  /*执行串指令操作stosl,将eax中的值保存到es:edi指向的内存中,并且将edi自增4*/

# Jump to C code (should not return)
	calll	main  /*跳转到main函数中*/

首先比较setup_sig的值是否与0x5a5aaa55相等,若不等则跳转至setup_bad标号处,同样setup_sig的值是在链接的时候由链接器填充的,该值同样定义在arch\x86\boot\setup.ld链接脚本文件中:

  1. .signature  : {  
  2.     setup_sig = .;  
  3.     LONG(0x5a5aaa55)  
  4. }  
	.signature	: {
		setup_sig = .;
		LONG(0x5a5aaa55)
	}

可以发现该值确实被定义为0x5a5aaa55,因此jne  setup_bad指令将不会发生跳转。接下来就是清空实模式下内核代码的bss段——该段是Kernel setup的最后一段内存空间,需要注意bss段与数据段的区别:bss段存放的是未初始化的全局变量和静态变量,而数据段存放的则是已初始化的全局变量和静态变量。首先将bss段的起始地址装载至di寄存器中并将eax清零,随后设置带前缀的串指令rep; stosl的循环次数,该循环次数存放在cx寄存器中,这里需要注意的是由于执行一次串指令将清除4个字节的内存空间,因此cx寄存器存放的应该是整个bss段占用的内存空间大小除以4之后的值,由于bss段的大小可能并非4的倍数,而除4之后会将余数舍去,因此为了保证将整个bss段所占内存全部清零,需要将Kernel setup的结束地址_end加3之后再存入寄存器cx中,即执行movw  $_end+3, %cx指令才能正确清空整个bss段,而非movw  $end, %cx。在将整个bss段全部清零之后,即执行call main指令跳转至C语言的main函数中,该函数主要执行一系列硬件检测及初始化操作,至于详细内容放在后文剖析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Hyperledger Fabric是一个开源的区块链平台,可用于搭建溯源系统。在使用Hyperledger Fabric搭建溯源系统之前,需要进行以下步骤: 1. 设置网络:首先,需要定义参与者和组织的角色,创建通道以便参与者之间进行交互。可以使用配置文件定义参与者的访问权限和身份验证策略。网络配置的正确设置非常重要,以确保只有授权的参与者才能参与溯源。 2. 设计链码:链码是在Hyperledger Fabric中运行的智能合约。在溯源系统中,链码用于定义商品的可追溯性规则。可以使用链码设计不同的授权和查询操作,以便在区块链上记录和检索相关数据。链码可以使用Go、Java等编程语言进行开发。 3. 部署链码:部署链码是将链码安装到网络中的各个参与者节点上。通过将链码部署到多个节点,可以确保系统的高可用性和数据冗余,并防止单点故障。链码部署后,可以使用链码的标识符在网络中进行交互。 4. 数据记录和查询:一旦链码部署完成,参与者可以将数据记录到区块链中。在溯源系统中,每次商品的生产、流通或消费都可以被记录为一个或多个交易。通过使用合适的查询操作,可以检索和分析区块链中的数据,以实现商品的溯源。 5. 权限管理和隐私保护:在搭建溯源系统时,需要确保参与者只能访问其需要的数据,并且保护数据的隐私性。Hyperledger Fabric提供了灵活的身份验证和访问控制机制,可以根据需求对参与者进行身份验证,并限制其对数据的访问权限。 通过以上步骤,可以使用Hyperledger Fabric搭建一个可靠、高效的溯源系统。该系统能够确保数据的完整性和不可篡改性,并提供溯源功能,帮助企业追踪和验证商品的生产和流通过程,从而确保商品的品质和安全性。 ### 回答2: Hyperledger Fabric是一个开源的区块链平台,可以用于构建各种应用程序,包括溯源系统。 首先,我们需要搭建一个Hyperledger Fabric网络。Fabric网络由多个节点组成,每个节点都可以执行智能合约并参与区块链交易的验证和记录。部署网络需要设置两个主要组件:网络组织和通道。 网络组织是指参与Fabric网络的实体,可以是具有共同目标或业务关系的组织。每个组织可以拥有一个或多个节点,负责参与链码的执行和交易的验证。我们需要为每个组织生成一个身份证书和私钥,并将其加入到网络中。 通道是一个私有的数据传输通道,用于限制特定组织之间的数据共享。我们可以创建一个或多个通道,并将需要共享数据的组织添加到通道中。只有在同一通道上的组织才能看到和交互共享的数据。 接下来,我们需要定义并部署链码。链码是Fabric中的智能合约,用于定义业务逻辑并执行相关操作。通过编写链码,我们可以实现溯源系统所需的功能,如记录和跟踪产品的来源和流向。链码可以使用Go、Java等编程语言进行开发,并在网络中的节点上进行部署和执行。 在溯源系统中,我们可以使用链码来记录产品的生产信息、交易记录和其他关键数据,确保其真实性和透明度。通过查询链码,我们可以追踪产品的整个供应链,并确保其来源和质量。 最后,我们可以使用Fabric提供的应用程序开发框架构建用户界面和交互功能,以便用户能够方便地查看和操作溯源数据。可以使用Web或移动应用程序开发技术来实现这些功能,确保系统的易用性和用户体验。 总之,通过使用Hyperledger Fabric搭建溯源系统,我们可以建立一个安全、透明和可追溯的供应链管理系统,为消费者提供可靠的产品信息和保障。 ### 回答3: Hyperledger Fabric 是一个用于构建基于区块链的分布式应用程序的开源平台。搭建溯源系统时,使用 Hyperledger Fabric 可以提供以下功能和优势。 首先,Hyperledger Fabric 提供了高度可配置的区块链网络。可以根据实际需求来设计和配置私有链或联盟链,这样可以确保只有授权的参与方可以参与到溯源系统中,提高了数据的隐私性和可信任性。 其次,Hyperledger Fabric 使用了“通道”概念来实现不同参与方之间的数据隔离。在溯源系统中,可能涉及到多个参与方,每个参与方只能看到和访问与自己相关的数据,从而保护了商业敏感信息。 另外,Hyperledger Fabric 提供了智能合约的支持。通过编写智能合约,可以实现溯源系统中的业务规则和逻辑。参与方可以通过智能合约执行特定的功能和操作,确保溯源过程的透明性和可验证性。 此外,Hyperledger Fabric 还提供了跨组织验证和控制机制。在溯源系统中,可能涉及到多个参与方之间的数据共享和验证,使用 Hyperledger Fabric 可以实现这些功能,并保护数据的完整性和安全性。 总之,Hyperledger Fabric 是一个非常适合构建溯源系统的开源平台。它提供了可配置的区块链网络、通道隔离、智能合约支持和跨组织验证等功能,能够满足溯源系统的需求,提供安全、可靠和可扩展的解决方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值