从零开始的ARMv8操作系统内核实践 2 hello the wold (其一)

有关这个hello,world的代码,我已经上传至代码库的helloworld分支
代码库地址:
iversJin/ToyOS: A simple SMP OS on ARMv8a (github.com)

这一部分,我想分为3个部分来说,可能有点啰嗦,嘿嘿您可以跳着看,我这人比较话多 😛

  1. 树莓派3B+(即bcm2837)的启动流程
  2. 操纵树莓派外围设备的方法
  3. 构建一个裸机软件的方法

树莓派启动流程

树莓派的启动相对于x86的启动有一些区别,最奇葩的一点是,这货并不是从CPU启动,而是GPU.有兴趣的同学可以去树莓派官方论坛找一找.
具体来说,流程是这样的:

  1. 板子上电,CPU此时关闭,而GPU核心启动,它加载内部的ROM中的bootloader,挂载SD卡上的FAT32启动分区,然后将bootcode.bin载入L2cache,这里面的程序就是下一步的bootloader(第二级bootloader).运行第二阶段bootloader
  2. 位于bootcode.bin中的第二级bootloader运行,打开SDRAM(即主内存).加载start.elf文件,并运行
    start.elf中第三阶段bootloader会读取config.txt中的启动参数,将内核镜像kernel8.img复制到内存中0x8_0000的位置,如果存在设备树文件,会将其放置在0x100的位置
  3. 启动CPU. 主核心会从0x8_0000的位置开始运行,其余核心休眠,等待通过自旋表的方式唤醒

在1至3步的时候,CPU都没有启动,是GPU在操办着这个启动流程.不过,如果我们使用qemu模拟运行的话,就没这么复杂了,qemu会将指定的内核镜像放在0x8_0000,然后让CPU主核心从0x8_0000开始执行.

另外,qemu的模拟与实际运行还有一点差别:
某些老版本的qemu模拟的cpu在进入0x8_0000时,位于EL3级别.而真机在进入0x8_0000时,处于EL2级别.
不过,这只是我在查现有树莓派启动代码时,一些资料上记录的.我比较懒,就不找老版本qemu复现这个东西了.至少在我们之前配置环境章节编译安装的qemu-6.0.1,是不会有这个差异的,进入0x8_0000时也是EL2.不过为了兼容性,我们还是在代码编写时考虑一下这个问题,判断一下在进入0x8_0000后,CPU的异常级别,如果是EL3,就降至EL2

操纵树莓派外围设备的方法

与x86架构不同,Armv8的总线设备地址与内存地址是统一编址的,也就是说,在x86上,给定一个地址,它可能是内存地址,也可能是总线地址,即使二者数字一样,但是在不同的指令上,这两个地址的语义也不一样.但是Armv8则不然,一部分地址指代内存,另一部分指代总线设备,I/O设备同样被放置在内存空间.CPU访问外设的方式与访问内存的方式是一致的,这也就是所谓的MMIO(memory mapping i/o).

所以,我们先来查查树莓派的Soc手册,来看看它的内存空间分布情况.

树莓派3B+的Soc是bcm2837,它的手册网上找不到,所以树莓派官网上给出了bcm2836的手册,这两者只是CPU核心不同,I/O是一致的.而bcm2836又是bcm2825的改进型号,而这个型号的手册网上能找到,那我们就来看这个芯片的手册.

这里是完整下载链接 https://datasheets.raspberrypi.com/bcm2835/bcm2835-peripherals.pdf
我们来看这个手册的第五页image

这个图上,我们要要关注的只有物理地址(Physical Address),因为还没开启MMU,没虚地址什么事.而总线地址,我们现在暂时还用不到,但是,我也要啰嗦两句.
总线地址是给有DMA能力的外部设备使用的.比如我希望某个设备自己将数据放在内存中的某个位置,在向DMA控制器描述这个地址时,用的就是总线地址,而不是CPU用的物理地址.
另外,你看到总线地址上的SDRAM和I/O设备地址各存在4个区域,这说明在这个Soc上存在4个主存和I/O设备吗?并不是,四个区域指的是同一个设备,但是向不同地址的读写会影响缓存的行为.图上也有写,对吧.

