探本溯源——深入領略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就表示堆的頂部。隨後做適當的調整工作以防止這兩者相互重疊,從而完成了堆的初始化。

發佈了6 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章