0%

MIT6.828-lab1

         蕾姆,终于小小地填了点坑

         这课B站上没找到很合适的视频,就直接跟着文档走了。这篇记一下自己不熟悉的知识点和没搞懂的地方。

Part 1: PC Bootstrap

         汇编器NASM使用的是intel语法,GNU使用的是AT&T语法。本实验使用GNU

The PC’s Physical Address Space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+------------------+  <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000

         BIOS一开始存放在真正的只读存储器中,现在则存放在可刷新的闪存中。BIOS用来实现基本的系统初始化,如激活显卡、检查安装的内存。初始化完成后,BIOS从正确的设备(如软盘、硬盘、CD-ROM或网络)中加载操作系统,并将执行权限交给操作系统。

         IBM PC从物理地址0x000ffff0开始执行,这是BIOS区相当高的一个地址,此处存放了一条指令:ljmp $0xf000,$0xe05b,设置了CS IP。这样设计的目的是使BIOS永远在开机或重启后先得到机器的控制权(啊?)。

         当BIOS运行时,建立一个中断描述符表,初始化如VGA display(视频图形阵列 显示?)在内的不同的设备。这就是QEMU窗口中可以看到字符的原因。当初始化完PCI bus和所有BIOS知道的重要设备后,它找到一个可引导的设备如软盘、硬盘、CD-ROM。找到后BIOSboot loader从硬盘中读出,并将控制权交给它。

Part2: The Boot Loader

         如果一个硬盘是可引导的,那么称它的第一个扇区为引导扇区,也就是boot loader所在的位置。当BIOS找到一个可引导设备时,它将这个引导扇区加载入物理地址为0x7c00~0x7dff的内存中,然后用jmp设置CS IP,将控制权交给boot loaderBIOS加载地址是相对任意的(?),而对PC来说这些地址是固定且标准化的。从CD-ROM中引导的能力是在PC开始发展很久以后才出现的,CD-ROM扇区大小为2048,能加载大得多的引导镜像。本实验使用的是传统硬件驱动引导机制。boot loader包括一个汇编语言源文件boot/boot.S和一个C源文件boot/main.c

         boot loader的功能应包括:

  • 将处理器从实模式转换到32位保护模式,因为只有在这种模式下才能访问1MB空间的内存。
  • 从硬盘里读取内核来通过x86特殊的IO指令直接访问IDE硬盘设备(啥?)寄存器。

         obj/kern/kernel.asm文件是JOS kernel的反汇编文件。

Exercise 3.

         在0x7c00处下断点,执行到此处后对照boot/boot.Sobj/boot/boot.asm 调试。回答以下问题。

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

         (这个不知道问的是保护模式还是32位模式就行)第一次跟的时候没在0x7c00处下断点,从头跟到这句以为就进了。可以看到此时地址是0xfd781故还在BIOS中,而这里我们学习的是boot loader所以应该不是这儿233。

1
2
3
4
5
6
[f000:d187]    0xfd187:	ljmpl  $0x8,$0xfd18f
0x0000d187 in ?? ()
(gdb)
The target architecture is assumed to be i386
=> 0xfd18f: mov $0x10,%eax
0x000fd18f in ?? ()

         下方是想提问的地方。程序加载全局描述符表,取cr0原始值,将值最后一位置1,赋给cr0,最后ljmp $PROT_MODE_CSEG, $protcseg跳转到32位代码段,程序进入保护模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[   0:7c1e] => 0x7c1e:	lgdtw  0x7c64
0x00007c1e in ?? ()
(gdb)
[ 0:7c23] => 0x7c23: mov %cr0,%eax
0x00007c23 in ?? ()
(gdb)
[ 0:7c26] => 0x7c26: or $0x1,%eax
0x00007c26 in ?? ()
(gdb) p/x $cr0
$1 = 0x0
(gdb) si
[ 0:7c2a] => 0x7c2a: mov %eax,%cr0
0x00007c2a in ?? ()
(gdb)
[ 0:7c2d] => 0x7c2d: ljmp $0x8,$0x7c32
0x00007c2d in ?? ()
(gdb) si
The target architecture is assumed to be i386
=> 0x7c32: mov $0x10,%ax
0x00007c32 in ?? ()

