基于ARMv8架构的mini操作系统

基于ARMv8架构的mini操作系统

深度参考了xv6实现。(… 持续更新中)

首先是配置环境

处理器选型

由于这个项目的目的是将基于RISC-V的xv6迁移到arm架构上,因此,需要选择合理的处理器。 同时,处理器的开发手册也应该是易得的,于是我了解了市面上常见的几种嵌入式开发板,分别是smt32, im6ull, 和树莓派。发现只有树莓派能支持armv8架构,而其他的开发板的cpu都是基于armv7架构。而在之前学习6s081时,6s081的源码是基于64位系统实现的。而armv7只支持32位。所以,采用树莓派就成了唯一的选择。

环境配置

为了方便调试,我首先使用了qemu模拟器进行模拟,首先在qemu模拟器跑通后再尝试将os迁移到开发板上。

首先是配置交叉编译环境

sudo apt-get install arm-linux-gnueabi-gcc 

之后是下载qemu源码

$ wget https://download.qemu.org/qemu-6.1.0.tar.xz
$ $tar xvJf qemu-6.1.0.tar.xz
cd qemu-6.1.0

接下来是配置qemu的编译环境。 当然中间还有nijia的配置过程,这里就不在叙述。

$ sudo apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev python-pip python-capstone virtualenv
# 配置编译选项,要求支持aarch64以及riscv64架构,支持调试
../configure --target-list=aarch64-softmmu,riscv64-softmmu,aarch64-linux-user,riscv64-linux-user --enable-debug



商业转载请联系作者获得授权,非商业转载请注明出处。
$ make 
$ make install

这样qemu就安装好了。检测一下qemu是否安装完毕

$ qemu-img --version
qemu-img version 6.0.1
Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers

接下来我们需要安装gdb。

apt install python3-dev # 如果希望gdb有python3的支持(pwndbg需要),那么就需要这个包
wget https://ftp.gnu.org/gnu/gdb/gdb-10.2.tar.xz
tar xf gdb-10.2.tar.xz
cd gdb-10.2
mkdir build
cd build
../configure --enable-targets=all --with-python=/usr/bin/python3
make -j$(nproc)
make install
# 接下来安装pwndbg
cd ~
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
处理器的运行,bootloader

设置SVC模式,关看门狗,屏蔽中断,初始化SDRA, 设置栈,时钟,将代码拷贝到SDRAM中,清BSS段,调用C函数。

上述为uboot的第一阶段

第二阶段从start arm开始。

从Flash里面读出内核,启动。

包括存储器初始化,环境变量初始化。

在接下来的实验中,我们先编译一个uboot

#配置交叉编译工具链
sudo apt install gcc-aarch64-linux-gnu

# clone uboot代码
export CROSS_COMPILE=aarch64-linux-gnu-

由于qemu不支持rasipi的bootloader启动,因此,暂时不再使用bootloader。同时,开发板也更换为rasibbery4.

嵌入式Linux内核

由于使用的是树莓派4B。但是qemu不支持树莓派4B的使用,因此需要从网上拉一个树莓派的qemu补丁

简单操作系统的第一步

待补充

使用GIC中断

由于在树莓派4B中使用GIC中断控制器,我们在这个章节首先实现mini操作系统的中断处理。

Tips: ESR寄存器只处理同步中断和错误。

建立异常向量表

对于中断处理,首先是要建立一个中断向量表,同时将异常向量表的的地址存放进vbar寄存器中

对于异常向量表,arm数据手册中有如图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RugfhEU2-1655091316542)(C:\Users\49853\AppData\Roaming\Typora\typora-user-images\image-20220523164315004.png)]

每一个异常向量表表项的长度为128位。因此,在表项中可以写一些处理函数。 CurrentEl 代表在该异常等级出现的异常。

参考Linux5.0内核代码,异常向量表暂且表示如下

.align 11
.global vectors
vectors:
	/* Current EL with SP0
	   当前系统运行在EL1时使用EL0的栈指针SP
	   这是一种异常错误的类型
	 */
	vtentry el1_sync_invalid
	vtentry el1_irq_invalid
	vtentry el1_fiq_invalid
	vtentry el1_error_invalid

	/* Current EL with SPx
	   当前系统运行在EL1时使用EL1的栈指针SP
	   这说明系统在内核态发生了异常

	   Note: 我们暂时只实现IRQ中断
	 */
	vtentry el1_sync_invalid
	vtentry el1_irq
	vtentry el1_fiq_invalid
	vtentry el1_error_invalid

	/* Lower EL using AArch64
	   在用户态的aarch64的程序发生了异常
	 */
	vtentry el0_sync_invalid
	vtentry el0_irq_invalid
	vtentry el0_fiq_invalid
	vtentry el0_error_invalid

	/* Lower EL using AArch32
	   在用户态的aarch32的程序发生了异常
	 */
	vtentry el0_sync_invalid
	vtentry el0_irq_invalid
	vtentry el0_fiq_invalid
	vtentry el0_error_invalid

