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

在完成控制台初始化之后,可以看到在arch\x86\boot\Main.c文件的main主函数中接着执行if (cmdline_find_option_bool("debug")),这条if判断语句首先调用cmdline_find_option_bool函数在内核命令行中查找"debug"选项,该函数的实现和在系统启动篇(三)[上]一文中剖析过的cmdline_find_option函数非常相似,但前者只需要判断在命令行中是否存在要找的选项,并不需要取出对应的值,因而实现过程较后者更为简单,在这里不再进行详细剖析。若if判断语句中的函数调用返回真,则执行puts("early console in setup code\n"); 语句,反之则跳过继续执行后续代码。因此如果在内核命令行中找到"debug"选项那么将打印出"early console in setup code\n"字符串,而联系到查找的选项名称则不难发现这条信息主要是用来调试内核的。

在裸机状态下向屏幕输出字符
许多初学者在学习编程的时候都是从最经典的"Hello World!"开始的,而如果学习的第一门语言是C的话,那么打印上述字符串的程序最主要的实现语句就是printf("Hello World!"); 其实printf的实现过程无非是首先进行格式解析,然后再将解析后的结果打印至屏幕。对于学习编程已经有一段时间的人来说,格式解析模块的实现只需逻辑上的一些细微处理,但对于如何操作硬件以实现字符的输出却相当不解,而这部分功能的实现其实与puts函数如出一辙,这也正是为什么我在这里详细剖析puts的原因。该函数位于arch\x86\boot\Tty.c文件中:

  1. void __attribute__((section(".inittext"))) puts(const char *str)  /*位于.inittext节区*/  
  2. {  
  3.     while (*str)  
  4.         putchar(*str++);  
  5. }  
void __attribute__((section(".inittext"))) puts(const char *str)  /*位于.inittext节区*/
{
    while (*str)
        putchar(*str++);
}

函数首先检测字符串str中当前所指向的字符是否为'\0',若是则直接退出循环结束整个串的输出,否则调用putchar函数输出当前指向的字符,并将指针值进行自增,而putchar函数的实现位于同样位于arch\x86\boot\Tty.c文件中:

  1. void __attribute__((section(".inittext"))) putchar(int ch)  
  2. {  
  3.     if (ch == '\n')  
  4.         putchar('\r');    /* \n -> \r\n */  
  5.   
  6.     bios_putchar(ch);  
  7.   
  8.     if (early_serial_base != 0)  
  9.         serial_putchar(ch);  
  10. }  
void __attribute__((section(".inittext"))) putchar(int ch)
{
    if (ch == '\n')
        putchar('\r');    /* \n -> \r\n */

    bios_putchar(ch);

    if (early_serial_base != 0)
        serial_putchar(ch);
}

首先判断是否为'\n'字符,若是则先输出'\r',之后再输出'\n'。其中'\r'表示return——指回到当前行的行首,而'\n'则表示next——指移动到下一行,所以其实\r\n连用才表示真正的回车换行。但通常写程序时都只用'\n'表示即可,之所以在这里按照这种方式执行,一种可能的猜测是此时处于文本模式下,对于回车换行必须严格按照相关的协议,正如HTTP中使用\r\n表示一行的结束。此后接着调用bios_putchar(ch)函数输出,顾名思义就是调用BIOS中断输出该字符,这个函数同样被定义在arch\x86\boot\Tty.c文件中:

  1. static void __attribute__((section(".inittext"))) bios_putchar(int ch)  
  2. {  
  3.     struct biosregs ireg;  
  4.   
  5.     initregs(&ireg);  
  6.     ireg.bx = 0x0007;  
  7.     ireg.cx = 0x0001;  
  8.     ireg.ah = 0x0e;  
  9.     ireg.al = ch;  
  10.     intcall(0x10, &ireg, NULL);  
  11. }  
static void __attribute__((section(".inittext"))) bios_putchar(int ch)
{
    struct biosregs ireg;

    initregs(&ireg);
    ireg.bx = 0x0007;
    ireg.cx = 0x0001;
    ireg.ah = 0x0e;
    ireg.al = ch;
    intcall(0x10, &ireg, NULL);
}

