MIT 6.828 JOS學習筆記5. Exercise 1.3


Lab 1 Exercise 3

 設置一個斷點在地址0x7c00處,這是boot sector被加載的位置。然后讓程序繼續運行直到這個斷點。跟蹤/boot/boot.S文件的每一條指令,同時使用boot.S文件和系統為你反匯編出來的文件obj/boot/boot.asm。你也可以使用GDB的x/i指令來獲取去任意一個機器指令的反匯編指令,把源文件boot.S文件和boot.asm文件以及在GDB反匯編出來的指令進行比較。

   追蹤到bootmain函數中,而且還要具體追蹤到readsect()子函數里面。找出和readsect()c語言程序的每一條語句所對應的匯編指令,回到bootmain(),然后找出把內核文件從磁盤讀取到內存的那個for循環所對應的匯編語句。找出當循環結束后會執行哪條語句,在那里設置斷點,繼續運行到斷點,然后運行完所有的剩下的語句。

 答:

  下面我們將分別分析一下這道練習中所涉及到的兩個重要文件,它們一起組成了boot loader。分別是/boot/boot.S/boot/main.c文件。其中前者是一個匯編文件,后者是一個C語言文件。當BIOS運行完成之后,CPU的控制權就會轉移到boot.S文件上。所以我們首先看一下boot.S文件。

  /boot/boot.S:

1 .globl start
2 start:
3   .code16                # Assemble for 16-bit mode
4   cli                    # Disable interrupts

  這幾條指令就是boot.S最開始的幾句,其中cli是boot.S,也是boot loader的第一條指令。這條指令用於把所有的中斷都關閉。因為在BIOS運行期間有可能打開了中斷。此時CPU工作在實模式下。

5  cld                         # String operations increment

  這條指令用於指定之后發生的串處理操作的指針移動方向。在這里現在對它大致了解就夠了。

6  # Set up the important data segment registers (DS, ES, SS).
7  xorw    %ax,%ax             # Segment number zero
8  movw    %ax,%ds             # -> Data Segment
9  movw    %ax,%es             # -> Extra Segment
10 movw    %ax,%ss             # -> Stack Segment

  這幾條命令主要是在把三個段寄存器,ds,es,ss全部清零,因為經歷了BIOS,操作系統不能保證這三個寄存器中存放的是什么數。所以這也是為后面進入保護模式做准備。

 

11  # Enable A20:
12  #   For backwards compatibility with the earliest PCs, physical
13  #   address line 20 is tied low, so that addresses higher than
14  #   1MB wrap around to zero by default.  This code undoes this.
15 seta20.1:
16  inb     $0x64,%al               # Wait for not busy
17  testb   $0x2,%al
18  jnz     seta20.1

19  movb    $0xd1,%al               # 0xd1 -> port 0x64
20  outb    %al,$0x64

21 seta20.2:
22  inb     $0x64,%al               # Wait for not busy
23  testb   $0x2,%al
24  jnz     seta20.2

25  movb    $0xdf,%al               # 0xdf -> port 0x60
26  outb    %al,$0x60

  這部分指令就是在准備把CPU的工作模式從實模式轉換為保護模式。我們可以看到其中的指令包括inb,outb這樣的IO端口命令。所以這些指令都是在對外部設備進行操作。根據下面的鏈接:

   http://bochs.sourceforge.net/techspec/PORTS.LST

  我們可以查看到,0x64端口屬於鍵盤控制器804x,名稱是控制器讀取狀態寄存器。下面是它各個位的含義。

  

  所以16~18號指令是在不斷的檢測bit1。bit1的值代表輸入緩沖區是否滿了,也就是說CPU傳送給控制器的數據,控制器是否已經取走了,如果CPU想向控制器傳送新的數據的話,必須先保證這一位為0。所以這三條指令會一直等待這一位變為0,才能繼續向后運行。

  當0x64端口准備好讀入數據后,現在就可以寫入數據了,所以19~20這兩條指令是把0xd1這條數據寫入到0x64端口中。當向0x64端口寫入數據時,則代表向鍵盤控制器804x發送指令。這個指令將會被送給0x60端口。

  

  通過圖中可見,D1指令代表下一次寫入0x60端口的數據將被寫入給804x控制器的輸出端口。可以理解為下一個寫入0x60端口的數據是一個控制指令。

  然后21~24號指令又開始再次等待,等待剛剛寫入的指令D1,是否已經被讀取了。

  如果指令被讀取了,25~26號指令會向控制器輸入新的指令,0xdf。通過查詢我們看到0xDF指令的含義如下

  

  這個指令的含義可以從圖中看到,使能A20線,代表可以進入保護模式了。

 

