Lab 1 Part 2: The Boot Loader

Exercise 3, 在地址 0x7c00 處設(shè)下端點(diǎn)繼續(xù)執(zhí)行

  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?

    ljmp    $PROT_MODE_CSEG, $protcseg
    
  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

    7d71: ff 15 18 00 01 00 call *0x10018

  • Where is the first instruction of the kernel?

    movw  $0x1234,0x472
    
  • How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

    ph->p_memsz
    

    Exercise 3

    #include <stdio.h>
    #include <stdlib.h>
    
    void
    f(void)
    {
        int a[4];
        int *b = malloc(16);
        int *c;
        int i;
    
        printf("1: a = %p, b = %p, c = %p\n", a, b, c);
    
        c = a;
        for (i = 0; i < 4; i++)
          a[i] = 100 + i;
        c[0] = 200;
        printf("2: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
         a[0], a[1], a[2], a[3]);
    
        c[1] = 300;
        *(c + 2) = 301;
        3[c] = 302;
        printf("3: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
       a[0], a[1], a[2], a[3]);
    
        c = c + 1;
        *c = 400;
        printf("4: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
         a[0], a[1], a[2], a[3]);
    
        // 輸出200 400 301 302
        // 現(xiàn)在a數(shù)組的字節(jié)分布為(小端)C8000000 90010000 2D010000 2E010000(00C8 0190 012D 012E)
        // c指向a[1]
        c = (int *) ((char *) c + 1);
        // 將c先轉(zhuǎn)換為char指針指向下一個(gè)字節(jié)后再轉(zhuǎn)回int指針
        *c = 500;
        //  C8000000 90*010000 2D*010000  修改 * 號(hào)里面的四個(gè)字節(jié)
        // 500 -> F4010000
        // a-> C8000000 90F40100 00010000 2E010000
        // 輸出200 128144 256 302
        printf("5: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
         a[0], a[1], a[2], a[3]);
    
        b = (int *) a + 1;
        c = (int *) ((char *) a + 1);
        printf("6: a = %p, b = %p, c = %p\n", a, b, c);
        //a = 0x7ffeebb28200, b = 0x7ffeebb28204, c = 0x7ffeebb28201
        //可以看到int指針+1 from c to b 是 4 個(gè)自己 而 char 指針只是一個(gè)字節(jié)
     }
    
    int
    main(int ac, char **av)
    {
        f();
        return 0;
    }
    
    
    

Exercise 6

i386-elf-objdump -f obj/kern/kernel 


obj/kern/kernel:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c

看到內(nèi)核的第一條程序的入口是 0x1000c

這里有個(gè)問題不是很明白, boot.asm 中,call 的最后一條程序的地址為此為啥會(huì)跳到knernal呢?

7d71:   ff 15 18 00 01 00       call   *0x10018

Part 3: The Kernel

i386-elf-objdump -x obj/kern/kernel | grep -2n LOAD

可以看見 text 的 link address("VMA")-> f0100000 與 load address ("LMA") 00100000 有所不同。

15-Idx Name          Size      VMA       LMA       File off  Algn
16-  0 .text         0000171e  f0100000  00100000  00001000  2**2
17:                  CONTENTS, ALLOC, LOAD, READONLY, CODE