lgdt指令在实模式保护模式下都可以执行

CR0是处理器内部的控制寄存器,包含了用于控制处理器操作模式和运行状态的标志位。32位,第1位是保护模式允许位PE

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

         对比main.c和自0x7c00起始的反汇编指令,得到最后一条指令

1
2
3
4
0x7d7f:	jmp    0x7d66
0x7d81: call *0x10018 <---
;((void (*)(void)) (ELFHDR->e_entry))();
0x7d87: mov $0x8a00,%edx
1
2
3
4
5
6
Continuing.
=> 0x7d81: call *0x10018
Breakpoint 4, 0x00007d81 in ?? ()
(gdb) si
=> 0x10000c: movw $0x1234,0x472 <--kernel第一条指令
0x0010000c in ?? ()
  • 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?

         bootmain首先从磁盘上读取8个扇区大小的数据载入0x10000处,检测到载入的是一个合法的ELF文件后,取ELF结构体成员段表偏移ELFHDR->e_phoff+文件起始地址为第一个段表所在位置,段表个数由成员e_phnum得知。遍历每一个段表,根据段表结构体Proghdr成员p_offset,p_memsz,p_pa将每个段载入各自对应的位置。这里猜测不直接将所有段一起载入是为了节约空间。

Loading the Kernel

         VMALMAVMA,或虚拟地址,运行时才会有;LMA,或加载地址,加载时使用。链接器对二进制文件中的链接地址进行不同的处理,如当程序需要一个全局变量的地址时,如果是从未经链接的地址开始执行,二进制文件就不能正常工作。(不需要就可以正常执行?这句话属实不懂,链接地址又是个啥)生成这种不引用任何绝对地址的位置无关代码的技术已存在,且为现代共享库广泛使用,但它存在性能和复杂性上的开销,所以本实验中不用。也就是说,在本实验中段的VMALMA相同。

链接:将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行与编译时(源代码->机器代码)、加载时(程序被加载器loader加载到内存并执行时)、甚至运行时(由应用程序执行)。

加载:将磁盘中的指令和数据载入内存中并运行的过程。

linker文档中提到,大部分时候两个地址都是相同的,一种二者可能不同的例子是数据段被加载到ROM中,当程序启动时又被复制到RAM中(这种技术常被用来在基于ROM的系统中初始化全局变量)

PS1:原文档翻译过来是VMA,或链接地址,加载到内存中的地址;LMA,或加载地址,这个段应该被载入内存中的地址。我就想问问VMA LMA这俩描述有啥区别??

PS2:为避免“链接”的概念混淆,上述链接替换为虚拟,特指运行时地址。

         ELF对象中需要被加载入内存的部分被标记为LOADph->p_pa字段是段的物理地址

Exercise 5 通过给 boot/Makefrag中的链接器传递 -Ttext 0x7C00来设置链接地址,这样程序才能正常执行。现改变0x7c00看程序如何崩溃。

【原因不会讲】

1
[   0:7c2d] => 0x7c2d:	ljmp   $0x8,$0x6c32

e_entry存放了程序入口点的链接地址

Exercise 6BIOS进入boot loader时和boot loader进入kernel时观察内存0x100000处8字节的值,为什么它们不同?第二次观察时加载的是什么?

         在进入boot loader时为0,进入kernel时:

1
2
3
(gdb) x/8wx 0x100000
0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8

         在从boot loader进入kernel时加载了kernel的第一页。前三个字段的含义可从这里查阅到,所有的OS image文件都会有这个文件头。

Part3: The Kernel

Using virtual memory to work around position dependence

         由objdump -h输出结果得知boot loader的链接和加载地址相同,内核的不同。许多机器没有0xf0100000这么高的地址,我们要将处理器的内存管理硬件从虚拟地址0xf0100000映射到物理地址0x100000处(kernel实际被加载的地址)。链接内核比boot loader更复杂,所以内核的链接和加载地址在kern/kernel.ld的顶部【啥因果关系】。操作系统内核通常更倾向于链接并运行在较高的虚拟地址,将处理器的低地址留给用户程序使用。