27   # Switch from real to protected mode, using a bootstrap GDT
28   # and segment translation that makes virtual addresses 
29   # identical to their physical addresses, so that the 
30   # effective memory map does not change during the switch.
31   lgdt    gdtdesc
32   movl    %cr0, %eax
33   orl     $CR0_PE_ON, %eax
34   movl    %eax, %cr0

  首先31號指令 lgdt gdtdesc,是把gdtdesc這個標識符的值送入全局映射描述符表寄存器GDTR中。這個GDT表是處理器工作於保護模式下一個非常重要的表。具體可以參照我們的Appendix 1關於實模式和保護模式的介紹。至於這條指令的功能就是把關於GDT表的一些重要信息存放到CPU的GDTR寄存器中,其中包括GDT表的內存起始地址,以及GDT表的長度。這個寄存器由48位組成,其中低16位表示該表長度,高32位表該表在內存中的起始地址。所以gdtdesc是一個標識符,標識着一個內存地址。從這個內存地址開始之后的6個字節中存放着GDT表的長度和起始地址。我們可以在這個文件的末尾看到gdtdesc,如下:

 1 # Bootstrap GDT
 2 .p2align 2                               # force 4 byte alignment
 3 gdt:
 4   SEG_NULL                               # null seg
 5   SEG(STA_X|STA_R, 0x0, 0xffffffff)      # code seg
 6   SEG(STA_W, 0x0, 0xffffffff)            # data seg
 7 
 8 gdtdesc:
 9   .word   0x17                           # sizeof(gdt) - 1