el1_sync_invalid:
	//inv_entry 1, BAD_SYNC
	bl kernel_entry
	mov x0, sp
	mov x1, 0
	mrs x2, esr_el1
	bl bad_mode
	bl kernel_exit

el1_irq_invalid:
	inv_entry 1, BAD_IRQ
el1_fiq_invalid:
	inv_entry 1, BAD_FIQ
el1_error_invalid:
	inv_entry 1, BAD_ERROR
el0_sync_invalid:
	inv_entry 0, BAD_SYNC
el0_irq_invalid:
	inv_entry 0, BAD_IRQ
el0_fiq_invalid:
	inv_entry 0, BAD_FIQ
el0_error_invalid:
	inv_entry 0, BAD_ERROR

之后在加入其它的异常处理函数

设置中断处理函数

在完成异常向量表之后需要设置中断处理函数,在中断处理函数的执行过程中,需要保护现场和恢复现场。需要保护x1到x30寄存器, sp, elr_ELx, spsr_elx这些寄存器。恢复现场则是需要将在栈中的这些寄存器重新恢复。

设置GIC中断控制器

为什么要使用GIC中断控制器? 首先树莓派4B提供的是GIC中断,其次,随着中断源的增多,为了统一的管理中断源所以需要使用GIC中断。

现在说一些设置GIC中断的步骤

具体步骤如下

  1. 设置distributor 和 CPU interface寄存器组的基地址

    // 由于在BCM2711手册中,树莓派处于Low Peripheral mode时 GIC控制器的地址为0xFF841000
    #define GIC_V2_DISTRIBUTOR_BASE     (ARM_LOCAL_BASE + 0x00041000)
    #define GIC_V2_CPU_INTERFACE_BASE   (ARM_LOCAL_BASE + 0x00042000)
    // cpu_base is GIC_V2_DISTRIBUTOR_BASE
    //dist_base is GIC_V2_DISTRIBUTOR_BASE
    gic->raw_cpu_base = cpu_base;
    gic->raw_dist_base = dist_base;
    

    BCM2711 有两种模式,一种Full34bit模式, 另一种是Low Peripheral模式。

  2. 读取GICD_TYPER寄存器,计算当前GIC最大支持多少个中断源。

    gic_irqs = readl(gic_dist_base(gic) + GIC_DIST_CTR) & 0x1f;
    
  3. 初始化distributor

    (1) Disable distributor

    (2) 设置SPI中断的路由

    (3) 设置SPI中断的触发类型

    (4) Disactive and Disable所有的中断源

    (5) Enable distributor

    /* 关闭中断*/
    	writel(GICD_DISABLE, base + GIC_DIST_CTRL);
    
    	/* 设置中断路由:GIC_DIST_TARGET
    	 *
    	 * 前32个中断怎么路由是GIC芯片固定的,因此先读GIC_DIST_TARGET前面的值
    	 * 然后全部填充到 SPI的中断号 */
    	cpumask = gic_get_cpumask(gic);
    	cpumask |= cpumask << 8;
    	cpumask |= cpumask << 16;
    
    	for (i = 32; i < gic_irqs; i += 4)
    		writel(cpumask, base + GIC_DIST_TARGET + i * 4 / 4);
    
    	/* Set all global interrupts to be level triggered, active low */
    	for (i = 32; i < gic_irqs; i += 16)
    		writel(GICD_INT_ACTLOW_LVLTRIG, base + GIC_DIST_CONFIG + i / 4);
    
    	/* Deactivate and disable all 中断(SGI, PPI, SPI).
    	 *
    	 * 当注册中断的时候才 enable某个一个SPI中断,例如调用gic_unmask_irq()*/
    	for (i = 0; i < gic_irqs; i += 32) {
    		writel(GICD_INT_EN_CLR_X32, base +
    				GIC_DIST_ACTIVE_CLEAR + i / 8);
    		writel(GICD_INT_EN_CLR_X32, base +
    				GIC_DIST_ENABLE_CLEAR + i / 8);
    	}
    
    	/*打开SGI中断(0~15),可能SMP会用到*/
    	writel(GICD_INT_EN_SET_SGI, base + GIC_DIST_ENABLE_SET);
    
    	/* 打开中断:Enable group0 and group1 interrupt forwarding.*/
    	writel(GICD_ENABLE, base + GIC_DIST_CTRL);
    
  4. 初始化cpu interface

(1)设置GIC_CPU_PRIMASK, 设置中断优先级mask level

(2) Enable CPU interface

