xv6实验1-启动计算机(⽂档)
PC Bootstrap
第⼀个练习的⽬的是介绍x86汇编语⾔和PC bootstrap程序,使⽤Q EMU和QEMU/GDB进⾏调试。这部分你不⽤写任何代码,但是不管怎样,你最好过⼀遍,并且回答⽂章后⾯的问题
x86汇编
如果你对x86汇编语⾔不熟悉,通过这个课程,你将很快熟悉它。是⼀个⾮常好的⼊门书籍,这本书混杂了最新和旧版本的信息
警告:不幸的是,这本书是⽤NASM汇编语⾔来编写的,然⽽,我们将使⽤GNU汇编。NASM使⽤所谓的Intel语法,然⽽GNU使⽤的是AT&T语法。两者的差异⾮常⼤,所幸的是,使⽤这个⼯具,能够快速的转换
进⾏Exercise 1
当然,x86汇编语⾔编程的参考是Intel的⽩⽪书,你可以在到两个版本:⼀个是⽼的, 这个版本简短,也⽐较简单,但是描述了所有x86处理器的特征,6.828课程也是使⽤这个作为参考;另⼀个是最新版本
的,最新版包含了最新处理器的所有特征,但是这个课程⽤不上,如果你感兴趣,可以阅读。还有⼀个关于相对来说更加友好,但是仅针对AMD的处理器
仿真x86
不是在真正的物理机上开发⼀个操作系统,⽽是使⽤模拟器模拟⼀个完整的PC。适⽤于模拟器的代码当然也可以在实际物理机器上跑。使⽤模拟器可以简化调试。例如,你可以设置在模拟的x86中设置断点,但是这个在实际的x86系统中却很难做到
在6.828课程中,我们将使⽤,QEMU可以配合⼀起使⽤,进⾏调试。
在lab⽬录输⼊make,可以看到下⾯的输出
+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
ld: warning: section `.bss' type changed to PROGBITS
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 390 bytes (max 510)
+ mk obj/kern/kernel.img
如果你有类似于"undefined reference to __udivdi3"这种错误,你可能没有32位gcc编译包,如果你运⾏在Ubuntu或Debian,尝试安装gcc-multilib包。使⽤我的Dockerfile,不会出现这个问题
现在,你准备运⾏QEMU,装载obj/kern/kernel.img⽂件,这个⽂件包含引导加载程序(obj/boot/boot)和内核(obj/kernel)
运⾏make qemu(有界⾯)或者make qemu-nox(⽆界⾯)。将会启动QEMU并且加载硬盘,成功进⼊系统。具体显⽰如下
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>
PC物理地址空间
⼀个计算机的物理内存地址通常是下⾯的结构
+------------------+ <- 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)
Loading [MathJax]/jax/output/HTML-CSS/jax.js
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
第⼀代PC是基于16位Intel 8088处理器,仅仅只有1MB的物理内存。因此早期PC的物理地址空间是从0x
00000000到0x000FFFFF,⽽不是0xFFFFFFFF。其中640KB的区域被标记为只有早期计算机能够使⽤的RAM(random-access memory),实际上,⾮常早期的PC能够配置16KB,32KB,或者64KB⼤⼩的RAM
从0x000A0000到0x000FFFFF的384KB是由硬件保留⽤作特殊⽤途的,⽐如视频显⽰缓冲区和保存在⾮易失内存的固件。最重要的保留区域是BIOS(Basic Input/Output System),BIOS占⽤从0x000F0000到0x000FFFFF的64KB,这个区域也被称为ROM(read-only memory),但是现在PC把BIOS保存在可更新的闪存中。BIOS负责系统的初始化,例如激活显卡,检查内存空间⼤⼩。完成初始化后,BIOS会从合适的位置(例如软盘,硬盘,CD-ROM或⽹络中)加载操作系统,然后把机器的控制权交给操作系统
Intel的80286和80386处理器最终打破了1MB的障碍,这两款处理器可以⽀持16MB和4GB物理地址空间,尽管如此,PC架构还是保留了低1MB物理地址空间的原始布局,以确保与现有软件的向后兼容性。因此现代PC在物理内存中存在⼀个"洞", 从0x000A0000到0x00100000,把RAM分成"低"或者"传统内存"(最初
640KB)和"扩展内存"(剩下部分)。此外,PC的32位物理地址空间RAM的顶部,现在被保留为由BIOS使⽤的32位PCI设备
最近x86处理器能够⽀持超过4GB的物理RAM,所以RAM可以延伸到超过0xFFFFFFFF。这样,为了为这些32位设备预留空间映射,BIOS必须在32位可寻址区域顶部的系统RAM留出第⼆个洞。由于设计限
制,JOS只会使⽤前256MB的物理内存,所以现在假设所有的PC只有⼀个32位物理地址空间。但是处理复杂的物理地址空间和硬件组织的其他⽅⾯是操作系统开发的重要挑战之⼀
BIOS
这部分实验,你将使⽤QEMU的调试⼯具来探索IA-32兼容的电脑是怎么启动的
打开两个两个terminal窗⼝,进⼊到实验⽬录,输⼊make qemu-nox-gdb。这会启动QEMU,但是在Q EMU会在第⼀条指令前停⽌,等待GDB的连接。在第⼆个terminal,输⼊make gdb,你可以看到gdb的输出
实验提供了⼀个.gdbinit⽂件,⽤来启动GDB的16位代码调试,并将其链接到正在监听的QEMU(如果没有起作⽤,你必须添加⼀个add-auto-load-safe-path在你的.gdbinit,确保gdb程序是按照上述的操作连到QEMU)
QEMU输出
***
*** Now run 'make gdb'.
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::25000 -D qemu.log -S
GDB输出
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB. Attempting to continue with the default i8086 settings.
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
为什么QEMU会如此启动?这与Intel设计的8088处理器有关。因为PC⾥的B IOS是"硬连接"到物理地址的0x000F0000~0x000FFFFF的,这个设计保证了PC在开机或重启的时候,BIOS总是可以拿到控制权,
这⼀点是⾮常重要的,因为开机的时候,机器的RAM没有处理器能够执⾏的其他软件。QEMU仿真器有⾃⼰的BIOS,位于处理器仿真的物理地址空间。当处理器重置的时候,仿真的处理器进⼊到实模式,设置CS为0xF00, IP为0xFFF0,以便执⾏从CS:IP段地址开始。段地址0xF000:0xFFF0是怎么转换成物理地址的呢?
为了解答上⾯的问题,我们需要知道实模式地址。在实模式中,地址转换是根据公式物理地址=16∗段+偏移值进⾏计算的。所以,当P设
img文件如何打开置CS为0xF00, IP为0xFFF0,物理地址为16∗0xF000+0xFFF0=0xFFFF0
0xFFFF0是BIOS结束前(0x100000)的16个字节,因此,我们不应该对BIOS所做的第⼀件事就是jmp到BIOS较早的位置感到惊讶;毕竟在16字节内,能完成多少?
进⾏练习2
当BIOS运⾏之后,它会设置中断描述符表,然后初始化各种各样的设备,例如VGA显⽰
初始化PC总线和所有BIOS知道的重要设备之后,它会搜索可引导设备例如软盘,硬盘或者CD-ROM。最终,当它到可引导磁盘之后,BIOS从磁盘读取引导扇区(boot loader)并把控制权交给它
引导加载程序
PC的软盘和硬盘被分成512字节的区域成为扇区(sectors)。⼀个扇区是磁盘最⼩转换单位:每次读或写操作必须是⼀个或多个扇区。如果磁盘是可引导的,第⼀个扇区被称为引导扇区(boot sector),因为这个地⽅是引导加载程序代码所在的位置。当BIOS到了⼀个可引导的软盘或硬盘,它会加载512字节的引导扇区到物理地址0x7c00~0x7dff的内存处,然后使⽤jmp指令设置CS:IP为0000:7c000,把控制权交给引导扇区。像BIOS加载地址⼀样,这些地址相当随意-但是这个地址已经固定并且标准化了
在PC发展过程中,从CD-ROM中启动的能⼒出现的更晚,因此PC架构师借此机会稍微重新考虑了启动过程。结果,现代BIOS从CD-ROM启动有⼀点复杂,但是也更强⼤。CD-ROM使⽤2048字节作为⼀个扇区,⽽不是512字节,在移交控制权之前,BIOS能够从磁盘加载更⼤的启动镜像到内存(不仅仅是⼀个扇区)。对于更多的细节可以参考
然⽽,对于6.828,我们使⽤传统的硬件驱动引导机制,这也意味着我们的引导扇区必须是512字节。引导扇区由汇编语⾔源码⽂件boot/boot.S和⼀个C源码⽂件boot/main.c组成。仔细查阅这些源码⽂件,确保你理解它是怎么运⾏的,引导扇区有两个主要的功能:
引导扇区把处理器从16位实模式切换到32位保护模式(protected mod),因为只有在保护模式下,软件才能使⽤超过1MB的物理地址空间。保护模式在的1.2.7和1.2.8部分有简短的介绍,Intel架构⼿册中有关于保护模式的详细介绍。此时,你只需要理解段地址转换(段地址:偏移值)在保护模式下转换时不同的,转换之后是32位⽽不是16位
引导扇区通过x86特殊I/O指令访问IDE磁盘设备寄存器,以此来从磁盘读取内核。如果你想对这⾥的特殊I/O指令了解更深,查阅的"IDE hard drive controller"部分内容。
理解了引导扇区的源码后,查看obj/boot/boot.asm⽂件,这个⽂件是引导扇区的反汇编代码,是由GNU的makefile在编译引导扇区代码之后创建的。这个反汇编⽂件可以很容易理解所有引导扇区代码在物理内存中的位置,也让GDB跟踪下⼀步代码发⽣了什么变得更加容易。同样的,obj/kern/kernel.asm也包含了JOS 内核的反汇编
你可以在GDB⽤b命令设置地址断点。例如b *0x7c00,在0x7C00设置⼀个断点,⼀旦运⾏到断点,你可以使⽤c和si命令继续执⾏,c是断点执⾏,si是单步执⾏
进⾏练习3
加载内核
我们将进⼊到引导扇区的C语⾔代码部分,去了解细节,在boot/main.c。但是在这之前,是时候停下来,阅读⼀些C程序的基础知识了。
进⾏练习4
为了理解boot/main.c,你需要了解什么是ELF⼆进制⽂件。当你编译和链接C程序(例如JOS内核)时,编译器把每个C源码(.c)编译成包含汇编语⾔的⽬标⽂件(.o),链接器然后把所有编译的⽬标⽂件组合到⼀个⼆进制镜像,例如obj/kern/kernel,这个⼆进制⽂件就是ELF格式,标准名称为"Executable and Linkable Format"
关于这个格式的详细信息可以参考,但是你不必深⼊到这个格式的每个细节。尽管整个格式是⾮常强⼤和复杂的,但是⼤部分复杂的部分都⽀持动态链接库动态加载,有简短的介绍
对于6.828,你可以把ELF可执⾏⽂件看成包含头部和⼀些程序部分,为了加载到指定地址的内存,每个部分都是连续的代码块和数据块。启动扇区不会修改代
码或数据,它会加载到内存然后开始执⾏
⼀个ELF⼆进制⽂件以⼀个定长ELF头部开始,然后是可变长度的程序头部,程序头部列出了每个程序会加载多少扇区。C定义了这些ELF头部在inc/elf.h,我们感兴趣的代码部分是:
.text: 程序可执⾏的指令
.rodata: 只读数据,⽐如A SCII字符
.data: 数据部分包含程序初始化数据,例如全局变量
当链接器计算程序的内存结构,它会为没有初始化的全局变量保留空间,这个部分被称为.bss,紧接着.data之后。C会把没有初始化的全局变量初始化成0。因此ELF⼆进制⽂件中的.bss没有内容。然⽽,链接器仅仅记录了.bss部分的地址和⼤⼩。加载器或程序本⾝必须把0分配给.bss部分
测试所有部分的名称,⼤⼩和链接地址,可以⽤指令objdump -h obj/kern/kernel
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001871 f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000714 f0101880 00101880 00002880 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000038d1 f0101f94 00101f94 00002f94 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 000018bb f0105865 00105865 00006865 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f0108000 00108000 00009000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000648 f0112300 00112300 00013300 2**5
CONTENTS, ALLOC, LOAD, DATA
6 ment 00000035 00000000 00000000 00013948 2**0
CONTENTS, READONLY
可以看到不仅仅是上⾯列的那些内容,但是其他的都不重要。其他的⼤部分都是保存调试信息的,实际运⾏过程中是不会加载到内存的
特别注意.text部分的"VMA"(或link address)和"LMA"(或load address),LMA是加载的地址,VMA是链接的地址。链接器以各种⽅式对链接地址进⾏⼆进制编码,例如当代码需要全局变量地址是,结果是如果
从⼀个没有链接的地址执⾏,⼆进制通常不能⼯作。值得⼀提的是,可以⽣成不包含任何绝对地址的代码,这就是通常说的动态链接库,但是6.828不使⽤
通常,链接和加载地址是相同的,例如可以查看.text部分的引导扇区内容objdump -h obj/boot/boot.out。可以看到VMA和L MA都是0x7c00,说明引导扇区从这个地⽅开始加载程序
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000186 00007c00 00007c00 00000074 2**2
CONTENTS, ALLOC, LOAD, CODE
1 .eh_frame 000000a8 00007d88 00007d88 000001fc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 00000720 00000000 00000000 000002a4 2**2
CONTENTS, READONLY, DEBUGGING
3 .stabstr 0000088f 00000000 00000000 000009c
4 2**0
CONTENTS, READONLY, DEBUGGING
4 ment 0000003
5 00000000 00000000 00001253 2**0
CONTENTS, READONLY
引导扇区使⽤ELF程序头去决定怎么加载⽚段,程序头指定了要加载的内容和⽬标地址。你可以检查程序头,通过objdump -x obj/kern/kernel
obj/kern/kernel: file format elf32-i386
obj/kern/kernel
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c
Program Header:
LOAD off 0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
filesz 0x00007120 memsz 0x00007120 flags r-x
LOAD off 0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12
filesz 0x0000a948 memsz 0x0000a948 flags rw-
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
filesz 0x00000000 memsz 0x00000000 flags rwx
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001871 f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000714 f0101880 00101880 00002880 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000038d1 f0101f94 00101f94 00002f94 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 000018bb f0105865 00105865 00006865 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f0108000 00108000 00009000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000648 f0112300 00112300 00013300 2**5
CONTENTS, ALLOC, LOAD, DATA
6 ment 00000035 00000000 00000000 00013948 2**0
CONTENTS, READONLY
结果中的程序头部包含了ELF的相关信息,需要被加载进内存的⽤"LOAD"标记。其他信息,如vaddr是虚拟地址,paddr是物理地址,memsz和filesz是加载区域。在boot/main.c中的代码。
进⾏练习5
回到boot/main.c程序,每个程序段的ph->p_pa字段包含段⽬标的物理地址,这种情况下,也是实际的物理地址
BIOS把引导扇区加载到0x7c00的内存地址位置,因此这是引导扇区的加载地址。这也是引导扇区执⾏的起始位置,所以也是链接地址。链接地址是通
过boot/Makefrag⽂件中通过-Ttext来设置的,所以,链接器在⽣成代码过程中将产⽣正确的内存地址
现在回头来看看内核加载地址和链接地址。不像启动引导,这两个地址是不同的:内核告诉引导加载程序加载低地址(1MB)的内存,但是可能从⼀个⾼地址执⾏,将会在下⼀个部分深挖
除了段信息,ELF头部也有⼀个重要的部分,叫做e_entry,这个部分保留了程序⼊⼝的链接地址。可以通过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
现在看来最⼩的boot/main.c中加载ELF,就是把内核从硬盘加载到内存,然后跳到内核⼊⼝
进⾏练习6
内核
尝试了解更多关于最⼩JOS内核的⼀些细节。跟引导加载程序⼀样,内核也有设置事件的汇编代码和能够执⾏的C语⾔代码
使⽤虚拟内存
使⽤虚拟内存是为了解决位置依赖问题。
上⾯通过objdump指令查询了内核的加载地址和链接地址。内核的链接⽐引导加载程序的链接要复杂,所以链接和加载地址在kern/kernel.ld的最顶部
操作系统链接和运⾏在⾼的虚拟地址,例如0xf0100000,以便于将处理器虚拟地址空间的低位部分留给⽤户使⽤。具体原因,下⼀个lab会更加清晰
很多机器都没有0xf0100000的物理地址,所以不能直接把内核保存在这⾥。相反,我们需要⽤处理器内存管理硬件把虚拟地址0xf0100000(内核链接和运⾏的地址)映射到物理地址0x00100000(内核加载程序加载的物理地址)。如此,尽管内核虚拟地址⾜够⽤户程序使⽤,它也会被加载到计算机RAM(在ROM上⾯)的
1MB物理地址内。这个⽅法,PC需要⾄少⼏MB的物理地址,但是这可以适⽤于1990年以后⽣产的任何计算机
实际上,在下⼀个lab中,将会把256MB映射到PC的物理内存地址,你现在明⽩为啥JOS可以仅使⽤前256MB的物理内存
现在,仅仅只映射前4MB的物理内存,这也⾜够运⾏了。我们使⽤⼿写的,静态初始化的页表⽬录和页
表来实现,具体代码可以参考kern/entrypgdir.c。现在不必理解这个⼯作的细节。kern/entry.S设置CR0_PG为1,内存引⽤被当作物理内存(严格来说,应该是线性地址,但是boot/boot.S会把线性地址映射到物理地址)。⼀旦CR0_PG被置位成功,内存引⽤就是虚拟地址。entry_pgdir转换虚拟地址到物理地址,同样也把物理地址转换成虚拟地址。任何不在这个范围内
(0f0000000~0xf0400000)的虚拟地址都会导致硬件异常,由于没有处理这些异常,就会导致QEMU退出
进⾏练习7
格式化打印输出
⼤多数⼈都会使⽤printf()来输出内容,有时候甚⾄会优先考虑C语⾔,但是在OS内核中,必须实现所有I/O
阅读kern/printf.c lib/printfmt.c 和 kern/console.c 确保你理解了其中的关系,你后⾯会理解为什么printfmt.c是位于lib⽬录
kern/printf.c⾥⾯主要是print的接⼝,⼀共有三个putch vcprintf cprintf 其中putch最终调⽤的是kern/console.c⾥⾯实现的cputchar打印⼀个字符;vcprintf调⽤的是在lib/printfmt.c中实现的vprintfmt,⽤来格式化字符串,最终也会调⽤putch
进⾏练习8
栈
这个实验的最后⼀个练习,这个练习会更详细讲解C语⾔在x86中使⽤栈,并且在程序中写⼀个新的监控函数来打印栈的backtrace:即函数调⽤栈信息
进⾏练习9
x86栈指针(esp寄存器)指向当前正在使⽤栈的最低内存位置,保留区域中低于这个位置的都是可以使⽤的。⼊栈操作会先把栈指针变⼩,然后把这个值写⼊栈指针指向的位置。出栈动作是先从栈中读取数值,然后再把栈指针增加。在32位模式中,栈只能保存32位的数值,esp寄存器也总是被拆分成4个。不同的x86指令,例如call,是硬连接(hard-wired)使⽤栈指针寄存器的
相⽐之下,ebp(基指针)寄存器,主要通过软件约定与栈相关联。在进⼊C函数⼊⼝前,prologue代码通常通过⼊栈操作保存之前函数的基指针,然后在函数使⽤期间,拷贝当前esp的值到ebp。程序中所有的函数都遵循这个规则,在程序运⾏中的任何⼀个给定的时刻,都是可以通过b保存的ebp的调⽤链来获取调⽤栈的数据。这个能⼒⾮常有⽤,例如当程序assert失败或者panic的时候,调⽤栈可以跟踪哪个地⽅有问题
进⾏练习10
后⾯都是⼀些废话了,具体就是实现mon_backtrace达到不同的功能
进⾏练习11
进⾏练习12
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论