1、引导篇的疑惑本文参照《Orange’S:一个操作系统的实现》与《一个64位操作系统的设计与实现》实现(部分内容有删改),如有描述不清或错误处,请阅读原著或联系本人
在学习过程中,由于操作系统知识过于庞大,我们奉行”懒加载“的原则,用到什么学习什么
2021-11-25
在引导篇中,我们仍有许多疑惑
- 为什么要把内核移至0x100000处
- 为什么DELLA_KERNEL_PARAMETER_INFORMATION要放在0x60000处
- 页表中的frameBuffer地址映射更新为什么出现那么多特别的数字
- 物理地址空间信息为什么要转为E820内存结构
接下来我们将一点点拨开眼前的迷雾
2、核心工作- 日志打印
- 异常处理
- 内存管理
- 中断处理
- 键盘驱动
- 进程管理
- 文件系统
- API库
- Shell
环境依赖
sudo yum install gtk2 gtk2-devel libXt libXt-devel libXpm libXpm-devel SDL SDL-devel xorg-x11-server-devel nasm gcc-c++ glibc-headers libX11-devel libXrandr-devel lrzsz -y
安装bochs-2.6.8
tar -xvf bochs-2.6.8.tar.gz
cd bochs-2.6.8
./configure --with-x11 --with-wx --enable-debugger --enable-disasm
--enable-all-optimizations --enable-readline --enable-long-phy-address
--enable-ltdl-install --enable-idle-hack --enable-plugins --enable-a20-pin
--enable-x86-64 --enable-smp --enable-cpu-level=6 --enable-large-ramfile
--enable-repeat-speedups --enable-fast-function-calls
--enable-handlers-chaining --enable-trace-linking
--enable-configurable-msrs --enable-show-ips --enable-cpp
--enable-debugger-gui --enable-iodebug --enable-logging
--enable-assert-checks --enable-fpu --enable-vmx=2 --enable-svm
--enable-3dnow --enable-alignment-check --enable-monitor-mwait
--enable-avx --enable-evex --enable-x86-debugger --enable-pci
--enable-usb --enable-usb-ohci --enable-usb-ehci --enable-usb-xhci
--enable-voodoo
cp misc/bximage.cpp misc/bximage.cc
cp iodev/hdimage/hdimage.cpp iodev/hdimage/hdimage.cc
cp iodev/hdimage/vmware3.cpp iodev/hdimage/vmware3.cc
cp iodev/hdimage/vmware4.cpp iodev/hdimage/vmware4.cc
cp iodev/hdimage/vpc-img.cpp iodev/hdimage/vpc-img.cc
cp iodev/hdimage/vbox.cpp iodev/hdimage/vbox.cc
make && make install
3.2、Makefile学习
3.2.1、基本格式
target: prerequisites command
- target:目标文件
- prerequisites:先决条件(依赖条件)
- command:make需要执行的命令 (任意的shell命令,必须以[tab]开头)
- * :表示任意一个或多个字符
- ? :表示任意一个字符
- […]:[abcd] 表示a,b,c,d中任意一个字符, [^abcd]表示除a,b,c,d以外的字符, [0-9]表示 0~9中任意一个数字
- ~ :表示用户的home目录
:= 只能使用前面定义好的变量, = 可以使用后面定义的变量
1、变量定义、使用
object1 = programA.o programB.o
object2 = $(object) programC.o
all:
@echo "object2: " $(object2)
# object2 = programA.o programB.o programC.o
2、变量替换
object1 := programA.c programB.c programC.c
object2 := $(object1:%.c=%.o)
all:
@echo "object2: " $(object2)
# object2 = programA.o programB.o programC.o
3、变量追加值
object1 = programA.c programB.c programC.c
object2 += programD.c
all:
@echo "object2: " $(object2)
# object2 = programA.c programB.c programC.c programD.c
4、override
使 Makefile中定义的变量能够覆盖 make 命令参数中指定的变量
object1 = programA.c programB.c # ============= 命令:make object1=programC.c # object1=programC.c
override object2 = programA.c programB.c # ============= 命令:make object2=programC.c # object2=programA.c programB.c
5、目标变量
target1: object1 = programA.c target1: @echo "object1: " $(object1) # object1 = programA.c target2: @echo "object1: " $(object1) # object1:3.2.4、命令前缀
- 不用前缀:输出执行的命令以及命令执行的结果, 出错的话停止执行
- 前缀@:只输出命令执行的结果, 出错的话停止执行
- 前缀-:命令执行有错的话, 忽略错误, 继续执行
伪目标并不是一个"目标(target)",不像真正的目标那样会生成一个目标文件
# .PHONY没有也行, 但是最好加上
.PHONY: clean
clean:
-rm -f *.o
3.2.6、引用Makefile
include (filename 可以包含通配符和路径)
3.2.7、自动变量| 自动变量 | 含义 |
|---|---|
| $@ | 目标集合 |
| $% | 当目标是函数库文件时, 表示其中的目标文件名 |
| $< | 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标 |
| $? | 比目标新的依赖目标的集合 |
| $^ | 所有依赖目标的集合, 会去除重复的依赖目标 |
| $+ | 所有依赖目标的集合, 不会去除重复的依赖目标 |
| $* | 这个是GNU make特有的, 其它的make不一定支持 |
$() ${ }
常用函数
-
字符串替换函数
# 把text中的from字符串换位to字符串 $(subst
, , ) -
模式字符串替换函数(可以使用%去匹配任意长度字符串)
$(patsubst
, , ) -
字符串查找函数
# 在in字符串中寻找find字符串 $(findstring
, ) -
过滤函数
$(filter
, ) -
反过滤函数
$(filter-out
, ) -
排序函数
$(sort
- )
-
取单词函数
$(word
, ) -
取单词串函数
$(word
,, ) -
单词个数统计
$(words
) -
首单词函数
$(firstword
) -
取目录函数
$(dir
) -
取文件名函数
$(notdir
) -
取后缀函数
$(suffix
) -
取前缀函数
$(basename
) -
加后缀函数
$(addsuffix
, ) -
加前缀函数
$(addprefix
, ) -
循环函数
$(foreach ,
- ,
) -
条件判断函数
$(if
, ) $(if , , ) $(ifdef) $(ifndef) -
判断变量的来源
$(origin
) 类型 含义 undefined 没有定义过 default 是个默认的定义, 比如 CC 变量 environment 是个环境变量, 并且 make时没有使用 -e 参数 file 定义在Makefile中 command line 定义在命令行中 override 被 override 重新定义过 automatic 是自动化变量
all:
ifeq ("a","b")
echo "equal"
else
echo "not equal"
endif
# echo "not equal"
# not equal
3.2.10、约定
| 伪目标 | 含义 |
|---|---|
| all | 所有目标的目标,其功能一般是编译所有的目标 |
| clean | 删除所有被make创建的文件 |
| install | 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去 |
| 列出改变过的源文件 | |
| tar | 把源程序打包备份. 也就是一个tar文件 |
| dist | 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件 |
| TAGS | 更新所有的目标, 以备完整地重编译使用 |
| check 或 test | 一般用来测试makefile的流程 |
as命令是gcc套件中的汇编器,它采用的是AT/T的汇编语法
格式为as [选项] 汇编文件
- --32/--64。它生成32/64位代码
- -o OBJFILE。它将编译生成的目标二进制程序段保存在OBJFILE文件内,OBJFILE的默认文件名为a.out
as --64 -o entry.o entry.s # 把汇编源文件entry.s编译成64位的二进制程序段,并将其保存在文件entry.o里 # 之所以将entry.o文件称为二进制程序段而不是二进制程序,是因为entry.o文件不是可执行文件,它需要经过链接后,才能成为可执行程序3.3.2、gcc
格式为gcc [选项] 文件
- -E。它使编译器只执行预处理过程,不执行编译、汇编、链接等过程,也不会生成目标文件,此时需要将源文件预处理的结果重定向到一个输出文件中
- -C。在预处理过程中,它不删除注释信息,通常情况下和-E联合使用
- -mcmodel=large。mcmodel用于限制程序访问的地址空间,选项large表明程序可访问任何虚拟地址空间,其他选择无法使程序访问整个虚拟地址空间
- -fno-builtin。除非使用前缀__builtin_明确引用,否则编译器不识别所有系统内建函数。常见的系统内建函数有alloca、memcpy等
- -m32/-m64。它生成32/64位代码
- -c。它执行预处理、编译、汇编等过程,但不执行链接过程
gcc -E entry.S > entry.s # 对汇编源文件entry.S进行预处理,同时将预处理的结果重定向(导出)到目标文件entry.s中
gcc -mcmodel=large -fno-builtin -m64 -c main.c # 将main.c编译成64位的二进制程序段文件main.o。与此同时,这条命令还不限制地址空间的访问范围、不识别所有系统内建函数。3.3.3、ld
格式为ld [选项] 文件
-
-b TARGET。它指定输入文件的文件格式。ld命令支持的文件格式有:elf64-x86-64、elf32-i386、a.out-i386-linux、pei-i386、pei-x86-64、elf64-l1om、elf64-little、elf64-big、elf32-little、elf32-big、srec、symbolsrec、verilog、tekhex、binary、ihex等。(可通过help选项查询ld命令支持的文件格式)
-
-z muldefs。它允许重复定义。当遇见重复定义时,编译器只使用其中一个
-
-o FILE。它指定输出文件的文件名
-
-T FILE。它为链接过程提供链接脚本文件
ld -b elf64-x86-64 -z muldefs -o system head.o entry.o main.o printk.o trap.o memory.o interrupt.o task.o -T Kernel.lds # 将head.o、entry.o、main.o等编译好的二进制程序段文件按照链接脚本kernel.lds的描述对程序段进行部署,最终将它们链接成elf64-x86-64文件格式的可执行程序system,这个链接过程会过滤掉重复定义的函数3.3.4、objcopy
格式为objcopy [选项] 输入文件 [输出文件]
- -I TARGET。它指定输入文件的文件格式。objcopy命令支持的文件格式有:elf64-x86-64、elf32-i386、a.out-i386-linux、pei-i386、pei-x86-64、elf64-l1om、elf64-little、elf64-big、elf32-little、elf32-big、srec、symbolsrec、verilog、tekhex、binary、ihex等。(可通过help选项查询objcopy命令支持的文件格式)
- -S。它移除所有symbol和relocation信息
- -R name。它从输出文件中移除名为name的程序段
- -O TARGET。它指定输出文件的文件格式
objcopy -I elf64-x86-64 -S -R ".eh_frame" -R ".comment" -O binary system kernel.bin # 这条命令的作用是移除可执行程序system中的所有`symbol`和`relocation`信息,并移除名为.eh_frame和.comment的程序段,最后将剩余程序段以二进制格式输出到文件kernel.bin中3.4、链接脚本学习
链接脚本的主要作用是描述如何将输入文件中的各程序段(数据段、代码段、堆、栈、BSS)部署到输出文件中,并规划输出文件各程序段在内存中的布局
OUTPUT_FORMAT("elf64-x86-64","elf64-x86-64","elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SECTIONS
{
. = 0xffff800000000000 + 0x100000;
.text :
{
_text = .;
*(.text)
_etext = .;
}
. = ALIGN(8);
.data:
{
_data = .;
*(.data)
_edata = .;
}
.rodata:
{
_rodata = .;
*(.rodata)
_erodata = .;
}
. = ALIGN(32768);
.data.init_task : { *(.data.init_task) }
.bss :
{
_bss = .;
*(.bss)
_ebss = .;
}
_end = .;
}
- OUTPUT_FORMAT(DEFAULT,BIG,LITTLE):为链接过程提供三种输出文件格式,若链接命令使用-EB选项,那么程序将链接成BIG指代的文件格式;如果链接命令中有-EL选项,那么程序将链接成LITTLE指代的文件格式。否则程序将链接成默认文件格式,即DEFAULT指代的文件格式
- DEFAULT(默认)
- BIG(大端)
- LITTLE(小端)
- OUTPUT_ARCH(BFDARCH):指定输出文件的处理器体系结构
- ENTRY(SYMBOL):将标识符SYMBOL设置为程序的入口地址,即执行程序的第一条指令所在地址
- SECTIONS:SECTIONS关键字负责向链接器描述如何将输入文件中的各程序段(数据段、代码段、堆、栈、BSS)部署到输出文件中,同时还将规划各程序段在内存中的布局
- 符号.是一个定位器或位置指针,它用于定位程序的地址或调整程序的布局位置
- 链接脚本中的正则表达式*(.text) ,说明了输出文件的.text程序段保存着所有输入文件的.text程序段。而且,.text程序段还使用了_text和_etext标识符来标示.text程序段的起始线性地址和结尾线性地址,这两个标识符可在程序中通过代码extern _text和extern _etext进行引用(将它们看作全局变量)
- ALIGN(NUM):将地址向后按NUM字节对齐
学习处理器的运行模式前,让我们先来了解一些有关地址空间的概念
地址空间在一般情况下主要分为两大类:虚拟地址空间和物理地址空间。而虚拟地址空间又可分为:逻辑地址、有效地址、线性地址等
3.5.1、虚拟地址- 逻辑地址(Logical Address):在操作系统的研发过程中,逻辑地址经常会使用到,它的书写格式为Segment:Offset
- 线性地址(Linear Address)。线性地址是通过逻辑地址中的段基地址与段内偏移地址组合而成,这使得程序无法直接访问线性地址,平坦地址(Flat Address)作为一种特殊的线性地址,将段基地址和段长度覆盖了整个线性地址空间,而非线性地址空间中的某一部分区域
对于繁琐复杂的段管理机制而言,采用平坦地址可将段管理机制的地址转换过程透明化,即当段基地址为0时,段内偏移地址(有效地址)与线性地址在数值上相等
3.5.2、物理地址物理地址(Physical Address)是真实存在于硬件设备上的,它通过处理器的引脚直接或间接地与外部设备、RAM、ROM相连接。因此,物理地址空间中不仅包含物理内存(RAM、ROM)还有硬件设备。在处理器开启分页机制的情况下,线性地址需要经过页表映射才能转换成物理地址;否则线性地址将直接映射为物理地址
3.6、IA-32e模式IA-32e模式在原有32位保护模式的基础上进行了诸多升级、改造与整合,可近似视为一种全新的64位处理器体系结构
3.6.1、IA-32e模式概述虽然IA-32e模式的线性地址位宽64位,但其线性寻址能力只有48位,其低48位用于线性地址寻址,高16位将作为符号扩展(将第47位数值扩展至第63位,即全为0或全为1),此种格式的地址被称为Canonical地址。IA-32e模式下,只有Canonical地址空间是可用地址空间,而Non-Canonical空间则属于无效地址空间
此时你应该明白为什么内核层的起始线性地址为0xffff800000000000
当64位模式采用Canonical型的64位线性地址后,页管理机制也改成4级,但只有线性地址的低48位参与页表空间检索,高16位(符号扩展位)依然不参与页表空间检索。而且,页管理机制在支持4 KB物理页的基础上,还支持2 MB和1 GB的物理页
3.6.2、IA-32e模式的段管理机制代码段描述符
| 位图范围 | IA-32e模式位功能 | 数值(HEX) | 数值解释 |
|---|---|---|---|
| 0~15 | 忽略 | 0000 | 忽略 |
| 16~31 | 忽略 | 0000 | 忽略 |
| 32~39 | 忽略 | 00 | 忽略 |
| 40~43 | Type区域 | 8 | 非一致性、不可读、未访问 |
| 44 | S | 1 | 代码段 |
| 45~46 | DPL | 00 | 0特权级 |
| 47 | P | 1 | 已在内存中 |
| 48~51 | 忽略 | 0 | 忽略 |
| 52 | AVL | 0 | 忽略 |
| 53 | L | 1 | 64位工作模式 |
| 54 | D/B | 0 | |
| 55 | G | 0 | 忽略 |
| 56~63 | 忽略 | 00 | 忽略 |
数据段描述符
| 位图范围 | IA-32e模式位功能 | 数值(HEX) | 数值解释 |
|---|---|---|---|
| 0~15 | 忽略 | 0000 | 忽略 |
| 16~31 | 忽略 | 0000 | 忽略 |
| 32~39 | 忽略 | 00 | 忽略 |
| 40~43 | Type区域 | 2 | 向上扩展、可读写、未访问 |
| 44 | S | 1 | 数据段 |
| 45~46 | DPL | 00 | 0特权级 |
| 47 | P | 1 | 已在内存中 |
| 48~51 | 忽略 | 0 | 忽略 |
| 52 | AVL | 0 | 忽略 |
| 53 | L | 0 | 忽略 |
| 54 | D/B | 0 | 忽略 |
| 55 | G | 0 | 忽略 |
| 56~63 | 忽略 | 00 | 忽略 |
系统段描述符(Type区域)
| Type区域 | 功能 | |||
|---|---|---|---|---|
| 43 | 42 | 41 | 40 | |
| 0 | 0 | 0 | 0 | 16 B描述符的高8 B |
| 0 | 0 | 0 | 1 | 保留 |
| 0 | 0 | 1 | 0 | LDT段描述符 |
| 0 | 0 | 1 | 1 | 保留 |
| 0 | 1 | 0 | 0 | 保留 |
| 0 | 1 | 0 | 1 | 保留 |
| 0 | 1 | 1 | 0 | 保留 |
| 0 | 1 | 1 | 1 | 保留 |
| 1 | 0 | 0 | 0 | 保留 |
| 1 | 0 | 0 | 1 | 64位TSS段描述符(有效的) |
| 1 | 0 | 1 | 0 | 保留 |
| 1 | 0 | 1 | 1 | 64位TSS段描述符(使用中) |
| 1 | 1 | 0 | 0 | 64位调用门描述符 |
| 1 | 1 | 0 | 1 | 保留 |
| 1 | 1 | 1 | 0 | 64位中断门描述符 |
| 1 | 1 | 1 | 1 | 64位陷阱门描述符 |
IA-32e模式的LDT段描述符共占用16 B的内存空间,其与TSS描述符的位功能完全相同
3.7.2、TSS段描述符 3.7.3、调用门描述符调用门描述符同样从原有的8 B扩展至16 B,其低8 B的Param Count位区域已被忽略,而高8 B则保存着程序入口地址的第31~63位
由于IA-32e模式的系统段描述符已不再支持任务门描述符,那么IDT仅剩下陷阱门描述符和中断门描述符可以使用
这两个门描述符均从8 B扩展至16 B,它的高8 B保存着Offset区域的第32~63位,不仅如此,其低8 B的第32~34位将用于IST功能
IST只在IA-32e模式下有效,它为了给不同的中断提供一个理想的栈环境,而对原有栈切换机制进行了改良。程序通过IST功能可使处理器无条件进行栈切换。在IDT的任意一个门描述符都可以使用IST机制或原有栈切换机制,当IST=0时,使用原有栈切换机制,否则使用IST机制
IA-32e模式的TSS已为IST机制提供了7个栈指针,供IDT的门描述符使用。图6-35中的IST位区域(共3位)就用于为中断/异常处理程序提供IST栈表索引,当确定目标IST后处理器会强制将SS段寄存器赋值为NULL段选择子,并将中断栈地址加载到RSP寄存器中。最后,将原SS、RSP、RFLAGS、CS和RIP寄存器值压入新栈中
3.9、IA-32e模式页管理机制开启IA-32e模式必须伴随着页管理机制的开启(置位CR0.PG、CR4.PAE以及IA32_EFER.LME标志位)
IA-32e模式的页管理机制可将Canonical型的线性地址映射到52位物理地址空间(由处理器最高物理可寻址位宽值MAXPHYADDR决定)中,使得IA-32e模式可寻址4 PB(252 B)的物理地址空间,可寻址256 TB(248 B)的线性地址空间。处理器通过CR3控制寄存器保存的物理地址,可将线性地址转换成一个多层级页表结构,IA-32e模式的页管理机制共支持4 KB、2 MB和1 GB
248=221*29*29*2^9
=2MB*512*1024*256
接下来,正式开启"hello,kernel!"之旅