在函数内部首先使用结构体类型biosregs定义了一个变量ireg,其中结构体类型biosregs被定义在arch\x86\boot\Boot.h文件中,这里不再将其列出,需要强调的一点是该结构体之所以如此定义,是因为x86采用的是小端法,对于某些可以单独设置低位的寄存器(如eax等通用寄存器),若将其保存在内存中时,其中的低位被放置在内存的低地址空间中,所以对于比如说u32 eax;之类的定义,只有u16 ax, hax才是与其等价的。之后调用initregs函数对变量ireg进行初始化,它被定义在arch\x86\boot\Regs.c文件中:

  1. void initregs(struct biosregs *reg)  
  2. {  
  3.     memset(reg, 0, sizeof *reg);  
  4.     reg->eflags |= X86_EFLAGS_CF;  /* CF标志置1 */  
  5.     reg->ds = ds();  
  6.     reg->es = ds();  
  7.     reg->fs = fs();  
  8.     reg->gs = gs();  
  9. }  
  10.   
  11. /*定义在arch\x86\include\asm\Processor-flags.h文件中*/  
  12. #define X86_EFLAGS_CF    0x00000001 /* Carry Flag */   
void initregs(struct biosregs *reg)
{
    memset(reg, 0, sizeof *reg);
    reg->eflags |= X86_EFLAGS_CF;  /* CF标志置1 */
    reg->ds = ds();
    reg->es = ds();
    reg->fs = fs();
    reg->gs = gs();
}

/*定义在arch\x86\include\asm\Processor-flags.h文件中*/
#define X86_EFLAGS_CF    0x00000001 /* Carry Flag */ 

这个函数无非将eflags字段中的CF标志置1,并将一些段寄存器ds/fs/gs保存在bios_putchar函数定义的ireg变量中。在完成初始化之后回到bios_putchar函数()中,接着设置了一些通用寄存器,而这些寄存器中保存的参数是为后续的BIOS中断所服务的,最后在bios_putchar中调用intcall函数,而这个函数才是真正调用BIOS中断实现了字符的输出,函数的实现被定义在了arch\x86\boot\bioscall.S文件中:

  1. /* 
  2.  * "Glove box" for BIOS calls.  Avoids the constant problems with BIOSes 
  3.  * touching registers they shouldn't be. 
  4.  */  
  5.     
  6. /* 函数原型:void intcall(u8 int_no, const struct biosregs *ireg, struct biosregs *oreg); */  
  7. /*定义在arch\x86\boot\Boot.h文件中*/  
  8.   
  9.     .code16gcc  /*生成运行于实模式中的16位代码,但在与栈相关的指令中仍使用32位字长*/  
  10.     .text    
  11.     .globl    intcall  /*定义全局标号intcall*/  
  12.     .type    intcall, @function  /*类型定义为函数*/  
  13. intcall:  
  14.   
  15.     /* Self-modify the INT instruction.  Ugly, but works. */  
  16.     cmpb    %al, 3f  /*将%al寄存器中的值0x10与标号3处占用的一个字节进行比较*/  
  17.     je    1f  /*若相等则直接跳至标号1处*/  
  18.     movb    %al, 3f  /*若不等则将%al寄存器中的值赋值到标号3所占的一个字节空间*/  
  19.     jmp    1f        /* Synchronize pipeline */  
  20. 1:  
/*
 * "Glove box" for BIOS calls.  Avoids the constant problems with BIOSes
 * touching registers they shouldn't be.
 */
  
/* 函数原型:void intcall(u8 int_no, const struct biosregs *ireg, struct biosregs *oreg); */
/*定义在arch\x86\boot\Boot.h文件中*/

    .code16gcc  /*生成运行于实模式中的16位代码,但在与栈相关的指令中仍使用32位字长*/
    .text  
    .globl    intcall  /*定义全局标号intcall*/
    .type    intcall, @function  /*类型定义为函数*/
