从零开始编写操作系统笔记
Lessn01 0x01
Bootstraping
阅读本文需要一些基本汇编代码知识, 不过本文中尽量将汇编代指令做好注释:
代码托管:
码云: https://gitee.com/muzi_since/Luinx0.1
github: https://github.com/cythinamissTack/neu-os
BIOS (放在RAM 中)
- Memory mapping 内存映射
- Power on self Test 开机自检
- Find MBR Sector 查找启动介质(可以设置启动顺序)
CPU Register – Init state
MBR Sector – 0x07c0 (SEG)
- Load system into RAM
- Do Some initialization
- Jump to the system code
操作系统引导过程:
BIOS (Basic Input & Output system)是放在主板固定的RAM中, 开机上电的时刻, BIOS代码会自动映射到内存中 此时CPU的指令寄存器会指向BIOS的映射内存地址入口代码
执行BIOS代码 如开机自检等
查找启动介质 通过设置的查找顺序, 一个一个查找 (筛查的依据是是否已"55AA"结束;
一旦找到了启动引导介质, 就会自动将介质中的代码主引导记录加载内存地址的 0x7c00 处
至此, BIOS完成了装在引导扇区到内存的启动默认地址的工作
接下来就是我们编写的引导扇区工作啦
完成一些初始化工作
设置GDT , IDT
关中断等一系列必要的操作
以上的过程称为处理器的实模式运行
最后, CPU 执行指令寄存器,会跳转到操作系统入口代码处, 执行操作系统的控制过程
Bootstraping 实际上就是一个BootLoaders, 用来状态操作系统的小程序
Grub : 广为使用的引导程序,
-
multiboot spec , 只要满足multiboot标准既可以执行
https://www.gun.org/software/grub/
-
Linux Loader
-
Syslinux
linux0.1 是没有提供bootloader的
文件准备: # bootsect.s Makefile , gitignore , ld.script
如何编写一个bootsect
启动引导程序(汇编代码) : bootsect.s
cat > bootsect.s <<"EOF"
########################################
# 实验一: #
# 编写一个启动引导bootsect.s #
# #
########################################
.code16 # 编写的是16进制实模式的bootsect
.global _bootstart # 导出符合_bootstart 供连接器使用,
#只有在global声明导出的符号,才会在将其汇编成一个object的时候,可以被其他的文件引用, 否则其他的文件不可见
.equ BOOTSEG, 0x07c0 #启动扇区的段号是0x7c00, BIOS 会跳转到0x7c00的地址对bootsect代码进行执行
# BOIS 在读取bootect启动的引导的时候,是默认读取地址为0x7c00的地址处的代码 那么这里为什么写 0x07c0呢?
# 这是因为,8086的地址线的物理结构:20根, 也就是他可以访问的物理寻址范围为2^20 即1M空间;
# 由于8086/8088所使用的寄存器都是16位,能够表示的地址范围为0~64k, 这和1M 地址空间比较也太小了,
# 所以为了能在8086/8088下能够访问1M内存, Intel采取了分段模式: 对地址转换, 一个地址是由 16位段基地址 : 16位偏移构成的
# 例如: 段(seg)地址 0x07c0 偏移量(offset)为: 0x0000 即为 0x07c0:0x0000
# 通过地址转换, 他实际的物理地址是 将16位段基地址左移4位 + 16位偏移量 = 20位地址 == 0x7c00
#
# ljmp 有两个目的,
# 第一个目的: 更换掉cs:ip两个寄存器,将代码段寄存器指向0x07c0 这样一个地址,将指令指针指向代码的入口地址
# 第二个目的: 清理掉之前的缓存,和流水线缓存, 保证cpu执行完BIOS到我们的bootsect代码执行过程中,不会出现错误
ljmp $BOOTSEG, $_bootstart
# 定义一个死循环, 用于启动引导成功后, 暂停
_bootstart:
jmp _bootstart
# .=510 表示从以上生成的机器码结尾开始,以0补充到510个字节;
.= 510
# BIOS 如何确定某个扇区可以启动?
# 就是通过检查每一介质的第一扇区的最后两个字节是否为低字节55 高字节aa, 如果是表明是可以启动的扇区
# 一旦找到了这个扇区, 就会将这个扇区的指令装载到0x7c00处
signature:
.word 0xaa55
#bootsect 的签名signature
#.word 定义一个world 为 aa55 , 指明当前扇区为启动引导扇区
#为什么是aa55呢? 因为Intel 的架构是小端序, 前面的内容在地址的高位, 后面的内容在地址的低位存放
EOF
测试编译一下,检查编写是否有问题,如果目录下成功生成以个test.o, 表明没有问题
as --32 bootsect.s -o test.o
删除临时文件test.o
rm test.o
编写Makefile
对bootsect.s 文件进行构建
cat> Makefile<<"EOF"
############################################
# 说明: @号标识指令只执行,但指令本身不会输出到屏幕
############################################
.PHONY=clean run-qemu # 表示这是一个伪target
# 指定一个target: run-qemu 并且依赖于bootsect.o文件, target: 后面跟依赖文件列表(以空格分隔)
run-qemu: bootsect.o
@qemu-system-i386 --boot a -fda bootsect.o
# qemu-i386 指定qemu运行的架构启动,
# --boot a 指定从软盘a盘启动
# -fda bootsect 将 bootsect.o文件加载到软盘中
# 定义一个target为bootsect.o 所谓target 就相当于为该文件的执行后可添加的option选项或者方法名
bootsect.o:
@as --32 bootsect.s -o bootsect.o
# 编译指令
# --32 指定 以16位i8086平台进行编译为二进制
# -o 指定输出二进制文件的文件名
# 定义一个清理临时文件的 target
clean:
@rm -f *.o
# 清理文件的临时输出二进制文件 *.o 清理所有以.o结尾的文件
EOF
小贴士:
直接运行makw bootsect.o 报错如下:
Makefile: missing separator(did you mean TAB instead of 8 spaces?). Stop.
由于Makefile文件的指令入口是Tab, 而 上面创建的Makefile的target指令是以8个空格起始,
要修改Makefile 中的8个空格为Tab空格, 注意这里的8需要根据vim的tab空格设置而定
sed 替换方式:
sed -i "s/ /\t/g" Makefile
make测试:
make bootsect.o
当前路径下会生成一个bootsect.o的可执行文件
查看其反汇编代码:
objdump -S bootsect.o
查看他的汇编信息如下
发现和我们编写的汇编代码bootsect.s 不一致, 这是因为没有指定正确的架构,因为我们编译时 使用 --32 指明了他输出的是一个16位i8086平台的二进制文件
因此我们使用参数 -m i8086 进行反汇编代码查看如下
可以看到, 代码与我们你编写的一致了
再次测试
make bootsect.o
查兰当前目录下生成了bootsect.o文件
测试qemu 启动 ,注意 qemu 工具需要安装在linux 环境下, 且qemu需要在图形化界面中启动:
make run-qemu
该命令将bootsect.o加载到软盘a中,并使用qemu虚拟化工具从盘启动引导程序
出现如下错误:
提示找不到启动引导盘
为什么不能直接装载软盘a中bootsect.o文件, 我们命名将文件结尾附加了aa55的标识符?
我们查看bootsect.o的汇编:
objdump -S bootsect.o
可见, bootsect的汇编代码时从ea 00 00 开始
使用16进制方式查看bootsect.o文件;
hexdump -C bootsect.o
首先文件的大小已经超出我们需要的512字节的大小, 且bootsect.o的代码被嵌入在可执行文件的中间
实际上 ea 00 00 之前的部分是ELF文件的head部分, 系统根据ELF 的head 判断是可执行文件
但是我们的BIOS是运行在实模式下, 不需要ELF head部分的信息, 他是直接跳转到段地址0x0000起始地址直接加载bootsect编译的代码 到内存的0x7c000处, 执行代码
因此需要从bootsec.o文件中提取出有效的执行代码段
关于可执行文件的段信息:
查看段信息:
objdump -s bootsect.o
编译生成可执行文件bootsect.o 仅有一个段.text 信息
使用命令
objcopy -O binary -j .text bootsect.o
# objcopy 去掉 bootsect 文件的ELF的文件头信息
# -O binary 指定输出的格式为binary格式
# -j 指定提取bootsect二进制文件中的 .text段的信息 , 由于这里仅有一个.text. 此项可以省略
发现第一个字节是ea , 最后也是一个启动引导表示55aa
至此, 一个真正的实模式下,可以在BIOS中启动引导的可执行文件准备完成
使用 qemu 加载启动引导项运行
在Makefile 中 添加文件提取操作, 该文件中使用了自定义连接规则ld-bootsect.ld来过滤段地址提取仅需的 .text段信息, 效果和通过命令行方式objectcopy是一样的;但是这种方式体现了模块化优势,为后续的开发提供更好的扩展
编写连接规则文件如下:
cat > ld-bootsect.ld <<EOF
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
SECTIONS{
.text 0x0000 : {
*(.text)
/* 指定 提取text段 , 且指定段的起始地址为 0x0000 */
}
/DISCARD/ : {
/* 这是一个默认丢弃段的规则, 写在这里面的段都会被丢弃掉 */
}
}
EOF
sed -i "s/ /\t/g" Makefile
cat> Makefile<<"EOF"
############################################
# 说明: @号标识指令只执行,但指令本身不会输出到屏幕
############################################
.PHONY=clean run-qemu # 表示这是一个伪target
# 指定一个target: run-qemu 并且依赖于bootsect文件, target: 后面跟依赖文件列表(以空格分隔)
run-qemu: bootsect.o
@qemu-system-i386 --boot a -fda bootsect.o
# qemu-i386 指定qemu运行的架构启动,
# --boot a 指定从软盘a盘启动
# -fda bootsect 将 bootsect.o文件加载到软盘中
# 定义一个target为bootsect.o 所谓target 就相当于为该文件的执行添加的option选项或者方法名
bootsect.o:
@as --32 bootsect.s -o bootsect.o
# 编译指令
# --32 指定 以16位i8086平台进行编译为二进制
# -o 指定输出二进制文件的文件名
# 提取有效段执行代码 ,依赖文件 bootsect.o 和 ld-bootsect.ld 文件
bootsect: bootsect.o ld-bootsect.ld
#
# link-script 是将不同的object 文件以规定的规则组合在一起, 并对他们重新进行排列
# 每个二进制文件都是由不同的段组成的
#
# 可以使用 objdump -S 可执行二进制文件名, 就可以查看该二进制文件的段信息.
# 例如 .text 段, .data 段
# gcc 在编译连接时, 默认的连接中使用的规则我们是不能使用的
# 因为我们bootsect仅仅需要.txt段 并且该.text的段起始地址为0000
# 因此需要自定ln规则文件 ld-bootsect.ld
@- ld -T ld-bootsect.ld bootsect.o -o bootsect
# -T 指定我们自己的link-script 连接规则文件
# bootsect.o作为输入文件
# -o 输出文件为 bootsect
@- objcopy -j .text -O binary bootsect
# objcopy 用来指定规则 提取有效信息,去掉 bootsect 文件的ELF的文件头信息
# -O binary 指定输出的格式为binary格式
# -j 指定提取bootsect二进制文件中的 .text段的信息
# 定义一个清理临时文件的 target
clean:
@rm -f *.o
# 清理文件的临时输出二进制文件 *.o 清理所有以.o结尾的文件
EOF
sed -i "s/ /\t/g" Makefile
测试 :
make run-qemu