Exercise 7. 在切换到保护模式前观察内存0x001000000xf0100000处的值,单步运行一步,观察发生了什么?如果新的映射建立不正确,此后第一条出错的指令是什么?

1
2
3
4
5
6
7
8
=> 0x100025:	mov    %eax,%cr0
0x00100025 in ?? ()
(gdb) x/8wx 0x100000
0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
(gdb) x/8wx 0xf0100000
0xf0100000 <_start-268435468>: 0x00000000 0x00000000 0x00000000 0x00000000
0xf0100010 <entry+4>: 0x00000000 0x00000000 0x00000000 0x00000000

执行后

1
2
3
4
5
6
7
(gdb) si
=> 0x100028: mov $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/8wx 0xf0100000
0xf0100000 <_start-268435468>: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0xf0100010 <entry+4>: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
(gdb)

第一条出错感觉这句吧

1
2
3
f0100028:	b8 2f 00 10 f0       	mov    $0xf010002f,%eax
jmp *%eax
f010002d: ff e0 jmp *%eax

Formatted Printing to the Console

Exercise 8 补全8进制输出代码

类比16进制写。可变长度参数函数参考

  • Explain the following from console.c:
1
2
3
4
5
6
7
8
/* 实现了滚屏功能 */
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));// 将第二行至结尾的字符移动到第一行至倒数第二行
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';// 最后一行输出空格,前背景色没查到
6 crt_pos -= CRT_COLS;
7 }

crt_pos:current position表示了当前光标位置

CRT_SIZE:显示屏最大字符数,宽×高

crt_buf:16位无符号整数数组,存储显示屏所有字符的ASCII码和显示属性。

  • 调试,回答问题
1
2
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);

1.In the call to cprintf(), to what does fmt point? To what does ap point?

         这个地方发现如果只跟到cprintf第一句:

1
2
3
4
pwndbg> p fmt
$1 = 0xf0101c52 "x %d, y %x, z %d\n"
pwndbg> p ap
No symbol "ap" in current context.

         ap的值看不到,不懂为啥。跟到cprintf调用的vcprintf第一句才看到正确的指向如下。fmt指向模板字符串地址,ap指向参数列表地址。

1
2
3
4
5
6
7
pwndbg> p fmt
$4 = 0xf0101c52 "x %d, y %x, z %d\n"
pwndbg> p ap
$5 = (va_list) 0xf0110fd4 "\001"
pwndbg> x/3wx ap
0xf0110fd4: 0x00000001 0x00000003 0x00000004
pwndbg>

6.Let’s say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

         不太懂想问啥

Stack

backtrace:回溯,嵌套调用call的过程中保存下来的IP指针的值集合

Exercise 9. 找出内核初始化栈的位置,栈的位置。内核是如何为栈保留空间的?栈指针最初指向这块区域的哪一端?

​    在kern/entry.S中的这两句初始化了栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
f010002f <relocated>:
relocated:

# Clear the frame pointer register (EBP)
# so that once we get into debugging C code,
# stack backtraces will be terminated properly.
movl $0x0,%ebp # nuke frame pointer
f010002f: bd 00 00 00 00 mov $0x0,%ebp

# Set the stack pointer
movl $(bootstacktop),%esp
f0100034: bc 00 00 11 f0 mov $0xf0110000,%esp

# now to C code
call i386_init
f0100039: e8 6c 00 00 00 call f01000aa <i386_init>

​   文档提示,由于编译器优化,在 mon_backtrace()序言前调用read_ebp会导致不完整的栈回溯,需要保证调用read_ebp发生在mon_backtrace之后。

【为啥直接调函数会优化】

Exercise 12. 改进回溯函数,打印文件名、函数名、源文件行数、eip相对函数地址的偏移。

【为啥填的是n_desc成员】

Reference

带有C表达式操作数的AT&T语法介个

我死活读不懂的VGA文档

CSAPP链接

这个lab真是做得费劲死了