intcall:

    /* Self-modify the INT instruction.  Ugly, but works. */
    cmpb    %al, 3f  /*将%al寄存器中的值0x10与标号3处占用的一个字节进行比较*/
    je    1f  /*若相等则直接跳至标号1处*/
    movb    %al, 3f  /*若不等则将%al寄存器中的值赋值到标号3所占的一个字节空间*/
    jmp    1f        /* Synchronize pipeline */
1:

在实现intcall函数的这个文件中,最顶层的注释提示这是为BIOS调用专门制作的"手套箱"(Glove box)——还没见识过这种东西,不过听名字就感觉很有趣,我们会在后续的剖析过程中发现intcall这个函数的实现确实如同其注释一般有意思。不难看出,这段代码是由汇编所实现的,而它则是由C函数所调用的,这种混合编程中最需要关注的一点就是函数参数和返回值的传递问题。在默认情况中gcc使用栈来传递参数,并且压栈的顺序是从右往左依次入栈,而返回值则保存在%eax寄存器中,而在gcc的扩展中则可以使用附加属性__attribute__(regparm(n))指定使用寄存器进行传参,其中的n表示参数的个数,一般来说n的值不能大于3,在这种情况下参数从左往右依次被传入%eax, %edx, %ecx寄存器,而返回值则仍旧保存在%eax寄存器中。

然而在intcall函数的声明中我们并未发现其采用了附加属性__attribute__(regparm(3)),这是因为在编译Linux内核时直接在gcc命令行中附加了"-mregparm=3"这个选项——其作用等同于前述的附加属性。这样根据bios_putchar中的调用形式intcall(0x10, &ireg, NULL);我们便可以知道在进入intcall函数之前,%eax寄存器存放了常量0x10,%edx寄存器则保存指向临时变量ireg的指针,最后%ecx被赋值为0。所以在cmpb %al, 3f指令中此时%al的值为0x10,在进入标号1之前首先执行这些指令的用途是为了正确设置好中断调用号。

  1.     /* Save state */  
  2.     pushfl  /*将状态寄存器eflags压栈*/  
  3.     pushw    %fs  /*将fs寄存器压栈*/  
  4.     pushw    %gs  /*将gs压栈*/  
  5.     pushal  /*保存通用寄存器中的上下文环境*/  
  6. /* 
  7. 在pushal指令中各寄存器的入栈顺序分别为: 
  8. %eax->%ecx->%edx->%ebx->%esp->%ebp->%esi->%edi 
  9. 总共占用4*8=32字节 
  10. */  
  11.   
  12.     /* Copy input state to stack frame */  
  13.     /* 将输入状态(ireg)拷贝至栈帧 */  
  14.     subw    $44, %sp  /*首先分配44字节的栈空间*/  
  15.     movw    %dx, %si  /*%dx寄存器中存放参数ireg的指针,将其赋值给源变址%si*/  
  16.     movw    %sp, %di  /*%sp指向当前栈顶,将其赋值给目的寄存器%di*/  
  17.     movw    $11, %cx  /*将%cx寄存器的值赋为11,表示循环次数*/  
  18.     rep; movsd  /*串指令movsd将地址ds:[esi]处的数据块拷贝至es:[edi]*/  
    /* Save state */
    pushfl  /*将状态寄存器eflags压栈*/
    pushw    %fs  /*将fs寄存器压栈*/
    pushw    %gs  /*将gs压栈*/
    pushal  /*保存通用寄存器中的上下文环境*/
/*
在pushal指令中各寄存器的入栈顺序分别为:
%eax->%ecx->%edx->%ebx->%esp->%ebp->%esi->%edi
总共占用4*8=32字节
*/

    /* Copy input state to stack frame */
    /* 将输入状态(ireg)拷贝至栈帧 */
    subw    $44, %sp  /*首先分配44字节的栈空间*/
    movw    %dx, %si  /*%dx寄存器中存放参数ireg的指针,将其赋值给源变址%si*/
    movw    %sp, %di  /*%sp指向当前栈顶,将其赋值给目的寄存器%di*/
    movw    $11, %cx  /*将%cx寄存器的值赋为11,表示循环次数*/
    rep; movsd  /*串指令movsd将地址ds:[esi]处的数据块拷贝至es:[edi]*/