這時(shí)因?yàn)椴僮飨到y(tǒng)常常會(huì)被鏈接到很高的虛擬地址(eg. f0100000 ) 為了原理用戶程序會(huì)使用到的地址空間。然而有許多的機(jī)器并沒有0xf0100000 (3.7GB) 這么大的內(nèi)存,所以我們需要處理器的內(nèi)存管理器講這個(gè)地址map 到 0x00100000 (kernel 被 load 的地址。

Exercise 7.

b *0x0010000c #在內(nèi)核的第一條指令處放下一個(gè)斷點(diǎn)
si #進(jìn)行單步調(diào)試

(gdb) b *0x10000c
Breakpoint 1, 0x0010000c in ?? ()
(gdb) si
=> 0x100015:    mov    $0x110000,%eax
0x00100015 in ?? ()
(gdb) si
=> 0x10001a:    mov    %eax,%cr3
0x0010001a in ?? ()
(gdb) si
=> 0x10001d:    mov    %cr0,%eax
(gdb) si
=> 0x100020:    or     $0x80010001,%eax
0x00100020 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000  0x00000000  0x00000000  0x00000000
0xf0100010 <entry+4>:   0x00000000  0x00000000  0x00000000  0x00000000
(gdb) si
=> 0x100025:    mov    %eax,%cr0
0x00100025 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000  0x00000000  0x00000000  0x00000000
0xf0100010 <entry+4>:   0x00000000  0x00000000  0x00000000  0x00000000
(gdb) si
=> 0x100028:    mov    $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x1badb002  0x00000000  0xe4524ffe  0x7205c766
0xf0100010 <entry+4>:   0x34000004  0x0000b812  0x220f0011  0xc0200fd8

可以看見在當(dāng)內(nèi)核執(zhí)行了 mov $0xf010002f,%eax 這個(gè)指令之后,0xf0100000地址開始有值。==這里不是很明白這個(gè)地址的意思==

Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right. 導(dǎo)致沒有分頁會(huì)讓整個(gè)程序卡死。

Exercise 8.

We have omitted a small fragment of code - the code necessary to print octal numbers using patterns of the form "%o". Find and fill in this code fragment.

num = getuint(&ap, lflag);
base = 8;
goto number;
  1. Specifically, what function does console.c export? How is this function used by printf.c?
static void
putch(int ch, int *cnt)
{
    cputchar(ch);
    *cnt++;
}

用來在 console 中輸出字符。

  1. Explain the following from console.c
if (crt_pos >= CRT_SIZE)
{
  int i;
  memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
  for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    crt_buf[i] = 0x0700 | ' ';
  crt_pos -= CRT_COLS;
}

如果crt_pos 超過了crt_buf的最大值,將 crt_buf 的 1-n-1 移動(dòng)到 0-n-2

再將下標(biāo)為 n-1 的那行全部置為空格,屬性設(shè)置為0x0700

參數(shù)int c 是什么,0xff0x0700又是什么?
int c一共32bit,其中高16位用來表示屬性,低16位用來表示字符。因此,與0xff作 and 運(yùn)算就是去掉屬性,只看字符內(nèi)容。與~0xff作 and 運(yùn)算就是去掉字符,只看屬性。與0x0700作 or 運(yùn)算就是設(shè)為默認(rèn)屬性。

==這部分是什么意思我還沒有弄懂==。后面再看看是什么意思。

  1. fmt 指向的是 "x %d, y %x, z %d\n"

    (gdb) x/s fmt
    0xf0101a6e:  "x %d, y %x, z %d\n
    
vcprintf()
vcprintf (fmt=0xf0101a6e "x %d, y %x, z %d\n", ap=0xf010ff04 "\001")
putch (ch=120, cnt=0xf010fecc)
cons_putc('x')
cons_putc(' ')
va_arg()
cons_putc('1')
cons_putc(',')
cons_putc(' ')
cons_putc('y')
cons_putc(' ')
va_arg()
cons_putc('3')
cons_putc(',')
cons_putc(' ')
cons_putc('z')
cons_putc(' ')
va_arg()
cons_putc('4')
cons_putc('\n')

可以用下面這個(gè)命令將相關(guān)的函數(shù)從文件中找到并且生成斷點(diǎn)的命令:進(jìn)行debug。

grep -HnE "cons_putc|va_arg|vcprintf" lib/printfmt.c kern/printf.c | cut -f1,2 -d ":" | xargs -I {} echo break {}
  1. 57616 = 0xe110。此外,根據(jù)x86的小端序,&i指向了byte序列0x72、0x6c、0x64、0x00。這等同于字符串”rld”。所以,最終的輸出為”He110 World”。

The Stack

x86 的棧指針(esp)指向被使用棧的最低位置(因?yàn)闂5自诘刂返母呶弧?

關(guān)于棧的相關(guān)信息可以參考:

設(shè)置斷點(diǎn)

b kern/init.c:12
b kern/init.c:16
b kern/init.c:20
#調(diào)用test_backtrace時(shí)
lea    -0x1(%ebx), %eax #eax = ebx - 1 -> x-1
push   %eax #被調(diào)函數(shù)param壓棧 從右往左
call   0xf0100040 #call 標(biāo)號(hào)(將當(dāng)前的IP壓棧后,轉(zhuǎn)到標(biāo)號(hào)處執(zhí)行指令)
#等效于:
#pushl %eip 會(huì)在函數(shù)返回是通過 ret 將其出棧
#movl f, %eip
#執(zhí)行 call 指令后 esp - 1
#esp            0xf010ff80          0xf010ff80
#esp            0xf010ff7c          0xf010ff7c

#eip 指向當(dāng)前函數(shù)調(diào)用
#eip            0xf0100074          0xf0100074 <test_backtrace+52>
#eip            0xf01008e3          0xf01008e3 <cprintf>

#進(jìn)入test_backtrace時(shí)
push   %ebp #主調(diào)函數(shù)幀基指針EBP
mov    %esp,%ebp # 將父函數(shù)的棧頂作為被調(diào)函數(shù)的棧底
push   %ebx 

sub    $0xc,%esp #將棧頂指針向下移動(dòng) 12 字節(jié)(3wds) 在棧上開辟一個(gè)空間存儲(chǔ)局部變量,注意這里用的是 sub(改變ESP值來為函數(shù)局部變量預(yù)留空間)
mov    0x8(%ebp),%ebx #將ebx  設(shè)置為 ebp + 8

#test_backtrace return時(shí)
mov    -0x4(%ebp),%ebx
leave
#等效于
#movl %ebp, %esp
#popl %ebp
ret
#等效于
#popl %eip


根據(jù)慣例,寄存器%eax、%edx%ecx為主調(diào)函數(shù)保存寄存器(caller-saved registers),當(dāng)函數(shù)調(diào)用時(shí),若主調(diào)函數(shù)希望保持這些寄存器的值,則必須在調(diào)用前顯式地將其保存在棧中;被調(diào)函數(shù)可以覆蓋這些寄存器,而不會(huì)破壞主調(diào)函數(shù)所需的數(shù)據(jù)。

寄存器%ebx%esi%edi為被調(diào)函數(shù)保存寄存器(callee-saved registers),即被調(diào)函數(shù)在覆蓋這些寄存器的值時(shí),必須先將寄存器原值壓入棧中保存起來,并在函數(shù)返回前從棧中恢復(fù)其原值,因?yàn)橹髡{(diào)函數(shù)可能也在使用這些寄存器。

(gdb) bt
#0  test_backtrace (x=0) at kern/init.c:18
#1  0xf0100068 in test_backtrace (x=1) at kern/init.c:16
#2  0xf0100068 in test_backtrace (x=2) at kern/init.c:16
#3  0xf0100068 in test_backtrace (x=3) at kern/init.c:16
#4  0xf0100068 in test_backtrace (x=4) at kern/init.c:16
#5  0xf0100068 in test_backtrace (x=5) at kern/init.c:16
#6  0xf01000d4 in i386_init () at kern/init.c:39
#7  0xf010003e in relocated () at kern/entry.S:80

bt 命令可以看見堆棧的情況

通過 x/20x $esp 命令可以看到從棧頂往下走的情況其中 callee (被調(diào)函數(shù))的返回指令為 0xf0100068 是在 caller 的ebp 下面。所以通過 *(ebp+1) 就可以將起讀出來。

0x00000001 <- caller ebx
0xf010ff58 <- caller ebp
0xf0100068 <- add    $0x10,%esp  
0x00000000 <- callee param
0x00000001
0xf010ff78 
0x00000000
0xf0100882 
0x00000002 <- caller ebx
0xf010ff78 <- caller ebp
0xf0100068 <- add    $0x10,%esp(epi)
0x00000001 <- callee param

增加的代碼如下

uint32_t ebp,*args;
cprintf("Stack backtrace:\n");
ebp = read_ebp();
while (ebp != 0)
{
    args = (uint32_t *)ebp;//將 ebp 的值轉(zhuǎn)換為指針地址
    ebp = args[0]; //將 caller 的 ebp 傳給這個(gè)參數(shù)
    cprintf("\tebp %x  eip %x  args %08x %08x %08x %08x %08x\n",ebp, args[1], args[2], args[3], args[4],args[5], args[6]);
}

Exercise 12:

這里需要知道 stab 是什么東西。可以參考這個(gè)文章: STAB 格式。

// Hint:
//  There's a particular stabs type used for line numbers.
//  Look at the STABS documentation and <inc/stab.h> to find
//  which one.
// Your code here.
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline)
{
    info->eip_line = stabs[lline].n_desc;
}
else
{
    return -1;
}
static struct Command commands[] = {
        {"help", "Display this list of commands", mon_help},
        {"kerninfo", "Display information about the kernel", mon_kerninfo},
        {"backtrace", "Display information about the kernel", mon_backtrace}};
