蕾姆,终于小小地填了点坑
这课B站上没找到很合适的视频,就直接跟着文档走了。这篇记一下自己不熟悉的知识点和没搞懂的地方。
Part 1: PC Bootstrap
汇编器NASM
使用的是intel
语法,GNU
使用的是AT&T
语法。本实验使用GNU
。
The PC’s Physical Address Space
1 | +------------------+ <- 0xFFFFFFFF (4GB) |
BIOS
一开始存放在真正的只读存储器中,现在则存放在可刷新的闪存中。BIOS
用来实现基本的系统初始化,如激活显卡、检查安装的内存。初始化完成后,BIOS
从正确的设备(如软盘、硬盘、CD-ROM或网络)中加载操作系统,并将执行权限交给操作系统。
IBM PC
从物理地址0x000ffff0
开始执行,这是BIOS区
相当高的一个地址,此处存放了一条指令:ljmp $0xf000,$0xe05b
,设置了CS IP
。这样设计的目的是使BIOS
永远在开机或重启后先得到机器的控制权(啊?)。
当BIOS
运行时,建立一个中断描述符表,初始化如VGA display
(视频图形阵列 显示?)在内的不同的设备。这就是QEMU
窗口中可以看到字符的原因。当初始化完PCI bus
和所有BIOS
知道的重要设备后,它找到一个可引导的设备如软盘、硬盘、CD-ROM。找到后BIOS
将boot loader
从硬盘中读出,并将控制权交给它。
Part2: The Boot Loader
如果一个硬盘是可引导的,那么称它的第一个扇区为引导扇区,也就是boot loader
所在的位置。当BIOS
找到一个可引导设备时,它将这个引导扇区加载入物理地址为0x7c00~0x7dff
的内存中,然后用jmp
设置CS IP
,将控制权交给boot loader
。BIOS
加载地址是相对任意的(?),而对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.S
和 obj/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 | [f000:d187] 0xfd187: ljmpl $0x8,$0xfd18f |
下方是想提问的地方。程序加载全局描述符表,取cr0
原始值,将值最后一位置1,赋给cr0
,最后ljmp $PROT_MODE_CSEG, $protcseg
跳转到32位代码段,程序进入保护模式。
1 | [ 0:7c1e] => 0x7c1e: lgdtw 0x7c64 |
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 | 0x7d7f: jmp 0x7d66 |
1 | Continuing. |
- 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
VMA
和LMA
。VMA
,或虚拟地址,运行时才会有;LMA
,或加载地址,加载时使用。链接器对二进制文件中的链接地址进行不同的处理,如当程序需要一个全局变量的地址时,如果是从未经链接的地址开始执行,二进制文件就不能正常工作。(不需要就可以正常执行?这句话属实不懂,链接地址又是个啥)生成这种不引用任何绝对地址的位置无关代码的技术已存在,且为现代共享库广泛使用,但它存在性能和复杂性上的开销,所以本实验中不用。也就是说,在本实验中段的VMA
和LMA
相同。
链接:将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行与编译时(源代码->机器代码)、加载时(程序被加载器
loader
加载到内存并执行时)、甚至运行时(由应用程序执行)。加载:将磁盘中的指令和数据载入内存中并运行的过程。
linker文档中提到,大部分时候两个地址都是相同的,一种二者可能不同的例子是数据段被加载到ROM中,当程序启动时又被复制到RAM中(这种技术常被用来在基于ROM的系统中初始化全局变量)
PS1:原文档翻译过来是
VMA
,或链接地址,加载到内存中的地址;LMA
,或加载地址,这个段应该被载入内存中的地址。我就想问问VMA LMA
这俩描述有啥区别??PS2:为避免“链接”的概念混淆,上述链接替换为虚拟,特指运行时地址。
ELF
对象中需要被加载入内存的部分被标记为LOAD
,ph->p_pa
字段是段的物理地址
Exercise 5 通过给 boot/Makefrag
中的链接器传递 -Ttext 0x7C00
来设置链接地址,这样程序才能正常执行。现改变0x7c00
看程序如何崩溃。
【原因不会讲】
1 | [ 0:7c2d] => 0x7c2d: ljmp $0x8,$0x6c32 |
e_entry
存放了程序入口点的链接地址
Exercise 6 在BIOS
进入boot loader
时和boot loader
进入kernel
时观察内存0x100000
处8字节的值,为什么它们不同?第二次观察时加载的是什么?
在进入boot loader
时为0,进入kernel时:
1 | (gdb) x/8wx 0x100000 |
在从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. 在切换到保护模式前观察内存0x00100000
和0xf0100000
处的值,单步运行一步,观察发生了什么?如果新的映射建立不正确,此后第一条出错的指令是什么?
1 | => 0x100025: mov %eax,%cr0 |
执行后
1 | (gdb) si |
第一条出错感觉这句吧
1 | f0100028: b8 2f 00 10 f0 mov $0xf010002f,%eax |
Formatted Printing to the Console
Exercise 8 补全8进制输出代码
类比16进制写。可变长度参数函数参考
- Explain the following from console.c:
1 | /* 实现了滚屏功能 */ |
crt_pos
:current position
表示了当前光标位置
CRT_SIZE
:显示屏最大字符数,宽×高
crt_buf
:16位无符号整数数组,存储显示屏所有字符的ASCII码和显示属性。
- 调试,回答问题
1 | int x = 1, y = 3, z = 4; |
1.In the call to cprintf()
, to what does fmt
point? To what does ap
point?
这个地方发现如果只跟到cprintf
第一句:
1 | pwndbg> p fmt |
ap
的值看不到,不懂为啥。跟到cprintf
调用的vcprintf
第一句才看到正确的指向如下。fmt
指向模板字符串地址,ap
指向参数列表地址。
1 | pwndbg> p fmt |
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 | f010002f <relocated>: |
文档提示,由于编译器优化,在 mon_backtrace()
序言前调用read_ebp
会导致不完整的栈回溯,需要保证调用read_ebp
发生在mon_backtrace
之后。
【为啥直接调函数会优化】
Exercise 12. 改进回溯函数,打印文件名、函数名、源文件行数、eip
相对函数地址的偏移。
【为啥填的是n_desc
成员】
Reference
CSAPP链接
这个lab
真是做得费劲死了