在标号1之后首先将eflags、fs、gs以及各个通用寄存器压栈以保存上下文环境,之后将%sp寄存器减去立即数44,申请的这44个字节的栈空间主要用来存放输入状态ireg,可以在arch\x86\boot\Boot.h文件中看到biosregs结构体类型的定义,由这个自定义类型所定义的变量ireg确实占用了44个字节的内存空间——包括8个通用寄存器、4个段寄存器以及1个eflags寄存器,这里不再将该自定义类型列出。之后设置好源变址%si和目的变址%di以及循环计数器%cx之后,执行串指令rep;movsd将ireg所在内存拷贝至当前栈空间中,由于movsd串指令一次移动4个字节,故循环计数器设置为44/4=11。此时这44个字节的栈空间的内存布局如下:


图1

接着如下执行:

  1.     /* Pop full state from the stack */  
  2.     /* 将当前栈中ireg保存的所有状态弹出 */  
  3.     popal    
  4. /* 
  5. 在popal指令中弹出顺序与pushal入栈顺序相反: 
  6. %edi->%esi->%ebp->%esp->%ebx->%edx->%ecx->%eax 
  7. */  
  8.     popw    %gs  /*弹出%gs寄存器,下同*/  
  9.     popw    %fs  
  10.     popw    %es  
  11.     popw    %ds  
  12.     popfl  /*弹出至eflags寄存器*/  
    /* Pop full state from the stack */
    /* 将当前栈中ireg保存的所有状态弹出 */
    popal  
/*
在popal指令中弹出顺序与pushal入栈顺序相反:
%edi->%esi->%ebp->%esp->%ebx->%edx->%ecx->%eax
*/
    popw    %gs  /*弹出%gs寄存器,下同*/
    popw    %fs
    popw    %es
    popw    %ds
    popfl  /*弹出至eflags寄存器*/

可以看到上述指令已经将在ireg变量中设置好的一系列参数分别弹出至对应的寄存器,因而此时%esp寄存器的当前指向位于图1中的eflags寄存器之后,那么接下来很自然地就是使用int指令调用BIOS中断了,如下:

  1.     /* Actual INT */  
  2.     .byte    0xcd        /* INT opcode */  
  3. 3:   /*标号3处*/  
  4.     .byte    0  
    /* Actual INT */
    .byte    0xcd        /* INT opcode */
3:   /*标号3处*/
    .byte    0

在标号3之前的一个字节用于存放0xcd——这对应于int指令的硬编码形式,紧接着在该字节之后存入一个立即数,用于指明需要调用的中断向量号,这个值正是在刚进入intcall函数之后通过movb %al, 3f指令()传入的,因为所有的标号最后都将被翻译成地址,因此可以使用movb指令将寄存器中的值移入标号所对应的内存空间。由传入该函数的中断向量号可知,调用的是0x10号中断,这是一类专门提供视频显示服务的中断,有关这类中断的详细内容可以参考这里,根据传入%ah寄存器的功能号为0x0e以及%bl寄存器中的前景色为0x07可知,最后在%al寄存器中的待输出字符以浅灰色的形式出现在了屏幕上,此外虽然%bh寄存器被用于指定页号(Page Number),但并没有相关的详细资料对其进行解释,并且它通常都被设置为0,所以我们在这里就不去深究了。

其实这是一种很“奇葩”的赋值方式,相信很多童鞋都没有这么玩过,正如在先前的intcall标号之后所带的一段注释中指出的——"Ugly, but works."那么为什么要这么做呢?这是因为在调用intcall函数时直接使用了%eax寄存器传递中断向量号,而在执行这一中断时%eax寄存器又必须被设置成某些固定的参数,比如将%al寄存器设定为需要输出的字符,于是通过这种比较“奇葩”的方式将参数存入到指定的内存空间,也就免去了执行一些额外指令的需要。

  1.     /* Push full state to the stack */  
  2.     pushfl  /* 将eflags寄存器压栈 */  
  3.     pushw    %ds  /* 将%ds寄存器压栈,下同 */  
  4.     pushw    %es  
  5.     pushw    %fs  
  6.     pushw    %gs  
  7.     pushal  /* 将32位通用寄存器按指定顺序压栈 */  
    /* Push full state to the stack */
    pushfl  /* 将eflags寄存器压栈 */
    pushw    %ds  /* 将%ds寄存器压栈,下同 */
    pushw    %es
    pushw    %fs
    pushw    %gs
    pushal  /* 将32位通用寄存器按指定顺序压栈 */

注意在执行完BIOS中断后,处理器的状态也随之发生了改变。可以看到接下来所执行的一系列指令都是压栈操作,而之所以需要将这些关键的寄存器保存在栈中,是为了将这些值返回给传入intcall函数的输出参数中。再次声明一下这个函数的原型为:void intcall(u8 int_no, const struct biosregs *ireg, struct biosregs *oreg); 其中int_no为中断向量号,ireg为输入参数,oreg为输出参数,而后两者均为自定义结构体类型biosregs定义的变量,这也就是为什么在执行完BIOS中断后,寄存器压栈顺序和先前的弹出顺序正好相反。接着往下执行:

  1.     /* Re-establish C environment invariants */  
  2.     cld  /* 将eflags寄存器中的方向标志位清零 */  
  3.     movzwl    %sp, %esp  /* 将%esp寄存器的高16位补零 */  
  4.     movw    %cs, %ax  
  5.     movw    %ax, %ds  /* 将%cs段寄存器移入%ds以及%es寄存器 */  
  6.     movw    %ax, %es  
    /* Re-establish C environment invariants */
    cld  /* 将eflags寄存器中的方向标志位清零 */
    movzwl    %sp, %esp  /* 将%esp寄存器的高16位补零 */
    movw    %cs, %ax
    movw    %ax, %ds  /* 将%cs段寄存器移入%ds以及%es寄存器 */
    movw    %ax, %es

从这些指令前的注释可以看出,它们的用途就是为了重新恢复C语言的运行环境,这是因为之后仍将回到C语言的运行模式,而在执行BIOS中断的过程中有可能将%ds以及%es的值改变,因为其中的数据段寄存器%ds一旦出现偏差,那么在C语言中执行与全局变量相关的语句时将发生错误的内存引用——这将导致重大的灾难。而%cs段寄存器则不会发生任何改变,又因为在先前的执行过程中,6个段寄存器cs/ds/es/fs/gs/ss始终被设置为相同的值0x9000(原因参考系统启动篇(二)一文),所以通过将%cs段寄存器中的值重新移入%ds以及%es就完成了正确的修正。此外在先前还执行了cld指令,这条指令将清除eflags寄存器中的DF标志,从而在执行带前缀rep的串指令时,源变址%si以及目的变址%di都将自动递增。

  1.     /* Copy output state from stack frame */  
  2.     movw    68(%esp), %di    /* Original %cx == 3rd argument */  
  3.     andw    %di, %di  /*将%di寄存器执行逐位与运算*/  
  4.     jz    4f  /*若结果为0则跳转至标号4处,由于传入的参数oreg被设置为NULL,故发生跳转*/  
  5.     movw    %sp, %si  
  6.     movw    $11, %cx  
  7.     rep; movsd  
  8. 4:   
  9.     addw    $44, %sp  /*销毁44字节的栈空间*/  
    /* Copy output state from stack frame */
    movw    68(%esp), %di    /* Original %cx == 3rd argument */
    andw    %di, %di  /*将%di寄存器执行逐位与运算*/
    jz    4f  /*若结果为0则跳转至标号4处,由于传入的参数oreg被设置为NULL,故发生跳转*/
    movw    %sp, %si
    movw    $11, %cx
    rep; movsd
4: 
    addw    $44, %sp  /*销毁44字节的栈空间*/