我们可以看到,物理地址上,主存的地址空间从0x0开始.那个可选的VC SDRAM指的是GPU用的显存,因为咱们暂时还用不到GPU,所以先不管它.
然后是IO地址,BCM2835的IO地址从0x2000_0000开始.而在树莓派3b+上,用的是BCM2837,是从0x3f00_0000开始的.
这也就意味着,为什么树莓派3b+不存在1GB以上内存的版本,设计不允许呀... 而使用BCM2835的树莓派Zero更惨,从0x2000_0000开始的IO地址意味着它最大内存不能超过512M
找到IO地址后,根据手册上第十页的内容,我们就可以配置并使用树莓派的串口啦,不过这个部分我就不详细展开了,反正没什么用,有兴趣可以参考手册和网上的以及我的代码看一看.

构建一个裸机软件的方法

首先,裸机软件(bare metal)与普通软件有一些区别,最重要一点就是,裸机软件不能使用系统调用,不能使用动态链接,这也就意味着,大部分的标准库,都不能使用.
比如malloc,free,fopen,fclose等等等等

这是因为,这些内存分配函数操作,文件读写操作也好,核心操作必须依赖操作系统内核实现.

比如malloc,free,除开它们存在的一些池化的内存行为,若想真正的拿到可用的内存,必须通过系统调用sbrk实现.

这里我画一个简略的流程图,有点丑,莫怪...
image

在我们编写裸机程序时,会用到系统调用的函数全都不能用,这也就意味着大部分的标准库都不能使用了.
在这里,我先给出项目的文件层次结构

├── LICENSE
├── Makefile
├── README.md
└── kernel
    ├── arch
    │   └── aarch64
    │       ├── arm.h
    │       ├── board
    │       │   └── raspi3
    │       │       ├── gpio.h
    │       │       ├── peripherals_base.h
    │       │       ├── uart.c
    │       │       └── uart.h
    │       └── entry.S
    ├── console.c
    ├── console.h
    ├── linker.ld
    └── main.c

如你所见,我们将与平台,Soc相关的代码放在一个单独文件夹中.

手动编译内核

在使用自动编译工具make前,我们先来手动编译试试
首先,我们先来手动编译一个裸机镜像出来,我们首先将新建一个build文件夹,将源文件与构建产物相分离,干净又卫生~

makedir build ; cd build

我们要编译的文件有

  • main.c 这个是内核C语言部分的入口
  • console.c 实现printf向终端输出,在这里的实现就是向串口输出
  • entry.S 整个内核的入口,我们会将这里面的汇编代码放在0x8_0000处,让CPU启动后先执行这里
  • uart.c 控制串口的行为,包括初始化串口,向串口发送字符,从串口接收字符等等
    因为是裸机软件,我们需要控制一些链接行为,所以我们先编译一个一个.c文件为obj文件,然后再手动将它们链接在一起. 同样是裸机行为,我们有很长一串编译选项要传给gcc,但是重复输入太麻烦了,我们先用一个shell变量存储编译选项:
flags='-Wall -g -O2 -fno-pie -fno-pic -fno-stack-protector -static -fno-builtin -nostdlib -ffreestanding -nostartfiles -mgeneral-regs-only -MMD -MP -Iinc'

看的出来,我们对编译器的要求还挺多的,附加调试信息啦,静态链接啦,不要用堆栈保护啦.感兴趣的同学可以找一下gcc的文档,这个如果全写出来就太长了,网址在这里
Option Summary (Using the GNU Compiler Collection (GCC))
然后gcc的交叉编译器也有点长: aarch64-linux-gnu-gcc 我们也给它存到一个shell变量里面

cc=aarch64-linux-gnu-gcc

然后 逐一编译:

$cc $flags -c ../kernel/console.c -o console.c.o
$cc $flags -c ../kernel/main.c -o main.c.o
$cc $flags -c ../kernel/arch/aarch64/entry.S -o entry.S.o
$cc $flags -c ../kernel/arch/aarch64/board/raspi3/uart.c -o uart.c.o

链接. 因为我们要控制链接的行为,所以要写一个linker script,至于为什么,下面会讲.

aarch64-linux-gnu-ld -T ../kernel/linker.ld console.c.o main.c.o entry.S.o uart.c.o -o kernerl8.elf