for (i = 0; i < 32; i += 4)
		writel(0xa0a0a0a0,
			dist_base + GIC_DIST_PRI + i * 4 / 4);

	writel(GICC_INT_PRI_THRESHOLD, base + GIC_CPU_PRIMASK);

	writel(GICC_ENABLE, base + GIC_CPU_CTRL);
注册中断

在初始化GIC控制器后, 我们需要注册中断,使得当外设发出中断的时候,能够在GIC控制器中读取到相应的信息。

树莓派BCM2711的中断号分配如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZvyKEoo-1655091316543)(C:\Users\49853\AppData\Roaming\Typora\typora-user-images\image-20220531162836349.png)]

加入我们使用generic Timer, 那么我们首先需要初始化Generic Timer。然后在GIC中断中注册这一中断。

//设置cntp_ctl_el0寄存器
generic_timer_init(); 
// 设置cntp_tval_el0寄存器,这个寄存器主要是设置中断时间的
generic_timer_reset(val);
//在gic中注册中断
gicv2_unmask_irq(GENERIC_TIMER_IRQ);
enable_timer_interrupt();

之后,CPU捕获到中断,于是进入到中断处理的过程中,首先是保存现场,然后跳转到中断处理函数中进行处理

中断处理函数首先读取GIC_CPU_INTACK的值,然后与Mask相与得到irqnr。如果irq_nr等于 GENERIC_TIMER_IRQ, 则调用相关的函数进行处理。

struct gic_chip_data *gic = &gic_data[0];
	unsigned long base = gic_cpu_base(gic);
	unsigned int irqstat, irqnr;

	do {
		irqstat = readl(base + GIC_CPU_INTACK);
		irqnr = irqstat & GICC_IAR_INT_ID_MASK;
		if (irqnr == GENERIC_TIMER_IRQ)
			handle_timer_irq();
		else if (irqnr == SYSTEM_TIMER1_IRQ)
			handle_stimer_irq();

		gicv2_eoi_irq(irqnr);

	} while (0);
同步异常处理

这部分在做系统调用的时候再去做。

MMU

作为区别于RTOS的重要部分, MMU实现了物理地址到虚拟地址的转化。因此,在这一部分里面,我们首先介绍ARM中MMU的相关信息。

VMAS

实际上,对于armv8架构来说,MMU属于VMAS(virtrual Memory Address System) 的一部分,在VMAS中,对虚拟地址的长度限制为48位,当开启MMU后,PC, SP,LR, ELR使用的都是虚拟地址。在ARMv8-2 LVA中,使用的是52位的虚拟地址。

虚拟地址可以分为两个部分,其中一个部分是000_0000_0000_0000到0000_FFFF_FFFF_FFFF_FFFF, 另一个部分是FFFF_0000_0000_ 0000 到 FFFF_FFFF_FFFF_FFFF。

因为在后面我需要使用多进程操作系统,因此需要有内核的概念,内核的地址放在高地址, 用户态为低地址。因此我们需要同时使用两个部分的虚拟内存。而虚拟地址的55位决定使用高地址还是低地址。两个基址寄存器分别为TTBR0_ELx 和 TTBR1_ELx.

由于指令预取的缘故,建议采用恒等映射。

具体方案。

在boot.S中首先建立了2MB的恒等映射和内核空间映射。之后,就是配置相关的寄存器,打开MMU

由于配置页表的代码比较长,在这里就不加以描述。主要是分享一下打开MMU的代码

tlbi	vmalle1	// Invalidate local TLB
	dsb	nsh

	ldr	x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
		     MAIR(0x04, MT_DEVICE_nGnRE) | \
		     MAIR(0x0c, MT_DEVICE_GRE) | \
		     MAIR(0x44, MT_NORMAL_NC) | \
		     MAIR(0xff, MT_NORMAL) | \
		     MAIR(0xbb, MT_NORMAL_WT)
	msr	mair_el1, x5

    ldr	x10, =TCR_TxSZ(VA_BITS) | TCR_TG_FLAGS
	msr	tcr_el1, x10

	ldr x3, =SCTLR_ELx_M

	adrp	x0, idmap_pg_dir
	msr     ttbr0_el1, x0
	adrp    x1, init_pg_dir
	msr     ttbr1_el1, x1
	isb
	msr sctlr_el1, x3
	isb

	ic	iallu
	dsb	nsh
	isb
	ret

打开MMU主要是5个寄存器,MAIR_EL1, TCR_EL1, SCTLR_EL1 以及 基址寄存器ttbr0 和 ttbr1。之后就是对pc值进行修改,之后就是跳转到main函数里去执行了。需要注意的是TCR主要确定地址空间的大小以及页表的粒度,而SCTLR_EL1则是使得MMU开启。

跳转到main函数的汇编代码如下所示