因为将输入状态ireg拷贝至对应的栈空间之前已经保存好了当前寄存器的上下文,这其中就包括通过%ecx寄存器传入intcall函数的输出参数oreg,而在将ireg存放在44字节的栈空间后又将其弹出至对应的寄存器,此后这部分空间又用来存放了执行完BIOS中断之后的处理器状态,所以输出参数oreg所在的地址相对于%esp寄存器来说并未发生改变。因为它通过执行pushal指令与其他寄存器按照严格约定的顺序压栈,所以在%ecx寄存器之后,从高地址向低地址依次保存的寄存器为%edx->%ebx->%esp->%ebp->%esi->%edi,因而%ecx和%esp寄存器的当前指向相差了44+6*4=68个字节的内存空间,于是movw 68(%esp), %di指令正如其注释所指出的那样,是将输出参数oreg的值赋给%di寄存器。之后对参数oreg进行测试,判断其是否为0,若不是,那么就将上述44个代表处理器状态的字节拷贝至变量oreg所在的内存空间,不过可以看到在bios_putchar函数()中,调用语句intcall(0x10, &ireg, NULL);传入的输出参数为NULL,所以必然直接跳转至标号4处。

  1.     /* Restore state and return */  
  2.     popal  
  3.     popw    %gs  
  4.     popw    %fs  
  5.     popfl  
  6.     retl  
    /* Restore state and return */
    popal
    popw    %gs
    popw    %fs
    popfl
    retl

此后就是执行一系列弹出指令恢复上下文环境,这里的上下文指的是在执行BIOS中断之前对应的处理器状态,并且弹出顺序与入栈顺序正好相反,随后继续执行ret指令返回到bios_putchar函数中,由这个函数的实现不难发现此时它也已经执行完了。于是紧接着再回到putchar函数()中,判断early_serial_base其值是否为0,而我们在系统启动篇(三)[下]一文中看到它已被正确设置为串行端口,因此必将执行serial_putchar(ch);语句。函数serial_putchar同样被定义在arch\x86\boot\Tty.c文件中:

  1. static void serial_putchar(int ch)  
  2. {  
  3.     unsigned timeout = 0xffff;  /*设置超时间隔*/  
  4.   
  5.     while ((inb(early_serial_base + LSR) & XMTRDY) == 0 && --timeout)  
  6.         cpu_relax();  
  7.   
  8.     outb(ch, early_serial_base + TXR);  
  9. }  
  10.   
  11. /*定义在arch\x86\boot\Tty.c文件中*/  
  12. #define XMTRDY          0x20   
  13.   
  14. /*定义在arch\x86\boot\Boot.h文件中*/  
  15. #define cpu_relax()    asm volatile("rep; nop")  
static void serial_putchar(int ch)
{
    unsigned timeout = 0xffff;  /*设置超时间隔*/

    while ((inb(early_serial_base + LSR) & XMTRDY) == 0 && --timeout)
        cpu_relax();

    outb(ch, early_serial_base + TXR);
}

/*定义在arch\x86\boot\Tty.c文件中*/
#define XMTRDY          0x20

/*定义在arch\x86\boot\Boot.h文件中*/
#define cpu_relax()    asm volatile("rep; nop")

在serial_putchar函数中首先设置了超时间隔timeout,随后进入while循环,通过执行inb(early_serila_base+LSR)指令读取行状态[Line Status Register, LSR]寄存器(见系统启动篇(三)[下]一文图6),并测试第5位(0x20=0010 0000b)是否为0,以及timeout是否已被递减至0,如果这两者都满足,那么通过调用cpu_relax简单地插入一些气泡。根据上文中LSR各个位的解释可知,LSR第5位被用于判断接收装置持有寄存器(Transmitter holding register)是否为空,若是则该位置1,表示UART芯片可接收下一个字节的数据。因此整个循环的作用,就是不停地测试UART是否已将前一个数据输出,直至持有寄存器可用或者发生超时,此时退出循环并执行outb(ch, early_serial_base+TXR);语句将字符ch通过串行端口输出至外设。

