在我打算介绍具体的知识和实验经过之前,我选择先写下这些话,虽然有一部分是矫情,但是更多的是,我意识到,最难解决的 bug 不在电脑上,而是在心态上。有的时候再坚持一下,再多做一个测试,再多想一种可能,再多问一个人,或许就 de 出来了。但是真的完全就不想再动了,因为自己内心知道自己就算做出来了,也是得不偿失的,就很难下笔了。
这个必须要强调,真的是重中之重,因为没有有一个 Linux 的开发环境,就意味着你要使用虚拟机完成挑战性任务。咱不说这个意味着一般情况只能使用命令行,咱就说开发的时候能忍,但是读代码的时候呢?移植操作系统的很强调对于 MOS 源码的理解,我觉得只有像 vscode 这种工具才可以方便代码的阅读,vim 的局限性还是太大了一些(不排除大佬)。
虽然指导书说可以使用 WSL 或者其他的命令行界面进行项目开发,但是从我这个菜鸡的角度来看,如果没有 vscode ,我是不可能完成整个的移植操作系统开发的。而且为了装这个 manjaro,花费的时间要远远比装交叉编译器之类的东西要花的时间久(要是没有叶哥哥和哲哥的帮助,这步就卡死了)。不过自从装上之后,开发真的势如破竹。而且 OS 实验也可以本地开发了,堪称我这个学期做过作为明智的决定。
实验环境
条目
环境
备注
本机操作环境
manjaro
交叉编译器
aarch64-none-elf-gcc
在应用市场下载安装
硬件模拟器
QEMU emulator version 5.0.0
命令行安装
编辑器
vscode
思考题
Linux Targeted 和 Bare-Metal 分别有怎样的含义呢?
这说的是交叉编译器的两种模式,Bare-Metal 是裸机模式(这里不会)
大小端的差异是由什么决定的?
大小端是 CPU 实现的访存接口限制,同时需要,编译器将高级的 C 语言编译成机器语言,在这过程中调整了大小端。
Lab 1
内核编译运行
内核的编译运行需要采用新的交叉编译器,可以采用 MOS 提供的方法,写一个 include 文件优化代码接口
// include.mk CROSS_COMPILE := aarch64-none-elf- CC := $(CROSS_COMPILE)gcc CFLAGS := -Wall -ffreestanding -g LD := $(CROSS_COMPILE)ld # OC is used to transfer the file from one format to another format # We use it to transfer the kernel from the elf to img OC := $(CROSS_COMPILE)objcopy
// old #define MMIO_BASE (0x3F000000) // new #define MMIO_BASE (0x3F000000 + 0xffffff8000000000)
内核启动
因为处理器有四个核,我们只需要一个核,所以首先先选出一个核进行运行(0 号核)
.section ".text.boot" _start: // X1 will store the ID of processor, different processor has different ID mrs X1, MPIDR_EL1 and X1, X1, #3 // Only the processor who has the ID equals 0 can jump to the _start_master, others will wait cbz X1, _start_master
在 EL1(EL0)状态的时候访问 physical counter 和 timer 有两种配置,一种是允许其访问,另外一种就是 trap to EL2。
Lab 2
内存地址布局
内核的地址布局:
用户进程的地址布局
Arm 的地址映射与重定向
与 MOS 在高地址区提供直接映射不同,Arm 并不提供直接的虚拟地址到物理地址的映射,所有的映射都需要依靠页表进行,同时地址映射是一个硬件过程,相比于 MOS 在 TLB 中找不到就触发异常,然后软件查表,Arm 只需要页目录的物理地址即可自动完成查找,报异常的原因是页表项权限位错误,异常处理只需要修改页表项即可。
.globl tlb_invalidate tlb_invalidate: dsb ishst // ensure write has completed // tlbi vmalls12e1is // invalidate tlb, all asid, el1. tlbi vmalle1is dsb ish // ensure completion of tlb invalidation isb // synchronize context and ensure that no instructions // are fetched using the old translation ret
其中 dsb 是数据同步屏障,isb 是指令同步屏障,tlbi 用于使指令失效。
有趣的是,不知道上面注释掉的这个为什么不可以
// tlbi vmalls12e1is // invalidate tlb, all asid, el1.
内存测试测试
测试函数如下
voiddebug_print_pgdir(uint_64 *pg_root) { debug("start to retrieval address: %lx\n", pg_root); // We only print the first 16 casting int limit = 2048; for (uint_64 i = 0; i < 512; i++) { // First Level uint_64 pg_info = pg_root[i]; if (pg_info & PTE_VALID) { debug("|-Level1 OK %lx|\n", pg_info); // So we should go to level 2 uint_64 *level2_root = (uint_64 *)KADDR(PTE_ADDR(pg_info)); for (uint_64 j = 0; j < 512; j++) { uint_64 level2_info = level2_root[j]; if (level2_info & PTE_VALID) { debug("|-|-Level2 OK|\n"); // So we should go to level 3 uint_64 *level3_root = (uint_64 *)KADDR(PTE_ADDR(level2_info)); for (uint_64 k = 0; k < 512; k++) { uint_64 level3_info = level3_root[k]; if (level3_info & PTE_VALID) { debug("|-|-|-Level3 OK|\n"); // We should print our info here. uint_64 va = ((uint_64)i << PUD_SHIFT) | ((uint_64)j << PMD_SHIFT) | ((uint_64)k << PTE_SHIFT); uint_64 pa = level3_info; if (limit--) debug("cast from ...0x%016lx to 0x%016lx... %d %d %d\n", va, pa, i, j, k); else return; } } } } } } }
voidtest_pgdir() { printf("\n---test pgdir---\n"); printf("Stage 1 - build up a page table"); uint_64 *pgdir; uint_32 *data; structPage *lut_page; page_alloc(&lut_page); pgdir = (uint_64 *)page2kva(lut_page); structPage *data_page; page_alloc(&data_page); data = (uint_32 *)page2kva(data_page);
for (int i = 0; i < 1024; i++) { data[i] = i; // Fill data into the data page }
// Insert data_page into lut_page extern uint_64 *kernel_pud; extern uint_64 *user_pud; uint_64 *kernel = (uint_64 *)KADDR((uint_64)kernel_pud); uint_64 *user = (uint_64 *)KADDR((uint_64)user_pud); debug("Kernel pud is %lx , User pud is %lx\n", kernel, user); debug("kernel pud value is %lx\n", kernel[0]); page_insert(pgdir, data_page, 0x400000, 0); // page_insert(pgdir,lut_page,PADDR(pgdir),PTE_ISH | PTE_RO | PTE_AF | PTE_NON_CACHE); // page_insert(user,data_page,0x80000,PTE_ISH | PTE_RO | PTE_AF | PTE_NON_CACHE); debug_print_pgdir(pgdir); set_ttbr0(page2pa(lut_page)); // set_ttbr0(PADDR(user)); tlb_invalidate(); data = (uint_32 *)0x400000; debug("data is %d:%d @0x%lx,0x%lx\n", 800, data[800], data, page2pa(data_page)); }
.globl irq_vector_init irq_vector_init: ldr x0, =vectors // load VBAR_EL1 with virtual msr vbar_el1, x0 // vector table address ret
除此之外,还需要利用 MMIO 打开平台的中断控制器,代码如下
voidenable_interrupt_controller() { // enable all kind of exception! // as for the register mapping,see guidebook. put32((0xffffff8040000040), (0xf)); put32((0xffffff8040000044), (0xf)); put32((0xffffff8040000048), (0xf)); put32((0xffffff804000004c), (0xf)); }
时钟中断
时钟中断为 IRQ ,分发后即可处理,对接 env_sched 即可。对于设置时钟,如下所示:
.global reset_timer reset_timer: mov x0, #0x3 msr cntkctl_el1, x0 ldr x0, =(0x3b9aca0 >> 6) //when the qemu start,the frequency is 0x3b9aca0 HZ //i don't know why i cannot change the frquency of it //doing so to so set 1s per exception msr cntp_tval_el0, x0 mov x0, #0x1 msr cntp_ctl_el0, x0 ret
同步异常
同步异常一共有三种,分别是系统调用,缺页异常,写时复制。三者属于同一种异常,所以需要进一步的分发,进一步的分发需要依靠 ESR,他类似与 Cause 寄存器,可以根据他来分析原因,具体方法如下
typedefstruct { uint_8 e_ident[EI_NIDENT]; /* Magic number and other info */ uint_16 e_type; /* Object file type */ uint_16 e_machine; /* Architecture */ uint_32 e_version; /* Object file version */ Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ uint_32 e_flags; /* Processor-specific flags */ uint_16 e_ehsize; /* ELF header size in bytes */ uint_16 e_phentsize; /* Program header table entry size */ uint_16 e_phnum; /* Program header table entry count */ uint_16 e_shentsize; /* Section header table entry size */ uint_16 e_shnum; /* Section header table entry count */ uint_16 e_shstrndx; /* Section header string table index */ } Elf64_Ehdr;
思考题
什么时候会发生 EL1 异常并陷入 EL1 来处理 EL1 的异常的情况?
其实原有 MOS 中不会发生这种情况,因为在所有的中断和异常操作都是原子的,有 CLI 和 STI 保证,在 arm 中,考虑会发生内核栈过大导致的缺页异常。
Lab 4
系统调用
在 MOS 中系统调用需要通过系统调用表分发,在 arm 中,可以直接用 C 语言分发
switch (syscall_id) { case SYS_putchar: debug("syscall is sys_putchar\n"); sys_putchar(tf->x[1]); break; case SYS_getenvid: debug("syscall is sys_get_envid\n"); r = sys_getenvid(); break; case SYS_yield: debug("syscall is sys_yield\n"); sys_yield(); break; case SYS_env_destroy: debug("syscall is sys_destroy\n"); r = sys_env_destroy((u_int)tf->x[1]); break; case SYS_set_pgfault_handler: debug("syscall is sys_set_pgfault_handler\n"); r = sys_set_pgfault_handler((u_int)tf->x[1], tf->x[2], tf->x[3]); break; case SYS_mem_alloc: debug("syscall is sys_mem_alloc\n"); r = sys_mem_alloc((u_int)tf->x[1], tf->x[2], tf->x[3]); break; case SYS_mem_map: debug("syscall is sys_mem_map\n"); r = sys_mem_map((u_int)tf->x[1], tf->x[2], (u_int)tf->x[3], tf->x[4], tf->x[5]); break; case SYS_mem_unmap: debug("syscall is sys_mem_umap\n"); r = sys_mem_unmap((u_int)tf->x[1], tf->x[2]); break; case SYS_env_alloc: debug("syscall is sys_env_alloc\n"); r = sys_env_alloc(); break; case SYS_set_env_status: debug("syscall is sys_set_env_status\n"); r = sys_set_env_status((u_int)tf->x[1], (u_int)tf->x[2]); break; case SYS_set_trapframe: debug("syscall is sys_set_trapframe\n"); r = sys_set_trapframe((u_int)tf->x[1], (struct Trapframe *)tf->x[2]); break; case SYS_panic: debug("syscall is sys_panic\n"); sys_panic((char *)tf->x[1]); break; case SYS_ipc_recv: debug("syscall is sys_ipc_recv\n"); sys_ipc_recv(tf->x[1]); break; case SYS_ipc_can_send: debug("syscall is sys_ipc_can_send\n"); r = sys_ipc_can_send(tf->x[1], (u_int)tf->x[2], tf->x[3], tf->x[4]); break; case SYS_read_sd: debug("syscall is sys_read_dev\n"); r = sys_read_sd(tf->x[1], tf->x[2]); break; case SYS_write_sd: debug("syscall is sys_read_dev\n"); r = sys_write_sd(tf->x[1], tf->x[2]); break; case SYS_cgetc: default: printf("Unknown syscall id %d.\n", syscall_id); break; }