running JOS: (1.5s)
  printf: OK
  backtrace count: OK
  backtrace arguments: OK
  backtrace symbols: OK
  backtrace lines: OK
Score: 50/50

總結(jié):

至此,我已經(jīng)看了這個(gè)lab1快有一周的時(shí)間了,由于關(guān)于C語言, 匯編的許多東西我都沒有特別明白所以看起來非常的吃力。

技術(shù)總結(jié):

  1. 電腦上電之后開始載入 BIOS, BIOS 開始搜索外設(shè)尋找 bootable 的外設(shè),如果 bootable, BIOS 將會(huì)把 OS 的 bootloabder 載入到 0x7c00~ 0x7dff 并且將 CS:IP 7c00.
  2. OS 的bootloabder 開始加載核心。首先是是通過一系列的匯編語言開啟CPU的保護(hù)模式,然后調(diào)用 bootmain() 函數(shù)。bootmain() 函數(shù)通過讀取磁盤將 ELF 格式的 kernel 載入到內(nèi)存中,并且將 CS:IP 跳轉(zhuǎn)到 ELF 的 enty 地址開始運(yùn)行核心代碼。
  3. Kernel 將啟動(dòng)分頁模式將低地址的代碼(0x00100000) 映射到(0xf0100000) 高位地址。
  4. 之后實(shí)驗(yàn)內(nèi)容讓我們閱讀了 cprintf (類似 printf)的代碼,并且完善了其實(shí)現(xiàn)。主要的思想是根據(jù) % 后面的symbol來確獲取定不定變量的 va_arg函數(shù)的傳入類型,從而通過不同的指針獲取不同的數(shù)據(jù)類型。因?yàn)橹羔橆愋偷牟煌?,同樣的?shù)據(jù)會(huì)被翻譯成不同的結(jié)果。
  5. 接下來是函數(shù)調(diào)用的過程(怎么壓棧,以及棧的相關(guān)知識(shí))。
  6. 最后是學(xué)習(xí) backtrace 相關(guān)的東西,對(duì)程序保存時(shí)輸出的信息有了一點(diǎn)了解。

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容