/* set sp to top of kernel_sp*/
	ldr x2, =kernel_sp
	add  sp, x2, #4096

    ldr x3, =kernel_main
	br	x3

在这里需要同时设置内核栈地址的位置和main函数的入口地址。其中,栈指针的地址必须在之前已经被映射的地址空间区域,否则在运行的时候MMU会因为找不到地址而直接跑飞。其次不能直接跳转,因为bl只能跳转1MB范围,而物理地址和虚拟地址的差显然大于这个范围,因此直接跳转回跑飞。

之后就是建立从2mb到512MB的内核页表以及从FFE0000 到FFFFFFF的设备页表,需要注意的是由于qemu现有的树莓派4补丁不支持设备区域的虚拟地址转换,因此暂时使用树莓派3的qemu进行模拟。

ARM强烈推荐TCR的内存属性与页表项的内存属性一致。同时,在写页表项的时候推荐使用dsb指令。

一些辅助寄存器知识
ID_AA64MFR0_EL1

这个寄存器用来提供CPU支持什么样的内存结构。其中最后四位用来表示支持物理地址的范围。

TCR_EL1

控制虚拟地址的寄存器。

TBIDI 1 [52]控制高地址, TTBIDI 0[51] 控制低地址。

T0SZ用来指低地址空间大小, T1SZ用来指高地址空间大小。如果高于会用来触发translation falut。公式为2^(64 - TxSZ)

IPS字段确定地址的大小。

SH0, ORGEN0, IRGN0 预缓存相关。 TG0表示粒度, A1域表示不同的ASID。

SCTLR_ELx

用来控制mmu的开关。SCTLR_ELx.M 控制mmu的开关, SCTLR_ELx.EE控制大小端。

页表项

[54]XN 不能执行, UXN在用户不能执行。

[53] PXN 特权模式不能指向。

[52] TLB

[51]DBM 帐比特位

[11]nG, 用来TLB管理,通常分为全局和进程特有的,

[10] AF 访问比特位 第一次访问页面会设置

[9:8]AP 数据访问权限。AP[1]为1表示可以被用户权限和更高权限(EL1)访问,位0表示不能被用户访问 AP[2]表示只读还是可读可写权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxsQslux-1655091316544)(C:\Users\49853\AppData\Roaming\Typora\typora-user-images\image-20220601113117585.png)]

add-symbol-file my_os.elf 0x80000 -s .rodata 0x87000

add-symbol-file my_os.elf 0x80000 -s .rodata 0x80a00

从内核空间到用户空间

mmu的开关。SCTLR_ELx.M 控制mmu的开关, SCTLR_ELx.EE控制大小端。

页表项

[54]XN 不能执行, UXN在用户不能执行。

[53] PXN 特权模式不能指向。

[52] TLB

[51]DBM 帐比特位

[11]nG, 用来TLB管理,通常分为全局和进程特有的,

[10] AF 访问比特位 第一次访问页面会设置

[9:8]AP 数据访问权限。AP[1]为1表示可以被用户权限和更高权限(EL1)访问,位0表示不能被用户访问 AP[2]表示只读还是可读可写权限。

[外链图片转存中…(img-uxsQslux-1655091316544)]

add-symbol-file my_os.elf 0x80000 -s .rodata 0x87000

add-symbol-file my_os.elf 0x80000 -s .rodata 0x80a00

从内核空间到用户空间

由于本OS的最终目标是实现一个多进程的OS,因此,由内核空间到用户空间的转化是十分必要的。在xv6中的是先为每一个进程初始化一个栈。同时,为每个进程分配一个用户页表。问题在于,MMU如何读取用户页表。
raspberry-pi-os提供的解决方案如下,鉴于没有文件系统,进程直接被嵌入到Linker文件中与内核代码一起链接之后转移到用户态中。在start_kernel代码中先创建一个类似于initcode的进程,之后就是转入到用户态的过程,在转入到用户态前先需要建立用户页表,之后修改栈和pc的地址、

进程Schedule

作为一个多进程操作系统,需要使用schedule来切换进程,并同时保存进程的上下文。在本OS中,参考了xv6的实现。即通过一个cpu的数据结构来保存cpu的上下文并保存当前运行的进程,当内核完成一系列的初始化后,就进入schedule函数,在该函数中进行选择进程并切换上下文的操作。
核心代码如下

 for(;;){
     for(p = proc; p < &proc[NPROC]; p++){
      if(p->state == RUNNABLE){
         p->state = RUNNING;
         c->proc = p;
         cpu_switch_to(&c->context, &p->context);
         preempt_disable();
         c->proc = 0;
      }
     }

需要注意的是内核的上下文被保存在cpu的context中,当进程运行完后,调用yield函数,重新切换到schedule 函数中。进行新的一轮循环。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值