10   .long   gdt                            # address gdt

  其中第3行的gdt是一個標識符,標識從這里開始就是GDT表了。可見這個GDT表中包括三個表項(4,5,6行),分別代表三個段,null seg,code seg,data seg。由於xv6其實並沒有使用分段機制,也就是說數據和代碼都是寫在一起的,所以數據段和代碼段的起始地址都是0x0,大小都是0xffffffff=4GB。

  在第4~6行是調用SEG()子程序來構造GDT表項的。這個子函數定義在mmu.h中,形式如下:  

 #define SEG(type,base,lim)                    \
                    .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);    \
                    .byte (((base) >> 16) & 0xff), (0x90 | (type)),        \
                    (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

   可見函數需要3個參數,一是type即這個段的訪問權限,二是base,這個段的起始地址,三是lim,即這個段的大小界限。gdt表中的每一個表項的結構如圖所示:

  

   每個表項一共8字節,其中limit_low就是limit的低16位。base_low就是base的低16位,依次類推,所以我們就可以理解SEG函數為什么要那么寫(其實還是有很多不理解的。。)。

   然后在gdtdesc處就要存放這個GDT表的信息了,其中0x17是這個表的大小-1 = 0x17 = 23,至於為什么不直接存表的大小24,根據查詢是官方規定的。緊接着就是這個表的起始地址gdt。

 

27   # Switch from real to protected mode, using a bootstrap GDT
28   # and segment translation that makes virtual addresses 
29   # identical to their physical addresses, so that the 
30   # effective memory map does not change during the switch.
31   lgdt    gdtdesc
32   movl    %cr0, %eax
33   orl     $CR0_PE_ON, %eax
34   movl    %eax, %cr0

  再回到剛才那里,當加載完GDT表的信息到GDTR寄存器之后。緊跟着3個操作,32~34指令。 這幾步操作明顯是在修改CR0寄存器的內容。CR0寄存器還有CR1~CR3寄存器都是80x86的控制寄存器。其中$CR0_PE的值定義於"mmu.h"文件中,為0x00000001。可見上面的操作是把CR0寄存器的bit0置1,CR0寄存器的bit0是保護模式啟動位,把這一位值1代表保護模式啟動。

35  ljmp    $PROT_MODE_CSEG, $protcseg

  這只是一個簡單的跳轉指令,這條指令的目的在於把當前的運行模式切換成32位地址模式

protcseg:
  # Set up the protected-mode data segment registers
36  movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
37  movw    %ax, %ds                # -> DS: Data Segment
38  movw    %ax, %es                # -> ES: Extra Segment
39  movw    %ax, %fs                # -> FS
40  movw    %ax, %gs                # -> GS
41  movw    %ax, %ss                # -> SS: Stack Segment

    修改這些寄存器的值。這些寄存器都是段寄存器。大家可以戳這個鏈接看一下具體介紹 http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html

   這里的23~29步之所以這么做是按照規定來的,https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table鏈接中指出,如果剛剛加載完GDTR寄存器我們必須要重新加載所有的段寄存器的值,而其中CS段寄存器必須通過長跳轉指令,即23號指令來進行加載。所以這些步驟是在第19步完成后必須要做的。這樣才能是GDTR的值生效。

 

# Set up the stack pointer and call into C.
42  movl    $start, %esp
43  call bootmain

  接下來的指令就是要設置當前的esp寄存器的值,然后准備正式跳轉到main.c文件中的bootmain函數處。我們接下來分析一下這個函數的每一條指令:

// read 1st page off disk
1 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

  這里面調用了一個函數readseg,這個函數在bootmain之后被定義了:

    void readseg(uchar *pa, uint count, uint offset);

  它的功能從注釋上來理解應該是,把距離內核起始地址offset個偏移量存儲單元作為起始,將它和它之后的count字節的數據讀出送入以pa為起始地址的內存物理地址處。

  所以這條指令是把內核的第一個頁(4MB = 4096 = SECTSIZE*8 = 512*8)的內容讀取的內存地址ELFHDR(0x10000)處。其實完成這些后相當於把操作系統映像文件的elf頭部讀取出來放入內存中。

  讀取完這個內核的elf頭部信息后,需要對這個elf頭部信息進行驗證,並且也需要通過它獲取一些重要信息。所以有必要了解下elf頭部。


  注: http://wiki.osdev.org/ELF

    elf文件:elf是一種文件格式,主要被用來把程序存放到磁盤上。是在程序被編譯和鏈接后被創建出來的。一個elf文件包括多個段。對於一個可執行程序,通常包含存放代碼的文本段(text section),存放全局變量的data段,存放字符串常量的rodata段。elf文件的頭部就是用來描述這個elf文件如何在存儲器中存儲。
    需要注意的是,你的文件是可鏈接文件還是可執行文件,會有不同的elf頭部格式。

 

  

2 if (ELFHDR->e_magic != ELF_MAGIC)
3        goto bad;

  elf頭部信息的magic字段是整個頭部信息的開端。並且如果這個文件是格式是ELF格式的話,文件的elf->magic域應該是=ELF_MAGIC的,所以這條語句就是判斷這個輸入文件是否是合法的elf可執行文件。

 

4 ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);

  我們知道頭部中一定包含Program Header Table。這個表格存放着程序中所有段的信息。通過這個表我們才能找到要執行的代碼段,數據段等等。所以我們要先獲得這個表。

  這條指令就可以完成這一點,首先elf是表頭起址,而phoff字段代表Program Header Table距離表頭的偏移量。所以ph可以被指定為Program Header Table表頭。

 

5 eph = ph + ELFHDR->e_phnum;

   由於phnum中存放的是Program Header Table表中表項的個數,即段的個數。所以這步操作是吧eph指向該表末尾。

6 for (; ph < eph; ph++)
    // p_pa is the load address of this segment (as well
    // as the physical address)
7    readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

   這個for循環就是在加載所有的段到內存中。ph->paddr根據參考文獻中的說法指的是這個段在內存中的物理地址。ph->off字段指的是這一段的開頭相對於這個elf文件的開頭的偏移量。ph->filesz字段指的是這個段在elf文件中的大小。ph->memsz則指的是這個段被實際裝入內存后的大小。通常來說memsz一定大於等於filesz,因為段在文件中時許多未定義的變量並沒有分配空間給它們。

   所以這個循環就是在把操作系統內核的各個段從外存讀入內存中。

 