执行完serial_putchar函数之后,也就将退出puts函数回到main主函数中。纵观整个执行过程,我们发现将字符输出至屏幕的方式无非就是通过bios中断和串口通信的方式来实现的,但细想之下,其实会发现这里有一个无法解释的问题:既然已经通过bios中断实现了字符的输出,为何又要涉及到串口通信,这样不就将字符进行了两次输出吗?当然在系统的执行过程中,针对某个指定的字符串,我们只可能在屏幕上看到它们被输出一次,那么将字符传送至串口的作用又是什么?对于这个问题有独到见解的童鞋不妨谈下自己的看法。

初始化堆
在main函数中随后init_heap函数检查内核在初始化阶段所使用的堆,这个函数被定义在文件arch\x86\boot\Main.c文件中:

  1. static void init_heap(void)  
  2. {  
  3.     char *stack_end;  
  4.   
  5.     if (boot_params.hdr.loadflags & CAN_USE_HEAP) {  /* CAN_USE_HEAP = 0x80 */  
  6.   
  7.         /* leal -STACK_SIZE(%esp), stack_end */  
  8.         asm("leal %P1(%%esp),%0"  
  9.             : "=r" (stack_end) : "i" (-STACK_SIZE));  
  10.   
  11.         heap_end = (char *)  /* heap_end为全局变量,表示堆顶 */  
  12.             ((size_t)boot_params.hdr.heap_end_ptr + 0x200);  
  13.         if (heap_end > stack_end)  /* 判断堆顶heap_end是否大于栈顶stack_end,若成立则表示堆和栈发生重叠 */  
  14.             heap_end = stack_end;  /* 而后将stack_end赋给堆顶,表示减小整个堆的大小 */  
  15.     } else {  
  16.         /* Boot protocol 2.00 only, no heap available */  
  17.         puts("WARNING: Ancient bootloader, some functionality "  
  18.              "may be limited!\n");  
  19.     }  
  20. }  
  21.   
  22. /*定义在arch\x86\boot\Boot.h文件中:*/  
  23. #define STACK_SIZE    512    /* Minimum number of bytes for stack */  
static void init_heap(void)
{
    char *stack_end;

    if (boot_params.hdr.loadflags & CAN_USE_HEAP) {  /* CAN_USE_HEAP = 0x80 */

        /* leal -STACK_SIZE(%esp), stack_end */
        asm("leal %P1(%%esp),%0"
            : "=r" (stack_end) : "i" (-STACK_SIZE));

        heap_end = (char *)  /* heap_end为全局变量,表示堆顶 */
            ((size_t)boot_params.hdr.heap_end_ptr + 0x200);
        if (heap_end > stack_end)  /* 判断堆顶heap_end是否大于栈顶stack_end,若成立则表示堆和栈发生重叠 */
            heap_end = stack_end;  /* 而后将stack_end赋给堆顶,表示减小整个堆的大小 */
    } else {
        /* Boot protocol 2.00 only, no heap available */
        puts("WARNING: Ancient bootloader, some functionality "
             "may be limited!\n");
    }
}

/*定义在arch\x86\boot\Boot.h文件中:*/
#define STACK_SIZE    512    /* Minimum number of bytes for stack */

首先定义了一个临时变量stack_end,随后执行if判断语句,其实从else分支语句块的注释中不难看出,仅仅在启动协议为2.00时才不支持堆,而当前版本的Linux内核的启动协议已经达到2.10,所以肯定不会进入else分支执行,这也就是说loadflags中的第7位肯定会被置位,于是判断语句中的表达式的值为真。随后执行一条内联汇编语句,将%esp-STACK_SIZE所得结果赋给事先定义的临时变量stack_end,其中%esp的值表示当前使用栈的栈底,而STACK_SIZE表示栈的大小,那么stack_end的值自然就表示当前栈的栈顶。接着将hdr头变量中的heap_end_ptr字段加上0x200,回忆一下,heap_end_ptr在arch\x86\boot\header.S文件中被定义为_end+STACK_SIZE-512=_end,也就是堆的起始地址,其后的0x200表示整个堆的大小,于是全局变量heap_end就表示堆的顶部。随后做适当的调整工作以防止这两者相互重叠,从而完成了堆的初始化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值