MIT6.828——LAB1:Booting a PC
Part1:PC Bootstrap
练习1:
熟悉X86汇编语言
The PC’s Physical Address Space
电脑的物理地址空间是硬连线的具有以下常规布局:
BIOS
:负责进行一些基本的系统初始化任务,比如开启显卡,检测该系统的内存大小等等工作。在初始化完成后,BIOS就会从某个合适的地方加载操作系统。
虽然Intel处理器突破了1MB内存空间,在80286和80386上已经实现了16MB,4GB的地址空间,但是PC的架构必须仍旧把原来的1MB的地址空间的结构保留下来,这样才能实现向后兼容性。所以现代计算机的地址 0x000A00000x00100000区间是一个空洞,不会被使用。因此这个空洞就把地址空间划分成了两个部分,第一部分就是从0x000000000x000A0000,叫做传统内存。剩下的不包括空洞的其他部分叫做扩展内存。而对于这种32位字长处理器通常把BIOS存放到整个存储空间的顶端处。
The ROM BIOS
实模式和保护模式
实模式是早期CPU运行的工作模式,而保护模式则是现代CPU运行的模式
实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器。所以为了能够通过这些16位的寄存器去构成20位的主存地址,必须采取一种特殊的方式。当某个指令想要访问某个内存地址时,它通常需要用下面的这种格式来表示:
(段基址:段偏移量)
其中第一个字段是段基址,它的值是由段寄存器提供的。段寄存器有4种,%cs,%ds,%ss,%es。具体这个指令采用哪个段寄存器是由这个指令的类型来决定的。比如要取指令就是采用%cs寄存器,要读取或写入数据就是%ds寄存器,如果要对堆栈操作就是%ss寄存器。总之,不管什么指令,都会有一个段寄存器提供一个16位的段基址。
第二字段是段内偏移量,代表你要访问的这个内存地址距离这个段基址的偏移。它的值就是由通用寄存器来提供的,所以也是16位。那么问题来了,两个16位的值如何组合成一个20位的地址呢?这里采用的方式是把段寄存器所提供的段基址先向左移4位。这样就变成了一个20位的值,然后再与段偏移量相加。所以算法如下:
物理地址 = 段基址<<4 + 段内偏移
所以假设 %cs中的值是0xff00,%ax = 0x0110。则(%cs:%ax)这个地址对应的真实物理地址是 0xff00<<4 + 0x0110 = 0xff110。
上面就是实模式访问内存地址的原理。
发现 实模式中每次都是直接操作物理地址,如果操作失误、不当 可能会造成系统崩塌 等极为严重的事件。
并且随着CPU的发展,CPU的地址线的个数也从原来的20根变为现在的32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位。所以实模式下的内存地址计算方式就已经不再适合了。所以就引入了现在的保护模式,实现更大空间的,更灵活的内存访问。
在计算机中存在两个表,GDT,LDT。它们两个其实是同类型的表,前者叫做全局段描述符表,后者叫做本地段描述符表。他们都是用来存放关于某个运行在内存中的程序的分段信息的。比如某个程序的代码段是从哪里开始,有多大;数据段又是从哪里开始,有多大。GDT表是全局可见的,也就是说每一个运行在内存中的程序都能看到这个表。所以操作系统内核程序的段信息就存在这里面。还有一个LDT表,这个表是每一个在内存中的程序都包含的,里面指明了每一个程序的段信息。
我们从图中可以看到,无论是GDT,还是LDT。每一个表项都包括三个字段:
Base : 32位,代表这个程序的这个段的基地址。
Limit : 20位,代表这个程序的这个段的大小。
Flags :12位,代表这个程序的这个段的访问权限。
保护模式并不像实模式那样,segment : offset,直接将 segment 作为基地址,与offset偏移量直接相加。 而是,把segment的值作为一个selector(选择器),代表这个段的段表项在GDT/LDT表的索引。
系统会给程序自动分配程序段,代码段等,这些段以及偏移组成了逻辑地址,而逻辑地址通过GDTR/LDTR , 首先根据Flags字段判断能否访问此段内容(这样子是为了对进程间的地址进行保护),如果能访问,则把Base字段(段基址)的内容取出 直接与offset相加得到 线性(虚拟)地址。
练习2:
使用GDB(步骤指令)命令 跟踪到 ROM BIOS 有关更多说明, 并尝试猜测它可能在做什么?
使用si命令查看启动时的汇编代码:
汇编命令 src dst
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b //跳转
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) si
[f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8 //比较立即数0x0与寄存器%cs:0x6ac8的值
0x0000e05b in ?? ()
(gdb)
[f000:e062] 0xfe062: jne 0xfd2e1 //jump if not equal--从上面的比较结果来跳转
0x0000e062 in ?? ()
(gdb)
[f000:e066] 0xfe066: xor %dx,%dx //可见,没有进行跳转。xor(异或),异或本身,置零
0x0000e066 in ?? ()
(gdb)
[f000:e068] 0xfe068: mov %dx,%ss //将%dx移入%ss,初始化栈段
0x0000e068 in ?? ()
(gdb)
[f000:e06a] 0xfe06a: mov $0x7000,%esp//设置栈段的栈顶位置
0x0000e06a in ?? ()
(gdb)
[f000:e070] 0xfe070: mov $0xf34c2,%edx
0x0000e070 in ?? ()
(gdb)
[f000:e076] 0xfe076: jmp 0xfd15c //无条件跳转
0x0000e076 in ?? ()
(gdb)
[f000:d15c] 0xfd15c: mov %eax,%ecx
0x0000d15c in ?? ()
(gdb)
[f000:d15f] 0xfd15f: cli //clear interupt 关闭中断指令,防止在启动时被中断
0x0000d15f in ?? ()
(gdb)
[f000:d160] 0xfd160: cld //clear direction 设置方向标识位,置零则代表内存地址的变化方向从低地址值变为高地址
0x0000d160 in ?? ()
[f000:d161] 0xfd161: mov $0x8f,%eax
0x0000d161 in ?? ()
(gdb)
//操作CMOS存储器中的内容需要两个端口,一个是0x70另一个就是0x71。
[f000:d167] 0xfd167: out %al,$0x70 //将%al读入$0x70
0x0000d167 in ?? ()
(gdb)
[f000:d169] 0xfd169: in $0x71,%al //将$0x71写入%al
0x0000d169 in ?? ()
[f000:d16b] 0xfd16b: in $0x92,%al
0x0000d16b in ?? ()
(gdb)
[f000:d16d] 0xfd16d: or $0x2,%al //或
0x0000d16d in ?? ()
(gdb)
[f000:d16f] 0xfd16f: out %al,$0x92
0x0000d16f in ?? ()
[f000:d171] 0xfd171: lidtw %cs:0x6ab8 //加载中断向量表寄存器IDTR
0x0000d171 in ?? ()
[f000:d177] 0xfd177: lgdtw %cs:0x6a74 //把从0xf6a74为起始地址处的6个字节的值加载到全局描述符表格寄存器中GDTR中
0x0000d177 in ?? ()
[f000:d17d] 0xfd17d: mov %cr0,%eax //计算机中包含CR0~CR3四个控制寄存器,用来控制和确定处理器的操作模式
0x0000d17d in ?? ()
(gdb)
[f000:d180] 0xfd180: or $0x1,%eax
0x0000d180 in ?? ()
(gdb)
[f000:d184] 0xfd184: mov %eax,%cr0 //CR0寄存器的0bit是PE位,启动保护位,当该位被置1,代表开启了保护模式
0x0000d184 in ?? ()
(gdb)
很多指令的意义不清楚。不明白为什么这么做。
CR0
中含有控制处理器操作模式和状态的系统控制标志;
CR1
保留不用;
CR2
含有导致页错误的线性地址;
CR3
中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base addressRegister)
每一种中断都有自己对应的中断处理程序,那么这个中断的处理程序的首地址就叫做这个中断的中断向量。
PC启动后的运行顺序为 BIOS —从磁盘找到可启动设备,并加载至内存----> boot loader —从磁盘将kernel加载到内存-> 操作系统内核
Part2:The Boot Loader
对于PC来说,软盘,硬盘都可以被划分为一个个大小为512字节的区域,叫做扇区。一个扇区是一次磁盘操作的最小粒度。当BIOS找到一个可以启动的软盘或硬盘后,它就会把这512字节的启动扇区加载到内存地址0x7c00~0x7dff这个区域内。
整个boot loader是由一个汇编文件,boot/boot.S,以及一个C语言文件,boot/main.c。Boot loader必须完成两个主要的功能:
- 创建两个全局描述符表项(代码段和数据段),然后进入保护模式。因为只有在这种模式下软件可以访问超过1MB空间的内容。
- 从磁盘加载kernel到内存
boot loader
:引导加载程序(boot loader) 会引导操作系统。当机器引导它的操作系统时,BIOS 会读取引导介质上最前面的 512 字节(即人们所知的 主引导记录(master boot record,MBR))。在单一的 MBR 中只能存储一个操作系统的引导记录,所以当需要多个操作系统时就会出现问题。所以需要更灵活的引导加载程序。
为什么要开启A20地址线呢?
- **在 A20 Gate 被禁止时,**表现依然会与
8086/8088
一样,依然拥有地址环绕
,也就是,只有A0 - A19 能运作。默认是被禁止,因为BIOS是最先执行的程序。即实模式 - **在 A20 Gate 被开启时,**就可以访问1MB以上的内存空间。即保护模式
目标文件格式(ELF)
目标文件和可执行文件格式几乎一样,在linux下统称为ELF文件
动态链接库(DLL,Dynamic Linking Library)(windows的.dll和Linux的.so)
静态链接库(Static Linking Library)(windows的.lib和Linux的.a)
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件relocatable flie | 包含代码和数据,可连接成可执行文件或共享目标文件,静态链接库也归为这一类 | Linux的.o |
可执行文件executable file | 可执行,一般无扩展名 | /bin/bash文件 |
共享目标文件shared object file | 包含代码和数据,可连接成可执行文件或共享目标文件,动态链接库也归为这一类 | lLinux的.so |
核心转储文件core dump file | 进程意外终止时,将该进程的地址空间的内容以及终止信息转存到该文件 | Linux下的core dump |
目标文件内容
机器指令、数据、链接时需要的信息(符号表、调试信息、字符串),将这些信息按照不同的属性以section
形式存储,有时也叫segment
代码段(code section):存放编译后的机器指令,常见名字有.code
、.text
数据段(data section):存放全局变量和局部静态变量,.data
.bss
段:存放未初始化的全局或局部静态变量,预留位置,没有必要存放数据0
段表(section table):描述文件中各个段的数组。如各个段在文件中的偏移位置及段的属性
自定义段:_attribute_((section("name")))
,在全局变量或函数之前加上可以将其放到以name
作为段名的段中
_attribute_((section("FOO"))) int global = 42;
_attribute_((section("BAR"))) void foo()
{
}
ELF文件结构描述
ELF HEADER |
---|
.test |
.data |
.bss |
… other sections |
section header table |
string tables symbol tables … |
文件头(ELF HEADER)
成员 | readelf输出结果 |
---|---|
e_ident | Magic: Class: Data: Version: OS/ABI ABI Version: |
e_type | Type: |
e_machine | Machine: |
e_version | Version: |
e_entry | Entery point address:ELF程序的入口虚拟地址。可重定位文件一般没有,值为0 |
e_phoff | Start of program headers: |
e_shoff | Start of section headers: |
e_flags | Flages: |
e_ehsize | Size of this header: |
e_phentsize | Size of program headers: |
e_phnum | Number of program headers: |
e_shentsize | Size of section headers:段表描述符大小 |
e_shnum | Number of section headers:段表描述符数量 |
e_shstrndx | Section header string table index:段表字符串表所在的段在段表中的下标 |
Segment概念是从装载(从磁盘加载到内存中)的角度重新划分了ELF的各个段(Section),将目标文件链接成可执行文件的时候,连接器会尽量把相同权限属性的段分配在同一空间。
Section是从链接角度划(数据属性)分ELF:比如划分为代码段、数据段、.bss段
描述Section
的结构叫做段表
描述Segment
的结构叫程序头(program header):ELF可执行文件有程序头表(Program Header Table)描述了ELF文件该如何被操作系统映射到进程的虚拟空间,其结构体如下:
typedef struct {
Elf32_Word p_type; //segment类型,只关注“LOAD”类型
Elf32_Off p_offset; //segment在elf文件中的偏移
Elf32_Addr p_vaddr; //segment的第一个字节在虚拟地址空间的起始位置
Elf32_Addr p_paddr; //segment的物理装载地址
Elf32_Word p_filesz; //segment在ELF文件中所占空间长度
Elf32_Word p_memsz; //segment在虚拟地址空间中所占长度
Elf32_Word p_flags; //segment权限属性
Elf32_Word p_align; //segment对齐属性
}Elf32_Phdr
Load The Kernel
ELF文件中的每个字段都有两个比较重要的信息:
- VMA:链接地址(Virtual Memory Address):虚拟内存地址,代码要运行的时候,此时对应的地址,就是VMA。
- LMA:加载地址(Load Memory Address):内存装载地址(物理地址),代码被装载到内存的某个地方,那个地方的地址,就是LMA。
boot loader将内核加载到内存中后的布局如下:
Part 3:The Kernel
操作系统一般加载(物理地址)到低地址上,但希望运行在高地址上。然而实际的物理地址没有那么高的高地址。因此,引入虚拟地址:还是把操作系统放在高地址处0xf0100000(虚拟地址),但是在实际的内存中我们把操作系统存放在一个低的物理地址空间处,如0x00100000。那么,如何管理虚拟地址到物理地址的映射?
采用分段与分页管理。
The Stack
esp:指向当前栈帧的顶部。
ebp:指向当前栈帧的底部。
eip:CPU下次要执行的指令的地址,指向当前栈帧中执行的指令(可以理解为读取esp地址中所对应的信息)
eip应该是指向的代码段中的某个位置,表示cpu下一次要执行的指令。
esp和ebp是运行时的一个栈帧,当主函数调用子函数时,需要记录主函数下一次执行的指令,因此,在主函数执行call来调用子函数时,需要将主函数的eip入栈。ret就是当子函数调用结束后,能够从栈帧中取出主函数的下一个指令到eip中。
当子函数被主函数调用时,在主函数执行call指令之前,将子函数的参数从右到左入栈,并将当前eip入栈。子函数需要储存主函数的一些栈帧信息,只有这样,当子函数调用完成后才能返回到主函数调用子函数的那个位置。子函数开头两个指令为:
push %ebp
mov %esp,%ebp
第一条指令目的是储存主函数的栈底。第二条指令目的是用ebp记录子函数的栈底。
子函数的栈底就是主函数的栈顶!
返回时,则操作相反:
mov %ebp,%esp //子函数的栈底就是主函数的栈顶
pop %ebp //取出主函数的栈底位置
ret //从当前位置获得主函数的eip
stab表是什么?
GCC把C语言源文件( ‘.c’ )编译成汇编语言文件( ‘.s’ ), 汇编器把汇编语言文件翻译成目标文件( ‘.o’ )。在目标文件中, 调试信息用 ‘.stab’ 打头的一类汇编指导命令表示, 这种调试信息格式叫’Stab’, 即符号表(Symbol table)。这些调试信息包括行号、变量的类型和作用域、函数名字、函数参数和函数的作用域等源文件的特性。
参考资料
MIT 6.828 JOS 操作系统学习笔记 - 随笔分类 - fatsheep9146 - 博客园 (cnblogs.com)
MIT-6.828-JOS-lab1:C, Assembly, Tools, and Bootstrapping - gatsby123 - 博客园 (cnblogs.com)