8 ((void (*)(void)) (ELFHDR->e_entry))();

  e_entry字段指向的是這個文件的執行入口地址。所以這里相當於開始運行這個文件。也就是內核文件。 自此就把控制權從boot loader轉交給了操作系統的內核。

   

    以上就是我們對這兩個文件的分析。下面我們來完成Exercise要求我們做的事情:

   

    首先完成第一部分,即對boot.S程序的跟蹤,我們還是要采用gdb。首先打開一個terminal,並且cd到xv6的根目錄下,輸入make qemu-gdb。然后再打開一個terminal也來到xv6的根目錄下,輸入gdb,我們就可以開始調試了。

    首先要設置一個斷點,設置到boot.S程序開始運行的地方,因為我們之前已經介紹過,BIOS會把boot sector復制到0x7c00地址處,所以boot.S的起始運行地址就是0x7c00.

    所以我們在gdb窗口中輸入 b *0x7c00,然后再輸入c,表示繼續運行到斷點處,在這里我們輸入

   x/30i 0x7c00

  顯示如下:

    

   這條gdb指令是把存放在0x7c00以及之后30字節的內存里面的指令反匯編出來,我們可以拿它直接和boot.S以及在obj/boot/boot.asm進行比較,如下:

 首先是obj/boot/boot.asm

 

 然后是boot.S

 

 可見這三者在指令上沒有區別,只不過在源代碼中,我們指定了很多標識符比如set20.1,.start,這些標識符在被匯編成機器代碼后都會被轉換成真實物理地址。比如set20.1就被轉換為0x7c0a,那么在obj/boot/boot.asm中還把這種對應關系列出來了,但是在真實執行時,即第一種情況中,就看不到set20.1標識符了,完全是真實物理地址。

   緊接着完成Exercise的第二部分:

 我們可以對bootmain函數中的語句逐一分析:

 首先在boot.S中的最后一句是
        0. call bootmain
   call指令將會把返回地址壓入棧中,然后把bootmain的起始地址賦給%eip寄存器的值,所以在這句指令執行后%eip 的值變為 0x7d0b

 因為我們從obj/boot/boot.asm 文件中,正好可以看到bootmain函數的匯編形式:

 

  可見bootmain翻譯成匯編程序的第一條指令是push %ebp,地址為0x7d0b。所以正好和%eip的值對上。 另外%esp寄存器中的值由0x7c00,變為0x7bfc。其中0x7bfd~0x7c00中存放的值是boot.S運行時的返回地址。

  然后我們開始正式分析bootmain函數,上圖中的四句匯編代碼是進行過程調用時,被調過程必須要事先執行的一些通用的任務。

  關於匯編語言過程調用的知識,可以閱讀Apendix 2
        1 0x7d0b  push  %esp
        2 0x7d0c  mov  %esp, %ebp
  這兩句就是在修改棧幀界限的信息。閱讀Appdenix 2,里面具體解釋了這兩句的含義。
        3. 0x7d0e  push  %esi
        4  0x7d0f  push  %ebx
  這兩句操作是在備份%esi,%ebx的值,因為這兩個寄存器叫做被調用者保存寄存器,即如果要在子過程中使用了它們,那么在子過程開頭處必須先備份這些寄存器的值。
        那么此時%esp的值為0x7bf0,而%ebp的值設置為0x7bf8。可見進入bootmain函數后,把0x7c00之前的低地址空間拿來作為棧幀使用。

  以上這些都屬於過程調用的常見指令,下面進入bootmain的c語言程序部分。

  首先看第一條C語言指令 

// read 1st page off disk
1 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

  下面看下這條指令匯編的結果:

  

  首先7d10~7d17三條指令的目的是為了把三個輸入參數壓入棧幀之中,以供readseg子過程訪問,%esp寄存器的值也隨之改變為0x7be4。此時7d1c執行,調用readseg子過程,該子過程的第一條指令地址為0x7cd2。由於題目讓我們深入到reagsec里面看看,但是reagsec只在readseg函數里面被調用了,所以必須深入到readseg。調用call時,會把bootmain下一個要執行的指令地址0x7d21壓入堆棧0x7be0,所以%esp的值會變成0x7be0.

  然后就開始進入readseg函數,進入readseg函數的頭幾個操作仍舊是跟子過程調用有關的,包括保存調用過程bootmain的棧幀信息,保存一個被調用者保存寄存器,%edi,的值。

  

  下一條匯編語句 mov 0xc(%ebp), %edi  是要取出第二個輸入參數即0x1000,4096到%edi寄存器中。

  因為此時的%ebp的值為0x7bdc,在這里面存放的是bootmain過程的%ebp值,0x04(%ebp)即0x7be0存放的是bootmain的返回地址,0x08(%ebp)存放的是第1個輸入參數0x10000,0xc(%ebp)存放的是第2個參數0x1000,0x10(%ebp)中存放的是第3個參數0x00。(具體原理你看完Appendix 2就會明白。)

  接下來的語句如下:

  

  可見是把第3個參數0x0存入%esi,第1個參數0x10000存入%ebx。由於這兩個寄存器也是被調用者保護寄存器,所以在改變他們的值之前都會把他們之前的值壓入堆棧。

  下一條要執行的匯編語句為 shr $0x9 %esi

   由於shr是匯編邏輯右移指令,而%esi中存放的是第3個參數值,offset,將這個offset邏輯右移9位正好等於把offset的除以512即SECTSIZE,所以就是在完成

   offset = (offset / SECTSIZE) + 1指令的其中一部分。這條c語言指令的功能是計算要讀取這段區域的第一個扇區的扇區號。

 

  下一條匯編語句:  add  %ebx, %edi

   由於%ebx中存放的是第1個參數pa,%edi中存放的是第2個參數0x1000(即4096),那么這條指令完成的就是C語言語句:

   end_pa = pa + count;

     這句指令是讓end_pa指向要被讀取到內存的這塊數據所存放的最后一個位置的地址。

  

  下一條匯編語句:  inc %esi

  這條語句很好理解就是在完成C語言語句 offset = (offset / SECTSIZE) + 1  中的加1的部分。

 

  下一條匯編語句: and $0xfffffe00, %ebx

  這條語句完成的是C語言語句: pa &= ~(SECTSIZE-1)   

  功能就是把pa重新定向到offset存儲單元所在的扇區的起始地址。

 

  下一條匯編語句: jmp 7cff <readseg+0x2d>

    此時開始進入while循環,即開始把外存中的數據傳輸到內核。為了實現while循環,匯編通常采取的方式是就是這種先jmp,然后再判斷循環條件的這種方法來實現。所以這條指令首先是要進行jmp,jump到判斷循環條件的語句處,0x7cff.

 

  下兩條匯編語句: cmp  %edi, %ebx

         jb 7cef <readseg+0x1d>

     這條語句就是在判斷當前%ebx的值和%edi的值的大小,其中%ebx存放的就是指針pa,而%edi中存放的是指針end_pa,所以它執行的C語言指令就是  while(pa < end_pa) ,  那么當%edi即end_pa仍舊大於%ebx即pa時,則jb指令(大於時跳轉)會跳轉回while循環的第二個語句0x7cef。

  

  下三條匯編語句: push %esi 

          inc %esi

                          push %ebx

  這三條語句在為調用readsec函數而做准備,把輸入參數先壓入到堆棧中。

 

  下面進入到readsec函數中: call 7x81<readsec>   

              push %ebp

             mov %esp, %ebp

             push %edi

    首先還是過程調用時的通用操作,修改棧幀等操作。              

 

  下一條指令: call 7x6c<waitdisk>

  這一條操作其實是調用了一個子函數,waitdisk(),這個函數用於查詢當前磁盤的狀態是否已經准備好進行操作。如果沒有准備好,那么程序就會一直停在這里,直到磁盤准備好。

  下一條指令:

  根據boot.c文件中,我們可以看到在readsect子程序中,waitdisk()操作之后需要調用一系列的outb子函數,這個子函數其實就是匯編語言中的outb匯編指令,它屬於IO端口命令,IO端口指令用於向外部設備的端口輸出指令,或從外部設備的端口讀入數據,之前我們也介紹過。那么outb函數有兩個參數,第1個參數是端口號,第2個參數是輸入的值。整個函數的功能就是想該端口輸出一個字節的數據

  那么具體的一個outb函數的實現如下,比如我們現在就考察第一個outb指令:outb(0x1F2, 1);  它所對應的匯編語句如下:

  

  可見應該把端口號送入%edx,把輸出數據送入%al中,然后調用out匯編命令即可。

  那么后邊的命令0x7c95~0x7cb9就是完成后續的所有outb操作,只不過端口號和數據有所差別。通過這些指令可以看出,系統是先想0x1F2端口送入一個值1,代表取出一個扇區,然后向0x1F3~0x1F6中送入你要讀取的扇區編號的32bit表示形式。最后向0x1F7端口輸出0x20指令表示要讀取這個扇區。

 

  下一條:

  那么輸入完上述地址,指令到相應的端口后,就可以讓磁盤自己去工作,此時系統只需調用waitdisk過程來等待磁盤完成讀取。waitdisk退出后,代表數據已經被讀取。然后就可以執行下一個指令了。

  下一個指令又是一個IO端口指令,insl,這個函數包含3個輸入參數,port代表端口號,addr代表這個扇區存放在主存中的起始地址,cnt則代表讀取的次數。

  

  圖中是insl的匯編實現。

  首先0x7cbf指令會把readsect函數的第1個參數送入%edi,第1個參數是dst,即這個扇區數據存放的目的起始地址,其中readseg是把pa送給readsect作為第一個參數的。所以當前%edi中存放的是pa。 0x7cc2指令會把%ecx賦值為0x80, 0x7cc7會把%edx賦值為0x1f0。

  0x7ccc執行指令cld,用於清除方向標識,這個在前面討論過,主要是為了能夠實現串操作,串操作的含義就是連續的一串相同的操作,通常作用在連續的內存上,比如把一串字符串常量送入到某個連續地址處,此時如果采用串操作的話,每傳一個字節的數據,串操作可以自動的把源操作數和目的操作數的地址加或減1。那么下一個操作就直接作用在下一個空間了。cld清除標志位,表明一個串操作完成后源操作數和目的操作數的地址加1。

  而在0x7ccd處的指令:

     repnz  insl  (%dx), %es:(%edi)

  其中首先關注一下repnz指令:

  repnz指令又叫做重復串操作指令,它是一個前綴,位於一條指令之前,這條指令將會一直被重復執行,並且直到計數寄存器的值滿足某個條件。repnz指令是當計數器%ecx的值不為零是就一直重復后面的串操作指令。那么被重復調用的指令就是insl指令。

    insl (%dx), %es:(%edi)

  這個指令中,%dx中存放着要訪問的端口號,0x1f0。該指令的目的就是把端口0x1f0中的數據傳輸給后面所指向的地址。而后面的地址采用%es:(%edi)格式,其中%edi中存放的就是要被存放的內存空間的起始地址。由於當前計數寄存器%ecx中存放的數值為128,代表我們進行128次存取操作就能讀取512byte的扇區。所以每次存取4個字節。 我們可以具體調試驗證下:

  剛剛執行完cld指令后,查詢寄存器信息。dx中存放的0x1f0,端口號,edi中存放的是pa,0x10000,起始地址。存放在0x10000~0x10005中是:

  

   執行了一次insl操作后,發現dx中存放端口號沒有改變,但是edi發生改變,值變成0x10004, 而存放在0x10000~0x10004地址處內容如下:

  

  可見0x10000到0x10004地址處發生了改變。一次操作會讀取4個字節。所以我們只需要調用128次函數,就能完成512的字節的存儲。

 

  下一條:

  當完成了128次讀取操作后,循環退出,執行下面三句

  

  這三句用於返回調用函數。這就是讀取一個扇區readsect子函數的整個運行過程。

 

  下一條:

  然后我們回到readseg函數,繼續執行下一條命令:

  

  緊接着是兩個出棧操作,當前的棧頂元素是上一輪while(pa < end_pa)循環中傳遞給readsect的兩個輸入參數,由於在下一輪調用時,這兩個參數會改變,所以先把這兩個值pop出來。然后進行下一句。

  

  首先把%edi %ebx中的內容進行比較,其中%edi存放的是end_pa值,而%ebx存放的是當前pa值。如果end_pa仍舊大於pa,說明該段包含多個扇區,這些扇區還沒有全部讀取完成。所以跳回 0x7cef。當讀取到end_pa之后,准備退出readseg,最后執行readseg退出之前的操作:

  

  其中第一句是把-0xc(%ebp)這個有效地址值發送給%esp,在執行這條指令之前,通過info registers發現%esp中為0x7bd0,%ebp中為0x7bdc。可見這個操作並不會改變什么。然后就是恢復我們之前保存的 被調用者保存寄存器的值 %ebx,%esi,%edi。 最后恢復bootmain的棧幀,回到bootmain。

 

  回到bootmain后,由於我們這個操作讀取出來的數據塊是內核的第一個塊,里面存放的是內核文件的elf文件頭。所以我們要下一步要做的事情就是驗證讀出的這個文件頭是不是代表一個有效的elf文件?其中一個文件如果是有效的ELF文件,那么這個文件的ELF文件頭部的頭四個字節EI_MAG0~EI_MAG3分別是0x7f,'E','L','F'。下面是驗證頭四個字節的代碼:

  

  首先把esp的內容加上了一個0x0c,執行完成后%esp的值由0x7be4變為0x7bf0。在bootmain開始時,曾經把%esp的值減去0xc,相當於為這個過程分配一些額外的內存空間,現在這個操作把這些空間收回了。

  然后0x7d24指令把0x10000地址處的內容和$0x464c457f進行比較,其實0x464c457f正好是0x7f,'E','L','F'。而0x10000正好是EI_MAG0,所以這一步操作就是在比較EI_MAG0~EI_MAG30x7f,'E','L','F'是否相等。如果不相等,則0x7d2e則會跳轉到0x7d69處,這里面存放着這種異常情況發生的話如何操作,但是一般不會走到這里。如果相等則代表這是一個有效的elf文件,所以繼續往下執行。

  

  緊接着執行兩條指令。他們分別對應着兩個C語言語句:

  

  0x7d30指令是完成它上面的那條C語言的操作的一部分。先把ELF文件頭部中e_phoff字段的值存入%ebx。這個字段中存放的是Program Header Table(段頭部表)的起始地址在文件中的偏移。而這個e_phoff字段是elf頭部字段中的第28個字節到第31個字節。所以匯編語句中這個字段的起始地址為0x1001C。所以%ebx中存放着Program Header Table的起始地址的偏移。

  0x7d36指定就是把e_phnum字段的值,讀入到%eax中,該字段代表Program Header Table中表項的個數。

  在這里我們就知道了操作系統內核一共有多少個段,以及段表起始地址。通過info registers指令查看,%eax的值為3,%ebx的值為0x34,即52。可見操作系統內核文件一共3個段,段表起始地址在相對於內核文件起始地址的0x34單元處。而ELF文件頭大小就是52,所以段表緊挨着ELF文件頭。

 

  接下來:

  

  接下來0x7d3d指令把%ebx的內容和整個內核文件的起始地址0x10000相加,得到的就是Program Header Table的起始物理地址。0x7d43操作,是把%eax的值左移5位,相當於乘以32倍。原因就是Program Header Table表中的每個表項大小為32字節,所以這步操作求出Program Header Table表一共占多少字節。

  0x7d46指令完成的操作就是,把%ebx+%eax*1的值送入到%esi,可見%esi中存放的是Program Header Table的最后一個存儲單元的地址。

  

  接下來:

  

  接下來就進入到for循環了,這個循環的操作是要把內核的每一段從外存取到內存。由於是循環,匯編要想實現循環,一般先進行一步jmp操作,跳轉到判斷終止條件處。

  

  這里就是判斷循環條件的地方,其中%esi存放的是Program Header Table表尾地址,%ebx存放的是當前訪問到Program Header Table中的位置。如果%esi > %ebx,代表還沒有取出所有段,自然要跳回循環開始處0x7d4b。

      

  0x7d4b ~ 0x7d5c就是在完成main.c中for循環里面的唯一操作:

     readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

  這一個步操作的意思:

  ph當前存放的是一個Program Header Table中一個表項的起始地址。ph->p_pa字段就是p_paddr字段,代表這個段的將要被存放在這個系統的內存中的起始物理地址。ph->p_memsz字段,代表這個段被實際的裝入內存后,它所占用的內存大小。ph->p_offset字段,代表這個段的起始地址距離整個內核文件起始地址的偏移。所以這個C語言語句的含義就是把這個表項所代表的段存放到ph->p_pa字段的所指定的內存地址處。

  至於匯編程序也和前面在bootmain剛開始的時候調用readseg函數的方式類似。0x7d4b~0x7d51是在把readseg的輸入參數壓入棧中。0x7d54是修改ph指針,讓它指向下一個Program Header Table中的表項。0x7d57調用readseg函數。0x7d5c指令是修改%esp,因為之前在輸入參數時一共輸入3個長度為4字節的參數,所以%esp減少了0x0c,這里讓%esp的值恢復。0x7d5f~0x7d61 判斷循環條件。

  

  然后就是循環執行完成的操作了,循環執行完成后,操作系統內核中所有的指令,數據都已經轉移到內存中。下面就要執行最后一步的操作:

    ((void (*)(void)) (ELFHDR->e_entry))();

  其中ELF文件頭的e_entry字段的含義是這個可執行文件的第一條指令的虛擬地址。所以這句話的含義就是把控制權轉移給操作系統內核。

  

  以上就是對整個Exercise1.3的完整解答~

  如果有疑問或者建議歡迎騷擾

    zzqwf12345@163.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM