| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 开发工具 -> mit6.828_lab1_系统启动 -> 正文阅读 |
|
[开发工具]mit6.828_lab1_系统启动 |
1.BIOS启动流程BIOS(Basic Input Output System)“基本输入输出系统”,它是一组固化到主板上一个ROM芯片上的程序,使用汇编语言编写。PC通电后,CPU马上就从地址0xFFFF0处开始执行指令,这个地址在系统BIOS的地址范围内,将BIOS程序加载到内存中执行,BIOS接下来的作用包括机器自检,对系统进行初始化,识别并加载主板上的重要硬件和集成元件,如硬盘、显卡、声卡以及各种接口,然后按照预设顺序读取存储器上操作系统的引导文件Boot Loader,储存Boot Loader的存储器可以是软盘、硬盘、CD-ROM甚至是网络输入,可以在BIOS的设置页面更改读取Boot Loader的搜索顺序。 那么在这其中就有一个问题,那就是,BIOS程序是固化到自己的ROM芯片中的,那CPU是如何在刚通电的情况下就能读取到BIOS程序并且运行的?再者,上段的说法也有些问题,“CPU马上就从地址0xFFFF0处开始执行指令,这个地址在系统BIOS的地址范围内”,那地址0xFFFF0指的是什么地址?是外加内存条RAM的内存地址还是储存BIOS程序的ROM地址?这又涉及到现代PC的内存地址是如何编排的。我也是查了很多资料才摸到一些门道,下面的说法糅合了我从网上搜索的资料和自己的整合以及猜测,不保证正确,仅为缕清自己的思路。 在计算机通电完成之后,CPU会自动将其CS寄存器设定为0xFFFF,IP寄存器设定为0x0000。关于CS寄存器和IP寄存器的介绍可以看文末的链接6,简单来说CS是代码段寄存器,IP是指令指针寄存器(相当于偏移地址),而CPU在访问内存的时候访问的是物理内存地址,而这个物理地址就是由CS和IP这两个寄存器中的值合成的(物理地址=段地址X16+偏移地址)。所以如果将CS寄存器设定为0xFFFF,IP寄存器设定为0x0000,那么这时的物理地址就=0xFFFF*16+0x0000=0xFFFF0,而在0xFFFF0存放的是什么呢?其实是一条无条件转移指令JMP,这个JMP会将程序跳转到BIOS真正的入口点。至于跳转到哪里,那么不同的BIOS会有不同的跳转地址。 上图为8086的内存架构,我们来详细讨论一下:早期基于16位英特尔8086处理器的个人电脑,只能寻址1MB的物理内存。因此,早期计算机的物理地址空间将从0x00000000开始,以0x000FFFFF结束。用户能够使用的内存地址只有标记为“低内存”的640KB区域(Low Memory),而之后从0x000A0000到0x00100000的384KB空间则被预留为硬件以及ROM的地址,用户不可用。具体可以看下图: 从上图可以看到,0xF0000到0x100000的地址范围便进入到了真正的BIOS的区域,而在这个区域中,BIOS又是以何种方式存在的呢?我们上面说到,计算机通电后,CPU会到0xFFFF0的位置执行,而0xFFFF0这个位置处在BIOS地址范围中,它只占0xFFFF0到0x100000短短的16B。这个地址处存放的只是一个跳转指令,它会将CPU跳转至真正的BIOS程序开始的位置,这个位置在不同的BIOS中是不同的。这里引用一个比较清晰的描述: 8086是16位的CPU,但是却有20根地址线。也就是说它可以寻址1MB内存空间。这段内存空间由RAM、ROM组成。ROM是随机只读存储器,里面的程序是在计算机出厂的时候直接烧录在里面的,完成一些主机自检等操作,并提供一些访问磁盘等基本输入输出服务,因而这段程序常被称为BIOS(Basic Input/Ouput Service)。由于不同的计算机厂商生产的计算机所带的外设不一样,因此,这段程序大小也限机型的不同而不一样,有可能A厂出产的计算机所带的这段程序的大小为1K,而B厂出产的这段程序的大小为2K。如果将这段程序放在0x0000处,那么用户写的程序就可能从0x0400处开始也可能从0x0800处开始,非常不统一。故而,将此段程序放在1M内存的顶部,那么用户写的程序就都可以从0x0000处开始了。 但将BIOS这段程序放在1M内存的顶部,如果这段程序大小为1K,那么应当从0xFFC00开始放。如果这段程序的大小为2K,那应当从0xFF800开始放,对于CPU而言,到底是应当从0xFFC00开始执行还是应当从0xFF800开始执行呢?为了解决这个问题,8086规定,CPU均从0xFFFF0处开始执行,而在0xFFFF0处,放一条无条件转移指令JMP。如果A厂的BIOS是从0xFFC00开始放的,那么这条转移指令就跳转到0xFFC00处开始执行。如果B厂的BIOS是从0xFF800开始放的,那么这条转移指令就跳转到0xFF800处开始执行,各个厂家可以根据自己所生产的BIOS程序的大小,来决定此转移指令具体跳转到的位置。 这里有一点需要清楚的是,通常认为,内存编址是连续的,不会出现空洞,其实完全不是这样。比如,假设BIOS的编址是从 0xF0000开始,而RAM,即通常讲的内存编址是从0x00000开始,那么,如果用户只安装了32K内存,那么内存的编址范围就是0x00000~0x07FFF,那么从0x08000至0xEFFFF处就没有安装内存,这就是一个内存空洞。 原文在这里
2.Boot Loader个人电脑的软盘和硬盘被分为一个个512B的区域,称为扇区。扇区是磁盘的最小传输粒度:每个读写操作必须是一个或多个扇区,并在扇区边界上对齐。Boot Loader程序代码所在的磁盘称为引导磁盘,它的第一个扇区称为引导扇区,因为这是引导加载程序代码所在的位置。而存放在第一个扇区的Boot Loader加载程序的大小不能超过510个字节,由于磁盘的一个扇区的大小为512字节,这样便保证了bootloader仅仅只占据磁盘的第一个扇区。 在经过BIOS对系统进行初始化之后,它会按照预设顺序读取存储器上操作系统的引导文件Boot Loader,将其加载到内存中进行读取。那么BIOS是如何寻找并且判断Boot Loader的位置呢?BIOS按顺序检查磁盘的第一个扇区,如果该扇区的最后两个字节是"55 aa",那么这就是一个引导扇区,这个磁盘也就是一块可引导盘。通常这个大小为512B的程序就称为引导程序(Boot Loader)。如果最后两个字节不是"55 aa",那么BIOS就检查下一个磁盘驱动器,这个检查顺序也是可以在BIOS中设置的。当BIOS找到可引导软盘或硬盘时,它将512字节的引导扇区加载到物理地址0x7c00到0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制传递给引导加载程序。 至于为什么要将Boot Loader放到0x7c00处,是出于历史原因,IBM最早的个人电脑IBM PC 5150用的是Intel最早的个人电脑芯片8088,当时,搭配的操作系统是86-DOS。这个操作系统需要的内存最少是32KB。内存地址从0x0000开始编号,32KB的内存就是0x0000~0x7FFF。8088芯片本身预留了地址空间0x0000~0x03FF用来保存各种中断向量的储存位置。所以,内存只剩下0x0400~0x7FFF可以使用。为了把尽量多的连续内存留给操作系统,Boot Loader就被放到了内存地址的尾部。由于Boot Loader所在的这个扇区是512字节,另外Boot Loader数据和栈需要预留512字节。所以,Boot Loader加载位置是0x7FFF - 512 - 512 + 1 = 0x7c00,而且因为操作系统加载完成后Boot Loader不需要再使用,这部分内存之后操作系统是可以重复利用的。 在6.282实验中,我们所用的Boot Loader由一个程序集语言源文件boot/boot.S,和一个C源文件boot/main.c组成。实验中编译好的Boot Loader位于obj/boot/boot,我们可以使用vim打开它然后转换到16进制视图进行查看。
可以看到,boot文件确实是以"55 aa"结尾的。 boot.S主要是将处理器从实模式转换到32位的保护模式,因为只有在保护模式中我们才能访问到物理内存高于1MB的空间。main.c的主要作用是将内核的可执行代码从硬盘镜像中读入到内存中,具体的方式是运用x86专门的I/O指令读取。
boot/Makefrag 这个文件包含几个命令,作用是生成 obj/boot/ 目录下面的几个文件,该目录下 boot.out 是由 boot/boot.S 和 boot/main.c编译链接后生成的 ELF 可执行文件,而 boot.asm 是从可执行文件 boot.out 反编译的包含源码的汇编文件,而最后通过 objcopy 拷贝 boot.out中的 .text 代码节生成最终的二进制引导文件 boot (380个字节),最后通过 sign.pl这个perl脚本填充 boot 文件到512字节(最后两个字节设置为 55 aa,代表这是一个引导扇区)。最终生成的镜像文件在 obj/kern/kernel.img,它大小为5120000字节,即10000个扇区大小。第一个扇区写入的是 obj/boot/boot,第二个扇区开始写入的是 obj/kern/kernel。 关于ELF文件我们这里做一个简单介绍,当编译和链接一个C程序时,编译器将每个 .c 文件转换成一个 .o 对象文件,该文件包含以硬件期望的二进制格式编码的汇编语言指令。然后,链接器将所有 .o 对象文件合并成一个二进制映像,如obj/kern/kernel,这就是ELF文件,意思是“可执行和可链接格式”(“Executable and Linkable Format”)。 ELF可执行文件是由 ELF文件头、程序头表(program header table)、节头表(section header table)和文件内容四部分组成的。而文件内容部分又由.text 节、.rodata 节、.stab 节、.stabstr 节、.data 节、.bss 节、.comment 节等部分组成。在这里我们只对以下几个部分感兴趣:
当链接器计算程序的内存布局时,它会在.bss节中为未初始化的全局变量保留空间,如 可以使用 objdump 命令查看 ELF 文件的节信息:
上面列出的Size、VMA、LMA、File off分别表示节的大小,虚拟地址(Virtual Memory Address)、装载地址(Load Memory Address)以及节的偏移量。VMA是指编译器指定代码和数据所需要放置的内存地址,由链接器配置;而LMA是指程序被实际加载到内存的位置。在上图中可以看到,Boot Loader的VMA和LMA都是 0x7c00,而kernel的VMA和LMA却不相同。
Boot Loader的VMA和LMA是一样的,都是0x7c00。而Kernel的VMA和LMA却是不一样的。VMA是 0xf0100000,LMA地址是0x00100000,也就是说Kernel加载到了内存中的 0x00100000 这个低地址处,但是却期望在一个高地址 0xf0100000 执行,为什么要这么做呢?这是因为我们的内核通常期望运行在一个高的虚拟地址,以便把低位的虚拟地址空间让给用户程序使用。但是,以前的机器通常没有 0xF0100000 这么大的物理内存,这时候就需要通过处理器的内存管理硬件来将 0xf0100000 映射到 0x00100000,我们将在后面详细讨论。 在JOS中,Boot Loader的VMA是在 boot/Makefrag里面定义的,为 0x7c00,这是Boot Loader期望执行的地址。而BIOS也正是把Boot Loader装载到 0x7c00 地址处的,所以Boot Loader的VMA和LMA是一致的。如果我们在 boot/Makefrag文件中更改 0x7c00 为其它地址,Boot Loader在执行的过程中便会出错。 此外在ELF文件头中还有一个重要的字段,名为e_entry,此字段包含程序的入口地址(VMA):程序期望开始执行的内存地址。例如,可以使用以下命令查看Kernel的入口地址:
注意,入口地址不等于程序在内存中开始的地址,只是程序开始执行的地址。入口地址之前还有一段地址存储了程序的其他内容。 3.Kernel上一节我们说到过,Boot Loader的VMA和LMA都是 0x7c00,而Kernel的VMA和LMA却是不一样的,VMA是 0xf0100000,LMA地址是0x00100000,也就是说Kernel加载到了内存中的 0x00100000 这个低地址处,但是却期望在一个高地址 0xf0100000 执行,高地址和底地址的转换是由处理器的内存管理硬件来实现的。 物理地址 0xf0100000 要求至少3.75GB的内存,这在早期时候的PC上是达不到的。所以,我们将使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码希望运行的地址)映射到物理地址 0x00100000 处(Boot Loader将内核实际加载到的地址)。0x00100000 地址对应的内存大小是1MB,看看上面8086的内存分布图,所以内核实际上是被加载到了BIOS的上方。 在下一个实验中,我们将整个电脑底部256MB的物理地址空间,即从物理地址0x000000000到0x0fffffff,映射到虚拟地址0xf0000000到0xffffffff。现在,我们只需映射4MB的物理内存,这将足以让我们启动和运行内核。我们使用在 kern/entrypgdir.c 中一个手写的、静态初始化的页目录和页表项来实现这个映射。我们不需要知道实现细节,只需要知道,在kernel/entry.S 设置cr3寄存器的值为页目录的物理地址后,虚拟地址映射(分页)就开启了。在这之前,所有的内存引用都指的是真正的物理地址,而在开启分页之后,内存引用是由虚拟内存,这个虚拟地址可以由内存管理硬件转换为真正的物理地址。而 kern/entrypgdir.c 这个文件中的页目录和页表项便将 0xf0000000:0xf0400000 这个虚拟地址范围映射至物理地址 0x00000000:0x00400000,而虚拟地址 0x00000000:0x00400000 也映射到了物理地址 0x00000000:0x00400000。任何不在这两个范围中的虚拟地址都将导致硬件异常,由于我们尚未设置中断处理,这将导致QEMU转储机器状态并退出。 3.1 在控制台显示输出这里我们要完成cprintf函数,与printf函数不同,cprintf函数用于向当前窗口输出数据,printf是标准输出,就是指可以完全不知道你输出的对象,只是以标准的文本流方式输出,cprintf是与终端相关的,要用到一些系统平台,硬件设备相关的特性,所以可以有颜色和格式等很多选项可供选择,但同时也削弱了移植性。前者无可移植性,而后者是标准的。 这里我们来看一下几个跟cprintf函数相关的文件,kern/console.c 这个文件实现了cputchar,getchar等函数,而cputchar函数最终调用了cga_putc函数来完成显示功能。lib/printfmt.c 这个文件实现了vprintfmt函数,vprintfmt函数是一个最精简的原始printf函数,它会被printf, sprintf, fprintf等函数调用,是一个既被内核使用也被用户使用的函数。而cprintf是在 kern/printf.c 中实现的,它调用了vprintfmt函数,而vprintfmt函数调用了putch函数作为参数,putch函数最终调用了console.c中的cputchar。即 cprintf --> vprintfmt --> putch --> cputchar --> cga_putc。 查看 kern/init.c 文件,里面有一行:
这段代码是将十进制6828以八进制的形式打印出来,由于八进制的输出是lab1的一个作业,这部分需要我们自己去实现,所以在初始情况下会产生如下输出:
而当我们完成了8进制的输出后,上面那句输出会变成:
打开 kern/monitor.c 文件,在monitor函数内插入以下内容:
在系统执行到上述语句的时候,在cprintf, cons_putc, va_arg, 和 vcprintf 这几个函数处打上断点:
观察接下来的输出:
3.2 函数调用堆栈要搞清楚函数调用堆栈的原理,首先需要知道 esp 和 ebp 这两个寄存器,esp 指向栈顶,ebp 指向栈底,还有一个eip指向的是执行函数调用的入口地址,具体可参阅这里。现在让我们来看看在JOS中是怎么对栈定义的。 在 kern/entry.S 文件中,77行处将一个宏变量 接下来查看反汇编代码obj/kern/kernel.asm,58行处向 esp 中写入0xf011f000。这就是栈顶的位置,栈将向地址值更小的方向生长。
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 5:53:25- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |