一.概述 实现 loader
实现了汇编准备和跳转到 C 程序 EL 等级切换,sp 栈指针,bss 段清理,异常向量和跳转到 C 程序
实现了串口显示
实现了屏幕显示
实现了 sd 读写
实现了 fat32 文件系统及其读取
实现了 mmu 初始化,建立了 2 份页表
加载了 kernel.img 镜像文件,并跳转到 kernel 执
二.具体实现 loader 汇编部分 树莓派 3 默认从 0x80000 处执行,loader 程序将被加载到内存的物理地址 0x80000 处
链接器脚本 link.ld 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 SECTIONS { .= 0x80000 ; .text : {KEEP(*(.text .boot)) * (.text .text .*.gnu.linkonce.t *)} .rodata : {*(.rodata.rodata.*.gnu.linkonce.r *)} .= ALIGN(4096 ); PROVIDE(_data =.); .data : {*(.data.data.*.gnu.linkonce.d *)} .bss(NOLOAD) : { .= ALIGN(16 ); __bss_start =.; *(.bss.bss.*) * (COMMON) __bss_end =.; } .= ALIGN(0x00001000 ); _op_buf =.; .data.opbuf: { .+= (1 << 12 ); } .= ALIGN(0x00001000 ); _fat_buf =.; .data.fatbuf: { .+= (4 * (1 << 12 )); } .= ALIGN(0x00001000 ); _end =.; / DISCARD /: { *(.comment) * (.gnu *)*(.note *)*(.eh_frame *) } } __bss_size = (__bss_end - __bss_start) >> 3 ; _kernel_addr = 0xFFFFFF8000200000 ;
链接器脚本中,首先指定了 elf 程序的起始虚拟地址为 0x80000,接下来是 text 代码段,再往后分别是 rodata,data,bss 段。 此外还设置了_op_buf 的 4K 空间用于存储格式化输出显示的 buffer,_fat_buf 的 16k 空间用于存储 fat32 文件系统的数据结构。 最 后 是 bss_size 指 明 bss 段 大 小 ,_kernel_addr表明内核加载地址为虚拟地址的0xFFFFFF8000200000。
Start.S 1 2 3 4 5 6 7 8 9 10 11 .section ".text.boot" .global _start _start: mrs x1, mpidr_el1 and x1, x1, #3 cbz x1, 2f 1 : wfeb 1b 2 :
_start 表示程序入口地址,section “.text.boot”与 link.ld 里的.text.boot 相对应,表示将.text.boot 段放在.text 下.
1 .text : { KEEP(*(.text .boot)) *(.text .text .* .gnu.linkonce.t*) }
程序读取了 cpu 的 id,因为一开始是多核在运行,需要关闭非 0 号核心,对不是 0 的核心跳转到 1 处,运行 wfe 暂停核心进入低功耗模式,并执行死循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 2 : ldr x1, =_start mrs x0, CurrentEL and x0, x0, #12 cmp x0, #12 bne 5f mov x2, #0x5b1 msr scr_el3, x2 mov x2, #0x3c9 msr spsr_el3, x2 adr x2, 5f msr elr_el3, x2 eret
将_start 的地址写到 x1 寄存器中,读取 el 等级,判断是否 el3,如果不是 el3 则向后跳转到5处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 5 : cmp x0, #4 beq 5f msr sp_el1, x1 mrs x0, cnthctl_el2 orr x0, x0, #3 msr cnthctl_el2, x0 msr cntvoff_el2, xzr mov x0, #0x33FF msr cptr_el2, x0 msr hstr_el2, xzr mov x0, #(3 << 20 ) msr cpacr_el1, x0 mov x0, #(1 << 31 ) orr x0, x0, #(1 << 1 ) msr hcr_el2, x0 mrs x0, hcr_el2 mov x2, #0x0800 movk x2, #0x30d0 , lsl #16 msr sctlr_el1, x2 ldr x2, =_vectors msr vbar_el1, x2 mov x2, #0x3c4 msr spsr_el2, x2 adr x2, 5f msr elr_el2, x2 eret
判断 el 等级是否 el1,如果是,则设置 sp_el1 寄存器,允许 el2 等级下对 el1 的配置寄存器进行控制。关闭核心陷入,打开 el1 的 64 位模式,设置 mmu 控制权限,设置异常向量寄存vbar_el1,设置 el 异常状态,设置 elr_el2 返回地址,表示 5f 的位置,执行 eret 后将进入el1 异常等级,并从5f 处开始执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 5 : mov sp, x1ldr x1, =__bss_start ldr w2, =__bss_size 3 : cbz w2, 4f str xzr, [x1], #8 sub w2, w2, #1 cbnz w2, 3b 4 : bl mainldr x2, =_kernel_addr br x2 b 1b
将代码起点_start 的地址设置为栈指针寄存器的值,将 bss 段内存初始化为 0,带返回地址跳转到 C 程序的 main 函数,跳转到_kernel_addr 位置处执行,最后一个死循环,用于执行异常返回到此后的指令安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 exc_handler: eret .align 11 _vectors: .align 7 mov x0, #0 mrs x1, esr_el1 mrs x2, elr_el1 mrs x3, spsr_el1 mrs x4, far_el1 b exc_handler
这里是一些简单的异常处理,设置了异常向量表,异常处理函数 exc_handler。
C 程序 gpio.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define MMIO_BASE 0x3F000000 #define GPFSEL0 ((volatile unsigned int*)(MMIO_BASE+0x00200000)) #define GPFSEL1 ((volatile unsigned int*)(MMIO_BASE+0x00200004)) #define GPFSEL2 ((volatile unsigned int*)(MMIO_BASE+0x00200008)) #define GPFSEL3 ((volatile unsigned int*)(MMIO_BASE+0x0020000C)) #define GPFSEL4 ((volatile unsigned int*)(MMIO_BASE+0x00200010)) #define GPFSEL5 ((volatile unsigned int*)(MMIO_BASE+0x00200014)) #define GPSET0 ((volatile unsigned int*)(MMIO_BASE+0x0020001C)) #define GPSET1 ((volatile unsigned int*)(MMIO_BASE+0x00200020)) #define GPCLR0 ((volatile unsigned int*)(MMIO_BASE+0x00200028)) #define GPLEV0 ((volatile unsigned int*)(MMIO_BASE+0x00200034)) #define GPLEV1 ((volatile unsigned int*)(MMIO_BASE+0x00200038)) #define GPEDS0 ((volatile unsigned int*)(MMIO_BASE+0x00200040)) #define GPEDS1 ((volatile unsigned int*)(MMIO_BASE+0x00200044)) #define GPHEN0 ((volatile unsigned int*)(MMIO_BASE+0x00200064)) #define GPHEN1 ((volatile unsigned int*)(MMIO_BASE+0x00200068)) #define GPPUD ((volatile unsigned int*)(MMIO_BASE+0x00200094)) #define GPPUDCLK0 ((volatile unsigned int*)(MMIO_BASE+0x00200098)) #define GPPUDCLK1 ((volatile unsigned int*)(MMIO_BASE+0x0020009C))
GPIO(General -purpose input/output):通用型输入输出。GPIO 可以作为一组的输入输出,通过拉高/拉低可以将每个引脚可以设置为不同的逻辑电平,可以用作模拟信号 IO、计数器/定时器、串口,用户可以通过 GPIO 口和硬件进行数据交互(如 UART)、控制硬件工作(如 LED、蜂鸣器等)、读取硬件的工作状态信号(如中断信号)等。此外,在一些IC(Integrated circuit )中,GPIO 可能是复用的,所以需要配置引脚的行为。 这里定义了每个 gpio 接口映射到内存的位置,MMIO_BASE 内存映射 IO 的起始地址是0x3F000000 .
main.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void main () { uart_init(); lfb_init(); if (sd_init() != SD_OK) { uart_puts("Error in sd_init\n" ); return -1 ; } unsigned int cluster; if (!fat_getpartition()) { uart_puts("FAT partition not found???\n" ); return -1 ; } cluster = fat_getcluster("KER ELF" ); if (cluster) { data = fat_readfile(cluster); } else { uart_puts("there is no File\n" ); } mmu_init(); kern_exec(data); return 0 ; }
main.c 阐述了 loader 程序主要的流程,uart 串口初始化,framebuffer 初始化也就是屏幕显示,sd 驱动初始化,获取 fat32 表信息,获取 kernel 文件的簇号,读取 kernel 文件到内存中data 起始地址处,初始化 MMU 建立页表,解析加载 elf 格式的 kernel 程序到_kernel_addr地址处,返回汇编 start.S 中继续执行跳转指令。
mailbox mbox.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 extern volatile unsigned int mbox[36 ];#define MBOX_REQUEST 0 #define MBOX_CH_POWER 0 #define MBOX_CH_FB 1 #define MBOX_CH_VUART 2 #define MBOX_CH_VCHIQ 3 #define MBOX_CH_LEDS 4 #define MBOX_CH_BTNS 5 #define MBOX_CH_TOUCH 6 #define MBOX_CH_COUNT 7 #define MBOX_CH_PROP 8 #define MBOX_TAG_SETPOWER 0x28001 #define MBOX_TAG_SETCLKRATE 0x38002 #define MBOX_TAG_LAST 0 int mbox_call (unsigned char ch) ;
声明了 mbox[36]数组,设置了 mailbox 相关的一宏定义——请求,channels 和 tags;声明了 mbox_call 函数。
mbox.c
1 2 3 4 5 6 7 8 9 10 11 12 volatile unsigned int __attribute__((aligned(16 ))) mbox[36 ];#define VIDEOCORE_MBOX (MMIO_BASE+0x0000B880) #define MBOX_READ ((volatile unsigned int*)(VIDEOCORE_MBOX+0x0)) #define MBOX_POLL ((volatile unsigned int*)(VIDEOCORE_MBOX+0x10)) #define MBOX_SENDER ((volatile unsigned int*)(VIDEOCORE_MBOX+0x14)) #define MBOX_STATUS ((volatile unsigned int*)(VIDEOCORE_MBOX+0x18)) #define MBOX_CONFIG ((volatile unsigned int*)(VIDEOCORE_MBOX+0x1C)) #define MBOX_WRITE ((volatile unsigned int*)(VIDEOCORE_MBOX+0x20)) #define MBOX_RESPONSE 0x80000000 #define MBOX_FULL 0x80000000 #define MBOX_EMPTY 0x40000000
定义了 mailbox 的 framebuffer channel 的相关宏的 mmap 到内存的地址; 定义了 mbox[36]数组,且数组按 16 字节对齐,因为 GPU 对消息地址对齐有这一要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int mbox_call (unsigned char ch) { unsigned int r = (((unsigned int )((unsigned long )&mbox) & ~0xF ) | (ch & 0xF )); do { asm volatile ("nop" ) ; } while (*MBOX_STATUS & MBOX_FULL); *MBOX_WRITE = r; while (1 ) { do { asm volatile ("nop" ) ; } while (*MBOX_STATUS & MBOX_EMPTY); if (r == *MBOX_READ) return mbox[1 ] == MBOX_RESPONSE; } return 0 ; }
该函数实现了通过 mbox 与 GPU 通信的功能; 首先在 r 中存储(mbox 的起始地址)&(channel)
树莓派 Mailbox 访问一般流程: 要从邮箱中读取: 1、读取状态寄存器,直到没有设置 empty 标志 2、从读寄存器读取数据 3、如果低 4 位与所需的通道编号不匹配,则从 1 开始重复 4、高 28 位是返回的数据 写入邮箱: 1、读取状态寄存器,直到未设置 full 标志 2、将数据(移入高 28 位)与通道(低 4 位)一起写入写寄存器 需要准备 28 bits 的 buffer address 也就是第一条指令处理 mbox 地址 4 bits 的 channel,也就是(ch&0xF),这里一般用的是 channel 8 也即是宏 MBOX_CH_PROP,其含义是 Request from ARM for response by VC 当MBOX_STATUS 的值不为 MBOX_FULL 也就是 0x80000000 时,表示 mailbox 已经准备好接收请求数据了; *MBOX_WRITE = r; 向 MBOX_WRITE 处写入准备的 32bits 变量 r 的值。 while 循环中读取 MBOX_STATUS 状态,直到不为 MBOX_EMPTY,说明非空就是有 GPU 返回的数据了,读取返回的 response,如果正确说明 mbox_call 执行成功,返回的数据在数组mbox 里,这里的*MBOX_READ 只是返回的状态。
Uart 串口 Uart.h
1 2 3 4 5 6 7 void uart_init () ;void uart_send (unsigned int c) ;char uart_getc () ;void uart_puts (char *s) ;void uart_hex (unsigned int d) ;void uart_dump (void *ptr) ;void uart_printf (char *fmt, ...) ;
定义了串口相关函数的声明,分别是串口初始化,串口发送,串口获取,串口字符串发送,串口 16 进制发送,串口解析二进制,串口格式化显示 printf。
Uart.c
1 2 3 4 5 6 7 8 9 10 11 #define UART0_DR ((volatile unsigned int*)(MMIO_BASE+0x00201000)) #define UART0_FR ((volatile unsigned int*)(MMIO_BASE+0x00201018)) #define UART0_IBRD ((volatile unsigned int*)(MMIO_BASE+0x00201024)) #define UART0_FBRD ((volatile unsigned int*)(MMIO_BASE+0x00201028)) #define UART0_LCRH ((volatile unsigned int*)(MMIO_BASE+0x0020102C)) #define UART0_CR ((volatile unsigned int*)(MMIO_BASE+0x00201030)) #define UART0_IMSC ((volatile unsigned int*)(MMIO_BASE+0x00201038)) #define UART0_ICR ((volatile unsigned int*)(MMIO_BASE+0x00201044)) extern volatile unsigned char _op_buf;
这里定义了 uart0 串口的地址,在 MMIO_BASE 基础上添加一个偏移量。 这些寄存器的具体含义见 BCM2837-ARM-Peripherals 的第 177 页,寄存器地址表和简介如下图所示
关于表中是 0x7E 为起始地址,但是代码中用 0x3F 作为起点,原因如下 此处在总线地址 0x7Ennnnnn 公布的外设在物理地址 0x3Fnnnnnn 可用。
_op_buf 是从 link.ld 中获取的符号,其地址作为 printf 格式化输出用的 buffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void uart_init () { register unsigned int r; *UART0_CR = 0 ; mbox[0 ] = 9 * 4 ; mbox[1 ] = MBOX_REQUEST; mbox[2 ] = MBOX_TAG_SETCLKRATE; mbox[3 ] = 12 ; mbox[4 ] = 8 ; mbox[5 ] = 2 ; mbox[6 ] = 4000000 ; mbox[7 ] = 0 ; mbox[8 ] = MBOX_TAG_LAST; mbox_call(MBOX_CH_PROP); r = *GPFSEL1; r &= ~((7 << 12 ) | (7 << 15 )); r |= (4 << 12 ) | (4 << 15 ); *GPFSEL1 = r; *GPPUD = 0 ; wait_cycles(150 ); *GPPUDCLK0 = (1 << 14 ) | (1 << 15 ); wait_cycles(150 ); *GPPUDCLK0 = 0 ; *UART0_ICR = 0x7FF ; *UART0_IBRD = 2 ; *UART0_FBRD = 0xB ; *UART0_LCRH = 0b11 << 5 ; *UART0_CR = 0x301 ; }
该函数初始化了 uart0 串口,将 uart0 串口 map 到 gpio 引脚 14 和 15 才能实现输入输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void uart_send (unsigned int c) { do { asm volatile ("nop" ) ; } while (*UART0_FR & 0x20 ); *UART0_DR = c; } char uart_getc () { char r; do { asm volatile ("nop" ) ; } while (*UART0_FR & 0x10 ); r = (char )(*UART0_DR); return r == '\r' ? '\n' : r; }
UART0_DR 是数据寄存器,UART0_FR 是 flag 寄存器,DR 负责读写数据,FR 负责体现 uart状态。 写串口的实现就是先判断 flag 是否可用,如果可用则写 DR 位置。 读串口也一样,判断 flag 是否可用,可用则读 DR 位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void uart_puts (char *s) { while (*s) { if (*s == '\n' ) uart_send('\r' ); uart_send(*s++); } } void uart_hex (unsigned int d) { unsigned int n; int c; for (c = 28 ; c >= 0 ; c -= 4 ) { n = (d >> c) & 0xF ; n += n > 9 ? 0x37 : 0x30 ; uart_send(n); } }
Puts 和 hex 只是对源数据进行了简单加工,实现了字符串写入和人可读的 16 进制写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void uart_dump (void *ptr) { unsigned long a, b, d; unsigned char c; for (a = (unsigned long )ptr; a < (unsigned long )ptr + 0xece0 ; a += 16 ) { uart_hex(a); uart_puts(": " ); for (b = 0 ; b < 16 ; b++) { c = *((unsigned char *)(a + b)); d = (unsigned int )c; d >>= 4 ; d &= 0xF ; d += d > 9 ? 0x37 : 0x30 ; uart_send(d); d = (unsigned int )c; d &= 0xF ; d += d > 9 ? 0x37 : 0x30 ; uart_send(d); uart_send(' ' ); if (b % 4 == 3 ) uart_send(' ' ); } for (b = 0 ; b < 16 ; b++) { c = *((unsigned char *)(a + b)); lfb_printf("%c" , c < 32 || c >= 127 ? '.' : c); uart_send(c < 32 || c >= 127 ? '.' : c); } uart_send('\r' ); uart_send('\n' ); } }
Dump 用于格式化显示内存数据,类似于 xxd 命令功能,效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void uart_printf (char *fmt, ...) { __builtin_va_list args; __builtin_va_start(args, fmt); char *s = (char *)&_op_buf; vsprintf (s, fmt, args); while (*s) { if (*s == '\n' ) uart_send('\r' ); uart_send(*s++); } }
uart_printf 调用 vsprintf 将数据按照 format 格式进行解析重组为一个字符串,将字符串传送到 uart_puts 进行打印。
sprintf.c
vsprintf 的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 unsigned int vsprintf (char *dst, char *fmt, __builtin_va_list args) { long int arg; int len, sign, i; char *p, *orig = dst, tmpstr[19 ]; if (dst == (void *)0 || fmt == (void *)0 ) { return 0 ; } arg = 0 ; while (*fmt) { if (*fmt == '%' ) { fmt++; if (*fmt == '%' ) { goto put ; } len = 0 ; while (*fmt >= '0' && *fmt <= '9' ) { len *= 10 ; len += *fmt - '0' ; fmt++; } if (*fmt == 'l' ) { fmt++; } if (*fmt == 'c' ) { arg = __builtin_va_arg(args, int ); *dst++ = (char )arg; fmt++; continue ; } else if (*fmt == 'd' ) { arg = __builtin_va_arg(args, int ); sign = 0 ; if ((int )arg < 0 ) { arg *= -1 ; sign++; } if (arg > 99999999999999999L ) { arg = 99999999999999999L ; } i = 18 ; tmpstr[i] = 0 ; do { tmpstr[--i] = '0' + (arg % 10 ); arg /= 10 ; } while (arg != 0 && i > 0 ); if (sign) { tmpstr[--i] = '-' ; } if (len > 0 && len < 18 ) { while (i > 18 - len) { tmpstr[--i] = ' ' ; } } p = &tmpstr[i]; goto copystring; } else if (*fmt == 'x' ) { arg = __builtin_va_arg(args, long int ); i = 16 ; tmpstr[i] = 0 ; do { char n = arg & 0xf ; tmpstr[--i] = n + (n > 9 ? 0x37 : 0x30 ); arg >>= 4 ; } while (arg != 0 && i > 0 ); if (len > 0 && len <= 16 ) { while (i > 16 - len) { tmpstr[--i] = '0' ; } } p = &tmpstr[i]; goto copystring; } else if (*fmt == 's' ) { p = __builtin_va_arg(args, char *); copystring: if (p == (void *)0 ) { p = "(null)" ; } while (*p) { *dst++ = *p++; } } } else { put : *dst++ = *fmt; } fmt++; } *dst = 0 ; return dst - orig; }
其内容就是根据 fmt 字符串的格式信息对原始参数字符串进行匹配解析和重组。
framebuffer lbf.h
1 2 3 4 5 void lfb_init () ;void lfb_print (char *s) ;void lfb_proprint (int x, int y, char *s) ;void lfb_printf (char *fmt, ...) ;void lfb_clear () ;
定义了 framebuffer 初始化,printf 格式化打印,清屏幕,ASCII 编码字符打印(lfb_print),非ASCII 编码字符打印(lfb_proprint)
lfb.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 typedef struct { unsigned int magic; unsigned int version; unsigned int headersize; unsigned int flags; unsigned int numglyph; unsigned int bytesperglyph; unsigned int height ; unsigned int width ; unsigned char glyphs; } __attribute__((packed)) psf_t ; extern volatile unsigned char _binary_font_psf_start;typedef struct { unsigned char magic[4 ]; unsigned int size ; unsigned char type; unsigned char features; unsigned char width ; unsigned char height ; unsigned char baseline; unsigned char underline; unsigned short fragments_offs; unsigned int characters_offs; unsigned int ligature_offs; unsigned int kerning_offs; unsigned int cmap_offs; } __attribute__((packed)) sfn_t ; extern volatile unsigned char _binary_font_sfn_start;unsigned int width , height , pitch;unsigned char *lfb;
定义了 ASCII 码字符和非 ASCII 字符的数据结构,定义了字体的宽度,高度,pitch。lfb 指针将被指向 frame buffer point。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 void lfb_init () { mbox[0 ] = 35 * 4 ; mbox[1 ] = MBOX_REQUEST; mbox[2 ] = 0x48003 ; mbox[3 ] = 8 ; mbox[4 ] = 8 ; mbox[5 ] = 800 ; mbox[6 ] = 600 ; mbox[7 ] = 0x48004 ; mbox[8 ] = 8 ; mbox[9 ] = 8 ; mbox[10 ] = 800 ; mbox[11 ] = 600 ; mbox[12 ] = 0x48009 ; mbox[13 ] = 8 ; mbox[14 ] = 8 ; mbox[15 ] = 0 ; mbox[16 ] = 0 ; mbox[17 ] = 0x48005 ; mbox[18 ] = 4 ; mbox[19 ] = 4 ; mbox[20 ] = 32 ; mbox[21 ] = 0x48006 ; mbox[22 ] = 4 ; mbox[23 ] = 4 ; mbox[24 ] = 1 ; mbox[25 ] = 0x40001 ; mbox[26 ] = 8 ; mbox[27 ] = 8 ; mbox[28 ] = 4096 ; mbox[29 ] = 0 ; mbox[30 ] = 0x40008 ; mbox[31 ] = 4 ; mbox[32 ] = 4 ; mbox[33 ] = 0 ; mbox[34 ] = MBOX_TAG_LAST; if (mbox_call(MBOX_CH_PROP) && mbox[20 ] == 32 && mbox[28 ] != 0 ) { mbox[28 ] &= 0x3FFFFFFF ; width = mbox[5 ]; height = mbox[6 ]; pitch = mbox[33 ]; lfb = (void *)((unsigned long )mbox[28 ]); } else { uart_puts("Unable to set screen resolution to 1024x768x32\n" ); } }
该函数的功能是初始化 frame buffer,mbox[36]是 CPU 与 GPU 通信的消息传输媒介,设置对应的 mbox 元素数值,调用 mbox_call 发送到 GPU,GPU 返回结果也是在 mbox 数组的对应元素中,其中 mbox[5]表示实际屏幕宽度,mbox[6]表示实际屏幕高度,mbox[33]是FrameBufferInfo.pitch。 lfb 是返回的 framebuffer 的 point,也就是写数据的起始地址,可用简单理解为屏幕左上角 第一个像素点的对应的内存地址。 framebuffer 初始化的原理见网页 https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface#frame-buffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 void lfb_print (char *s) { static int x = 0 , y = 0 ; psf_t *font = (psf_t *)&_binary_font_psf_start; if (y > 589 ) { y = 0 ; } if (x > 789 ) { y += font->height ; x = 0 ; } while (*s) { unsigned char *glyph = (unsigned char *)&_binary_font_psf_start + font->headersize + (*((unsigned char *)s) < font->numglyph ? *s : 0 ) * font->bytesperglyph; int offs = (y * pitch) + (x * 4 ); int i, j, line , mask, bytesperline = (font->width + 7 ) / 8 ; if (*s == '\r' ) { x = 0 ; } else if (*s == '\n' ) { x = 0 ; y += font->height ; } else { for (j = 0 ; j < font->height ; j++) { line = offs; mask = 1 << (font->width - 1 ); for (i = 0 ; i < font->width ; i++) { *((unsigned int *)(lfb + line )) = ((int )*glyph) & mask ? 0xFFFFFF : 0 ; mask >>= 1 ; line += 4 ; } glyph += bytesperline; offs += pitch; } x += (font->width + 1 ); } s++; } }
打印 ASCII 编码的字符,屏幕打印的本质是根据字体信息点亮像素点,在字体结构体中根据像素点数据点亮像素点,自动换行和翻页。
1 2 3 4 5 6 7 8 9 10 11 12 13 void lfb_clear () { for (int i = 0 ; i < 600 * 8 ; i++) { for (int j = 0 ; j < 800 ; j++) { *((unsigned int *)(lfb + i * 768 + j)) = 0 ; } } }
清屏函数,遍历 600*800 的屏幕像素点,设置值为 0 即可,背景黑色的情况下。RGB32 图像每个像素用 32 比特位表示,占 4 个字节,R,G,B 分量分别用 8 个 bit 表示,存储顺序为 B,G,R,最后 8 个字节保留。 R = color & 0x0000FF00; G = color & 0x00FF0000; B = color & 0xFF000000; A = color & 0x000000FF;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void lfb_printf (char *fmt, ...) { __builtin_va_list args; __builtin_va_start(args, fmt); char *s = (char *)&_op_buf; vsprintf (s, fmt, args); lfb_print(s); }
printf 格式化输出,首先根据 fmt 的格式解析重组参数,将重组的字符串传入 lfb_print.
SD 卡驱动 sd.h
1 2 3 4 5 6 #define SD_OK 0 #define SD_TIMEOUT -1 #define SD_ERROR -2 int sd_init () ;int sd_readblock (unsigned int lba, unsigned char *buffer , unsigned int num) ;int sd_writeblock (unsigned char *buffer , unsigned int lba, unsigned int num) ;
声明了三个 sd 接口函数,分别是初始化,从 lba 位置处读取 num 个块数据到 buffer 缓冲 区,在 lba 处写入 buffer 里 num 个块的数据。
sd.c 内容较复杂,单独作为文章。见另一篇 关于emmc访问sd卡的实现与分析
F32 文件系统 为了减小bootloader的体积,bootloader的文件系统部分不需要太复杂,只需能够从sd卡的第一个分区(fat32文件系统)读取指定的elf文件即可,所以没有多级目录等功能。
fat.h
1 2 3 4 int fat_getpartition (void ) ;unsigned int fat_getcluster (char *fn) ;char *fat_readfile (unsigned int cluster) ;void fat_listdirectory (void ) ;
定义了四个函数,fat_getpartition 获取 FAT 表;fat_getcluster 获取文件的第一个簇号;fat_readfile 将文件数据读取到内存中,返回首地址;fat_listdirectory 列出根目录下的内容。
fat.c
1 2 3 4 5 6 int memcmp (void *s1, void *s2, int n) {unsigned char *a=s1,*b=s2;while (n-->0 ){ if (*a!=*b) { return *a-*b; } a++; b++; }return 0 ;}
定义了一个内存比较函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int fat_getpartition (void ) { unsigned char *mbr = &_fat_buf; bpb_t *bpb = (bpb_t *)&_fat_buf; if (sd_readblock(0 , &_fat_buf, 1 )) { if (mbr[510 ] != 0x55 || mbr[511 ] != 0xAA ) { uart_puts("ERROR: Bad magic in MBR\n" ); return 0 ; } if (mbr[0x1C2 ] != 0xE && mbr[0x1C2 ] != 0xC ) { uart_puts("ERROR: Wrong partition type\n" ); } partitionlba = mbr[0x1C6 ] + (mbr[0x1C7 ] << 8 ) + (mbr[0x1C8 ] << 16 ) + (mbr[0x1C9 ] << 24 ); if (!sd_readblock(partitionlba, &_fat_buf, 1 )) { uart_puts("ERROR: Unable to read boot record\n" ); return 0 ; } magic bytes if (!(bpb->fst[0 ] == 'F' && bpb->fst[1 ] == 'A' && bpb->fst[2 ] == 'T' ) && !(bpb->fst2[0 ] == 'F' && bpb->fst2[1 ] == 'A' && bpb->fst2[2 ] == 'T' )) { uart_puts("ERROR: Unknown file system type\n" ); return 0 ; } return 1 ; } return 0 ; }
读取 sd 的第一个扇区 mbr 分区表,找见 fat32 文件系统所在分区的起始扇区。读取 fat32 文件系统的起始扇区到_fat_buf 位置处,该位置在 link.ld 中已定义。检查文件系统是否是 fat32 类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 unsigned int fat_getcluster (char *fn) { bpb_t *bpb = (bpb_t *)&_fat_buf; fatdir_t *dir = (fatdir_t *)(&_fat_buf + 512 ); unsigned int root_sec, s; root_sec = ((bpb->spf16 ? bpb->spf16 : bpb->spf32) * bpb->nf) + bpb->rsc; s = (bpb->nr0 + (bpb->nr1 << 8 )) * sizeof (fatdir_t ); if (bpb->spf16 == 0 ) { root_sec += (bpb->rc - 2 ) * bpb->spc; } root_sec += partitionlba; if (sd_readblock(root_sec, (unsigned char *)dir, s / 512 + 1 )) { for (; dir->name[0 ] != 0 ; dir++) { if (dir->name[0 ] == 0xE5 || dir->attr[0 ] == 0xF ) continue ; if (!memcmp (dir->name, fn, 11 )) { return ((unsigned int )dir->ch) << 16 | dir->cl; } } uart_puts("ERROR: file not found\n" ); } else { uart_puts("ERROR: Unable to load root directory\n" ); } return 0 ; }
查找根据文件名查找 root 根目录下的文件,如果找见该文件,则返回文件的第一个簇号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ***Read a file into memory * / char *fat_readfile (unsigned int cluster) { bpb_t *bpb = (bpb_t *)&_fat_buf; unsigned int *fat32 = (unsigned int *)(&_fat_buf + bpb->rsc * 512 ); unsigned short *fat16 = (unsigned short *)fat32; unsigned int data_sec, s; unsigned char *data, *ptr; data_sec = ((bpb->spf16 ? bpb->spf16 : bpb->spf32) * bpb->nf) + bpb->rsc; s = (bpb->nr0 + (bpb->nr1 << 8 )) * sizeof (fatdir_t ); if (bpb->spf16 > 0 ) { data_sec += (s + 511 ) >> 9 ; } data_sec += partitionlba; s = sd_readblock(partitionlba + 1 , (unsigned char *)&_fat_buf + 512 , (bpb->spf16 ? bpb->spf16 : bpb->spf32) + bpb->rsc); data = ptr = (char *)KERNEL_IMAGE; while (cluster > 1 && cluster < 0xFFF8 ) { sd_readblock((cluster - 2 ) * bpb->spc + data_sec, ptr, bpb->spc); ptr += bpb->spc * (bpb->bps0 + (bpb->bps1 << 8 )); cluster = bpb->spf16 > 0 ? fat16[cluster] : fat32[cluster]; } return (char *)data; }
传入参数第一个簇号,计算文件数据第一个扇区的相对 fat32 文件系统分区的 LBA 偏移,将相对偏移添加文件系统起始扇区号变为绝对 LBA 地址,加载 fat 表,根据文件数据的簇号联系读取文件内容到宏定义 KERNEL_IMAGE 位置处,返回文件在内存的起始地址。
MMU mmu.h
1 2 void mmu_init () ;void setExecPages (unsigned long start_addr, unsigned long end_addr, int attr) ;
定义了 2 个接口,mmu 初始化,设置页表属性
mmu.c
1 2 3 4 5 6 #define NUM_4k 4096 #define NUM_1k 1024 #define PAGETABLE_LOW_EL1 (0x200000 - 512*NUM_1k) 0x00000000 ~0x40000000 映射物理地址 0 ~1 G 到线性地址低端#define PAGETABLE_HIGH_EL1 (0x200000 - 256*NUM_1k) 0xffffff8000000000 ~0xffffff8040000000 映射物理地址 0 ~32 M 到线性地址高端
页表初始化时 ttbr0 页表和 ttbr1 页表所在的内存物理地址位置,其中 ttbr0 页表在 1.5MB处,ttbr1 页表在 1.75MB 处,分别是宏定义 PAGETABLE_LOW_EL1 和 PAGETABLE_HIGH_EL1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define PAGESIZE 4096 #define PT_PAGE 0b11 #define PT_BLOCK 0b01 #define PT_KERNEL (0<<6) #define PT_USER (1<<6) #define PT_RW (0<<7) #define PT_RO (1<<7) #define PT_AF (1<<10) #define PT_NX (1UL<<54) #define PT_OSH (2<<8) #define PT_ISH (3<<8) #define PT_MEM (0<<2) #define PT_DEV (1<<2) #define PT_NC (2<<2) #define TTBR_CNP 1
一些属性相关的宏定义 mmu_init 内容较长,分段解释ttbr0 页表
1 2 3 4 5 6 7 8 void mmu_init () { unsigned long data_page = (unsigned long )&_data / PAGESIZE; unsigned long r, b, *pagingttbr0 = (unsigned long *)PAGETABLE_LOW_EL1; unsigned long *pagingttbr1 = (unsigned long *)PAGETABLE_HIGH_EL1;
用了 2 个 long 类型指针 pagingttbr0 和 pagingttbr1 分别指向 PAGETABLE_LOW_EL1 和PAGETABLE_HIGH_EL1 位置内存。data_page 是 loader 的 data 段与 0x80000 的偏移页表数量。data_page 之前是 text 段,之后是 data 段。
1 2 3 4 5 6 7 8 pagingttbr0[0 ] = (unsigned long )((unsigned char *)PAGETABLE_LOW_EL1 + PAGESIZE) | PT_PAGE | PT_AF | PT_USER | PT_ISH | PT_MEM;
建立 ttbr0 的 level 1 页表第一个表项,指向下一个连续的物理页首地址,因为物理内存仅有1G,level 1 页表只占用 1 个表项
1 2 3 4 5 6 7 8 pagingttbr0[1 * 512 ] = (unsigned long )((unsigned char *)PAGETABLE_LOW_EL1 + 2 * PAGESIZE) | physical address PT_PAGE | PT_AF | PT_USER | PT_ISH | PT_MEM;
ttbr0 的 level 2 页表,第一个表项指向下一物理页的首地址,以备后续建立 4k map
1 2 3 4 5 6 7 8 9 10 11 b = MMIO_BASE >> 21 ; for (r = 1 ; r < 512 ; r++) pagingttbr0[1 * 512 + r] = (unsigned long )((r << 21 )) | PT_BLOCK | PT_AF | PT_NX | PT_USER | (r >= b ? PT_OSH | PT_DEV : PT_ISH | PT_MEM); memory
ttbr0 的 level 2 页表第 1-511 页用于建立 2M 的 map,可用覆盖 2M~1G 这片物理内存地址空间,区分了常规内存和设备内存,其区别在于——设备内存属性为 outter shareable 和device MMIO,常规内存属性为 inner shareable 和 normal memory。
1 2 3 4 5 6 7 8 for (r=0 ;r<512 ;r++)pagingttbr0[2 *512 +r]=(unsigned long )(r*PAGESIZE) | PT_PAGE | PT_AF | PT_USER | PT_ISH | ((r<0x80 ||r>=data_page)? PT_RW|PT_NX : PT_RO);
物理地址 02M 的空间采用了 4k map,位于 level 3 页表上,也就是 loader 所在的地址区间,需要区分程序的代码段和数据段,代码段位于 0x80000data_page*4k 采用只读属性,其余空间采用读写不可执行属性。
ttbr0 的页表结构如下图所示
ttbr1 页表
1 2 3 4 5 6 7 pagingttbr1[511 ] = (unsigned long )((unsigned char *)PAGETABLE_HIGH_EL1 + PAGESIZE) | PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_MEM;
ttbr1 的 level 1 页表最后一项指向下一物理页,这一表项以后用于设备内存在 ttbr1 的映射。
1 2 3 4 5 6 pagingttbr1[0 ] = (unsigned long )((unsigned char *)PAGETABLE_HIGH_EL1 + 3 * PAGESIZE) | PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_MEM;
ttbr1 的 level 1 页表第一项指向往后第 3 张页表的起始地址,因为只给内核建立 64MB 空间,所以 level 1 页表只需要一个表项,该表项有特权属性 PT_KERNEL.
1 2 3 4 5 6 7 8 pagingttbr1[1 * 512 + 511 ] = (unsigned long )((unsigned char *)PAGETABLE_HIGH_EL1 + 2 * PAGESIZE) | physical address PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_MEM;
ttbr1 起第二项页表的最后一个表项指向下一个页起始地址
1 2 3 4 5 6 7 for (r = 0 ; r < 32 ; r++) pagingttbr1[3 * 512 + r] = (unsigned long )((unsigned char *)PAGETABLE_HIGH_EL1 + 4 * PAGESIZE + r * PAGESIZE) | PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_MEM;
在 level 2 页表中建立 32 个表项,来建立 64MB 的 4k map,每项指向后面连续一张物理页。
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 0 ; i < 32 ; i++) { for (r = 0 ; r < 512 ; r++) { pagingttbr1[(4 + i) * 512 + r] = (unsigned long )(512 * i * PAGESIZE + r * PAGESIZE) | PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_RW | PT_NX; } }
ttbr1 的 level 3 页表,共 32 张物理页,每张物理页映射了 2MB 物理内存,每一个表项表示4KB 内存,覆盖了 0~64MB 的物理地址空间,这里内存属性全部暂时默认为了可读写不可执行,因为后续还要加载 kernel elf 文件,需要读写_kernel_addr 位置的内存。
1 2 3 4 5 6 7 8 pagingttbr1[2 * 512 ] = (unsigned long )(MMIO_BASE + 0x00201000 ) | PT_PAGE | PT_AF | PT_NX | PT_KERNEL | PT_OSH | PT_DEV;
在 ttbr1 起第三张页表的第一项建立 UART 串口读写的映射,也就是之前 uart 章节里的uart_DR 寄存器,实现用 0xffffff8000201000 地址来读写串口实现显示和输出。
enable MMU 设置
1 2 3 4 5 6 7 8 9 asm volatile ("mrs %0, id_aa64mmfr0_el1" : "=r" (r)) ;b = r & 0xF ; if (r & (0xF << 28 ) || b < 1 ){ uart_puts("ERROR: 4k granule or 36 bit address space not supported\n" ); return ; }
检查 AArch64 内存模型特性寄存器 0,AArch64 Memory Model Feature Register 0 的值,判断 cpu 是否支持 4K 和至少 bus 有 36bit 的宽度,因为三级页表+4K 占 3*9+11=36bit。
1 2 3 4 5 6 7 8 example r = (0xFF << 0 ) | (0x04 << 8 ) | (0x44 << 16 ); asm volatile ("msr mair_el1, %0" : : "r" (r)) ;
设置页表 mair_el1 属性 ARM.v8 架构引入了 mair_el1 寄存器。该寄存器由 8 个部分组成,每个部分为 8 位长。每个这样的部分都配置了一组通用属性。然后,描述符仅指定 mair 部分的索引,而不是直接指定所有属性。这允许仅使用描述符中的 3 位来引用 mair 部分。 mair 部分中每个位的含义在 AArch64-Reference-Manual 的第 3557 页 D13.2.95 节进行了描述。在这里,我们只使用了几个可用的属性选项。这是为 mair 寄存器准备值的代码。 在这里,我们只使用了 mair 寄存器中 8 个可用插槽中的 3 个。第二个对应于设备内存,第三个对应于普通的不可缓存内存。DEVICE_nGnRnE 和 NORMAL_NOCACHE 是我们将在块描述符中使用的索引,0x04 和 0x44 是我们存储在 mair_el1 寄存器的前 2 个插槽中的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 r = (0b00 LL << 37 ) | (b << 32 ) | (0b10 LL << 30 ) | (0b11 LL << 28 ) | (0b01 LL << 26 ) | (0b01 LL << 24 ) | (0b0 LL << 23 ) | (25L L << 16 ) | (0b00 LL << 14 ) | (0b11 LL << 12 ) | (0b01 LL << 10 ) | (0b01 LL << 8 ) | (0b0 LL << 7 ) | (25L L << 0 ); asm volatile ("msr tcr_el1, %0; isb" : : "r" (r)) ;
TCR_EL1 寄存器主要用来控制这 TTBR0 和 TTBR1 两套地址翻译系统,重点是要设置 T0SZ 和T1SZ 为 25,因为我们的页表翻译系统是 3 级页表+4K map,64bit 的地址高 25 位不计入地址翻译。
T1SZ, bits [21:16]和 T0SZ, bits [5:0]定义了虚拟地址的宽度。
TBI1,bit[38]和 TBI0,bit[37]用来控制是否忽略地址的高 8 位(TBI 就是 Top Byte ignored 的 意思),如果允许忽略地址的高 8 位,那么 MMU 的硬件在进行地址比对,匹配的时候忽略 高八位。
TG1,bits [31:30]和 TG0,bits [15:14]是用来控制 page size 的,可以是 4K,16K 或者 64K。
SH1, bits [29:28]和 SH0, bits [13:12]是用来控制页表所在 memory 的 Shareability attribute。ORGN1, bits [27:26]和 ORGN0, bits [11:10]用来控制页表所在 memory 的 outercachebility attribute 的。
IRGN1, bits [25:24]和IRGN0, bits [9:8]用来控制页表所在memory的inner cachebility attribute的。
最后将 r 的值写入到 tcr_el1 寄存器中。
1 2 3 4 5 asm volatile ("msr ttbr0_el1, %0" : : "r" ((unsigned long )PAGETABLE_LOW_EL1 + TTBR_CNP)) ;asm volatile ("msr ttbr1_el1, %0" : : "r" ((unsigned long )PAGETABLE_HIGH_EL1 + TTBR_CNP)) ;
这两句汇编指令是设置 ttbr0 和 ttbr1 页表寄存器的页表入口地址,分别是宏定义PAGETABLE_LOW_EL1 和 PAGETABLE_HIGH_EL1,TTBR_CNP 是 ASID - 局部页表的地址空间标识。
地址空间标识 ASID 许多现代的操作系统让所有的应用运行在同样的地址区域,这就是我们提到的用户地址空间。实际上,不同的应用需要不同的地址映射。比如,VA 0x8000 实际转换的物理地址取决于当前正在运行的应用,即每个应用程序自己维护一个页表。
理想状态下,我们希望不同的应用的页表项共存于 TLBs 中,防止上下文切换时 TLB 中没有当前应用(准确地讲应该是指进程)的页表项。但是处理器怎么知道不同应用的 VA 0x8000 对应的物理地址呢?在 ARMv8-A 架构中,用的是 Address Space Identifier (ASIDs)。
对于 EL0/EL1 虚拟地址空间,通过页表项属性字段的 nG 位标记页表为 Global(G)或者 Non-Global(nG)。比如,内核地址映射为全局页表,应用程序地址映射为非全局页表。不管当前执行的是哪个应用程序,全局页表都是生效的;非全局页表只有在特定应用执行时才生效。
非全局页表项在 TLBs 中使用 ASID 标记。在进行 TLB 查找时,将当前选择的 ASID 与TLB 页表项中的 ASID 进行比较。如果不匹配,则表示当前 TLB 页表项无效。下图显示了全局页表和局部页表,以及 ASID 标记:
上图展示了不同应用的 TLB 页表项可以在缓存中共存,由 ASID 决定哪一个页表项生效。
ASID 保存在两个 TTBRn_EL1 寄存器之中,通常用户空间使用 TTBR0_EL1 寄存器。因此,TTBRn_EL1 寄存器值的更新会同时更新 ASID 和当前生效的页表项。
TTBR0_ELx 和 TTBR1_ELx BADDR - 起始页表项的物理地址 ASID - 局部页表的地址空间标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 page translation asm volatile ("dsb ish; isb; mrs %0, sctlr_el1" : "=r" (r)) ;r |= 0xC00800 ; r &= ~((1 << 25 ) | (1 << 24 ) | (1 << 19 ) | (1 << 12 ) | (1 << 4 ) | (1 << 3 ) | (1 << 2 ) | (1 << 1 )); r |= (1 << 0 ); asm volatile ("msr sctlr_el1, %0; isb" : : "r" (r)) ;
mmu_init 的最后通过设置 sctlr_el1 启动 EL0 和 EL1 的 MMU SCTLR_EL1 是一个对整个系统(包括 memory system)进行控制的寄存器。 C bit[2]是用来 enable 或者 disable EL0 & EL1 的 data cache。具体包括通过 stage 1 translation table 访问的 memory 以及对 stage 1 translation table 自身 memory 的访问。 I bit[12]是用来 enable 或者 disable EL0 & EL1 的 instruction cache。 M bit[0]是用来 enable 或者 disable EL0 & EL1 的 MMU。
其它 bit 位含义介绍见下表
setExecPages
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void setExecPages (unsigned long start_addr, unsigned long end_addr, int attr) { unsigned long r, *paging = (unsigned long *)PAGETABLE_HIGH_EL1; unsigned long *pagingttbr0 = (unsigned long *)PAGETABLE_LOW_EL1; if (attr == 0 ) { for (int i = 0 ; i < 32 ; i++) { for (r = 0 ; r < 512 ; r++) { if ((i * 512 + r) >= start_addr >> 12 && (i * 512 + r) < end_addr >> 12 ) { paging[(4 + i) * 512 + r] = (unsigned long )(512 * i * 4096 + r * 4096 ) | PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_RO | PT_NX; } } } }
这段代码显示了函数的形参,起始地址,终止地址,属性;设置了 rodata 段的页表属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 else if (attr == 1 ) { for (int i = 0 ; i < 32 ; i++) { for (r = 0 ; r < 512 ; r++) { if ((i * 512 + r) >= start_addr >> 12 && (i * 512 + r) < end_addr >> 12 ) { paging[(4 + i) * 512 + r] = (unsigned long )(512 * i * 4096 + r * 4096 ) | physical address PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_RO; } } } } else if (attr == 2 ) { for (int i = 0 ; i < 32 ; i++) { for (r = 0 ; r < 512 ; r++) { if ((i * 512 + r) >= start_addr >> 12 && (i * 512 + r) < end_addr >> 12 ) { paging[(4 + i) * 512 + r] = (unsigned long )(512 * i * 4096 + r * 4096 ) | physical address PT_PAGE | PT_AF | PT_KERNEL | PT_ISH | PT_RW | PT_NX; } } } }
这两个 else if 处理了代码段和 data 段的页表属性设置,主要是读写和执行属性
1 2 3 4 5 6 7 8 else if (attr == -1 ) { pagingttbr0[1 * 512 + 1 ] = (unsigned long )((1 << 21 )) | PT_BLOCK | PT_AF | PT_USER | PT_ISH; }
这个 else if 分支让 USER 态可以执行 kernel 的 text 代码段,因为创建的第一个用户态程序的指令是在 kernel 中的,以后 kernel 实现了加载 elf 文件后就不再使用这段代码了。
elf 解析与加载 elf.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr; typedef struct { Elf64_Word sh_name; Elf64_Word sh_type; Elf64_Xword sh_flags; Elf64_Addr sh_addr; Elf64_Off sh_offset; Elf64_Xword sh_size; Elf64_Word sh_link; Elf64_Word sh_info; Elf64_Xword sh_addralign; Elf64_Xword sh_entsize; } Elf64_Shdr; typedef struct { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; Elf64_Xword p_filesz; Elf64_Xword p_memsz; Elf64_Xword p_align; } Elf64_Phdr;
与 x86 的 miniOS 中的 elf.h 一样,只是将数据结构中的成员类型改为 64 位数据结构,参考了 Linux 的/usr/include/elf.h 文件中对 elf header,Program Header 和 Section header 的定义。
elf.c 移植自 miniOS-x86,在加载 program 段的时候,做了少许修改,新增内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 else if (Echo_Phdr[ph_num]->p_flags == 0x7 ) { exec_elfPhdrcpy(fd, Echo_Phdr[ph_num], 1 ); for (sh_num = 0 ; sh_num < Echo_Ehdr->e_shnum; sh_num++) { if (Echo_Shdr[sh_num]->sh_flags == 0x6 ) { start_addr = Echo_Shdr[sh_num]->sh_addr; end_addr = Echo_Shdr[sh_num]->sh_addr + Echo_Shdr[sh_num]->sh_size; end_addr = end_addr % 4096 == 0 ? end_addr : end_addr + (4096 - end_addr % 4096 ); start_addr &= 0x0000007fffffffff ; end_addr &= 0x0000007fffffffff ; setExecPages(start_addr, end_addr, 1 ); } else if (Echo_Shdr[sh_num]->sh_flags == 0x32 ) { start_addr = Echo_Shdr[sh_num]->sh_addr; end_addr = Echo_Shdr[sh_num]->sh_addr + Echo_Shdr[sh_num]->sh_size; end_addr = end_addr % 4096 == 0 ? end_addr : end_addr + (4096 - end_addr % 4096 ); start_addr &= 0x0000007fffffffff ; end_addr &= 0x0000007fffffffff ; setExecPages(start_addr, end_addr, 0 ); } else if (Echo_Shdr[sh_num]->sh_flags == 0x3 ) { start_addr = Echo_Shdr[sh_num]->sh_addr; end_addr = Echo_Shdr[sh_num]->sh_addr + Echo_Shdr[sh_num]->sh_size; end_addr = end_addr % 4096 == 0 ? end_addr : end_addr + (4096 - end_addr % 4096 ); start_addr &= 0x0000007fffffffff ; end_addr &= 0x0000007fffffffff ; setExecPages(start_addr, end_addr, 2 ); } else { continue ; }
exec_load 函数中,本次的 kernel 只有一个 LOAD 段,需要对具体的 section 做进一步处理,在 Echo_Phdr[ph_num]->p_flags 的分类讨论添加一种情况,表示该 LOAD 段属性既有读写又有执行,即 flags==0x7;exec_elfPhdrcpy 用法与 miniOS-x86 一样, 这里需要对不同的段设置页表属性。 section 中 text 段,计算完起始页表物理地址后传入 setExecPages 函数设置页表属性为只读可执行 section 中 rodata 段,计算完起始页表物理地址后传入 setExecPages 函数设置页表属性为只读不可执行 section 中 data,bss 段,计算完起始页表物理地址后传入 setExecPages 函数设置页表属性为可读写不可执行
1 2 3 4 5 6 7 8 9 10 11 int kern_exec (char *data) { Elf64_Ehdr *Echo_Ehdr; Elf64_Phdr *Echo_Phdr[10 ]; Elf64_Shdr *Echo_Shdr[20 ]; read_elf(data, &Echo_Ehdr, Echo_Phdr, Echo_Shdr); exec_load(data, Echo_Ehdr, Echo_Phdr, Echo_Shdr); }
kern_exec 接口函数将被 main.c 调用,实现 kernel elf 文件的加载和解析。原理与 miniOSx86 一样,不做过多叙述。
kernel 的 link.ld 里指定了 kernel.elf 的起始虚拟地址是 0xffffff8000200000