这样,一个裸机elf文件就做好了.我们注意到,如果直接执行这个文件,会提示"Illegal instruction"的错误.这是因为,我的电脑是x64架构的,并不支持aarch64的指令.那如果我们用qemu模拟用户态程序呢?

$ qemu-aarch64 ./kernel8.elf 
qemu: uncaught target signal 4 (Illegal instruction) - core dumped
Illegal instruction

同样是非法指令. 这是因为我们的这个内核程序会执行一些特权指令,而直接运行的时候是用户态,不允许使用这些指令. Linux: 干嘛,想篡权?去死!
所以,我们要用qemu-system,模拟整个系统,而不是用户态

$ qemu-system-aarch64 -M raspi3 -nographic -serial null -chardev stdio,id=uart1 -serial chardev:uart1 -monitor none -kernel build/kernel8.img

## 输出:

hello the world.kernel/console.c:104: kernel panic.

完成,撒花,成功输出hello the world ~
使用make自动编译
手动编译很麻烦,对吧,一会就不麻烦了,我们引入一个Makefile文件,让make自动执行编译命令. 至于Makefile文件怎么编写我就不展开了,有兴趣学习可以看一下这个博客,讲的很详细,我就是按照这个学习的. 不过如果你对这个不感兴趣也无妨,这个Makefile文件我们就写这个一次,以后直接用就好
makefile介绍 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)
让我们试一下make的威力,在这个项目的根文件夹运行下面的命令

$ make qemu
## 输出:
mkdir -p build/./kernel/arch/aarch64/board/raspi3/
aarch64-linux-gnu-gcc -Wall -g -O2 -fno-pie -fno-pic -fno-stack-protector -static -fno-builtin -nostdlib -ffreestanding -nostartfiles -mgeneral-regs-only -MMD -MP -Iinc -c -o build/./kernel/arch/aarch64/board/raspi3/uart.c.o kernel/arch/aarch64/board/raspi3/uart.c
mkdir -p build/./kernel/arch/aarch64/
aarch64-linux-gnu-gcc -Wall -g -O2 -fno-pie -fno-pic -fno-stack-protector -static -fno-builtin -nostdlib -ffreestanding -nostartfiles -mgeneral-regs-only -MMD -MP -Iinc -c -o build/./kernel/arch/aarch64/entry.S.o kernel/arch/aarch64/entry.S
mkdir -p build/./kernel/
aarch64-linux-gnu-gcc -Wall -g -O2 -fno-pie -fno-pic -fno-stack-protector -static -fno-builtin -nostdlib -ffreestanding -nostartfiles -mgeneral-regs-only -MMD -MP -Iinc -c -o build/./kernel/console.c.o kernel/console.c
mkdir -p build/./kernel/
aarch64-linux-gnu-gcc -Wall -g -O2 -fno-pie -fno-pic -fno-stack-protector -static -fno-builtin -nostdlib -ffreestanding -nostartfiles -mgeneral-regs-only -MMD -MP -Iinc -c -o build/./kernel/main.c.o kernel/main.c
aarch64-linux-gnu-ld -o build/kernel8.elf -T kernel/linker.ld build/./kernel/arch/aarch64/board/raspi3/uart.c.o build/./kernel/arch/aarch64/entry.S.o build/./kernel/console.c.o build/./kernel/main.c.o
aarch64-linux-gnu-objdump -S -D build/kernel8.elf > build/kernel8.asm
aarch64-linux-gnu-objdump -x build/kernel8.elf > build/kernel8.hdr
aarch64-linux-gnu-objcopy -O binary build/kernel8.elf build/kernel8.img
qemu-system-aarch64 -M raspi3 -nographic -serial null -chardev stdio,id=uart1 -serial chardev:uart1 -monitor none -kernel build/kernel8.img
hello the world.kernel/console.c:104: kernel panic.

你看,一条命令,从编译,到链接,再到qemu模拟运行,行云流水,舒服
至于代码说明, 我今天码字码不动了... 就将这个hello the world分成两个章节 请听下回分解~

posted @ 2024-01-28 23:59  RiversJin  阅读(145)  评论(0)    收藏  举报