Lab 1 Part 2: The Boot Loader

Exercise 3, 在地址 0x7c00 处设下端点继续执行

  • 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
        // 现在a数组的字节分布为(小端)C8000000 90010000 2D010000 2E010000(00C8 0190 012D 012E)
        // c指向a[1]
        c = (int *) ((char *) c + 1);
        // 将c先转换为char指针指向下一个字节后再转回int指针
        *c = 500;
        //  C8000000 90*010000 2D*010000  修改 * 号里面的四个字节
        // 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 个自己 而 char 指针只是一个字节
     }
    
    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

看到内核的第一条程序的入口是 0x1000c

这里有个问题不是很明白, boot.asm 中,call 的最后一条程序的地址为此为啥会跳到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

这时因为操作系统常常会被链接到很高的虚拟地址(eg. f0100000 ) 为了原理用户程序会使用到的地址空间。然而有许多的机器并没有0xf0100000 (3.7GB) 这么大的内存,所以我们需要处理器的内存管理器讲这个地址map 到 0x00100000 (kernel 被 load 的地址。

Exercise 7.

b *0x0010000c #在内核的第一条指令处放下一个断点
si #进行单步调试

(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

可以看见在当内核执行了 mov $0xf010002f,%eax 这个指令之后,0xf0100000地址开始有值。==这里不是很明白这个地址的意思==

Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right. 导致没有分页会让整个程序卡死。

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 移动到 0-n-2

再将下标为 n-1 的那行全部置为空格,属性设置为0x0700

参数int c 是什么,0xff0x0700又是什么?
int c一共32bit,其中高16位用来表示属性,低16位用来表示字符。因此,与0xff作 and 运算就是去掉属性,只看字符内容。与~0xff作 and 运算就是去掉字符,只看属性。与0x0700作 or 运算就是设为默认属性。

==这部分是什么意思我还没有弄懂==。后面再看看是什么意思。

  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')

可以用下面这个命令将相关的函数从文件中找到并且生成断点的命令:进行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。此外,根据x86的小端序,&i指向了byte序列0x72、0x6c、0x64、0x00。这等同于字符串”rld”。所以,最终的输出为”He110 World”。

The Stack

x86 的栈指针(esp)指向被使用栈的最低位置(因为栈底在地址的高位。)

关于栈的相关信息可以参考:

设置断点

b kern/init.c:12
b kern/init.c:16
b kern/init.c:20
#调用test_backtrace时
lea    -0x1(%ebx), %eax #eax = ebx - 1 -> x-1
push   %eax #被调函数param压栈 从右往左
call   0xf0100040 #call 标号(将当前的IP压栈后,转到标号处执行指令)
#等效于:
#pushl %eip 会在函数返回是通过 ret 将其出栈
#movl f, %eip
#执行 call 指令后 esp - 1
#esp            0xf010ff80          0xf010ff80
#esp            0xf010ff7c          0xf010ff7c

#eip 指向当前函数调用
#eip            0xf0100074          0xf0100074 <test_backtrace+52>
#eip            0xf01008e3          0xf01008e3 <cprintf>

#进入test_backtrace时
push   %ebp #主调函数帧基指针EBP
mov    %esp,%ebp # 将父函数的栈顶作为被调函数的栈底
push   %ebx 

sub    $0xc,%esp #将栈顶指针向下移动 12 字节(3wds) 在栈上开辟一个空间存储局部变量,注意这里用的是 sub(改变ESP值来为函数局部变量预留空间)
mov    0x8(%ebp),%ebx #将ebx  设置为 ebp + 8

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


根据惯例,寄存器%eax%edx%ecx为主调函数保存寄存器(caller-saved registers),当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。

寄存器%ebx%esi%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。

(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 (被调函数)的返回指令为 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 的值转换为指针地址
    ebp = args[0]; //将 caller 的 ebp 传给这个参数
    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 是什么东西。可以参考这个文章: 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

总结:

至此,我已经看了这个lab1快有一周的时间了,由于关于C语言, 汇编的许多东西我都没有特别明白所以看起来非常的吃力。

技术总结:

  1. 电脑上电之后开始载入 BIOS, BIOS 开始搜索外设寻找 bootable 的外设,如果 bootable, BIOS 将会把 OS 的 bootloabder 载入到 0x7c00~ 0x7dff 并且将 CS:IP 7c00.
  2. OS 的bootloabder 开始加载核心。首先是是通过一系列的汇编语言开启CPU的保护模式,然后调用 bootmain() 函数。bootmain() 函数通过读取磁盘将 ELF 格式的 kernel 载入到内存中,并且将 CS:IP 跳转到 ELF 的 enty 地址开始运行核心代码。
  3. Kernel 将启动分页模式将低地址的代码(0x00100000) 映射到(0xf0100000) 高位地址。
  4. 之后实验内容让我们阅读了 cprintf (类似 printf)的代码,并且完善了其实现。主要的思想是根据 % 后面的symbol来确获取定不定变量的 va_arg函数的传入类型,从而通过不同的指针获取不同的数据类型。因为指针类型的不同,同样的数据会被翻译成不同的结果。
  5. 接下来是函数调用的过程(怎么压栈,以及栈的相关知识)。
  6. 最后是学习 backtrace 相关的东西,对程序保存时输出的信息有了一点了解。

参考文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容