一.概述

实现 loader

  1. 实现了汇编准备和跳转到 C 程序
    EL 等级切换,sp 栈指针,bss 段清理,异常向量和跳转到 C 程序
  2. 实现了串口显示
  3. 实现了屏幕显示
  4. 实现了 sd 读写
  5. 实现了 fat32 文件系统及其读取
  6. 实现了 mmu 初始化,建立了 2 份页表
  7. 加载了 kernel.img 镜像文件,并跳转到 kernel 执

二.具体实现

loader 汇编部分

树莓派 3 默认从 0x80000 处执行,loader 程序将被加载到内存的物理地址 0x80000 处

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); /*output format buffer*/
_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:
// read cpu id, stop slave cores
mrs x1, mpidr_el1
and x1, x1, #3
cbz x1, 2f
// cpu id > 0, stop
1: wfe
b 1b
2: // cpu id == 0

_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: // cpu id == 0
// set stack before our code
ldr x1, =_start
// set up EL1
mrs x0, CurrentEL
and x0, x0, #12 // clear reserved bits
// running at EL3?
cmp x0, #12
bne 5f
// should never be executed, just for completeness
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
// running at EL2?
5: cmp x0, #4
beq 5f
msr sp_el1, x1
// enable CNTP for EL1
mrs x0, cnthctl_el2
orr x0, x0, #3
msr cnthctl_el2, x0
msr cntvoff_el2, xzr
// disable coprocessor traps
mov x0, #0x33FF
msr cptr_el2, x0
msr hstr_el2, xzr
mov x0, #(3 << 20)
msr cpacr_el1, x0
// enable AArch64 in EL1
mov x0, #(1 << 31) // AArch64
orr x0, x0, #(1 << 1) // SWIO hardwired on Pi3
msr hcr_el2, x0
mrs x0, hcr_el2
// Setup SCTLR access
mov x2, #0x0800
movk x2, #0x30d0, lsl #16
msr sctlr_el1, x2
// set up exception handlers
ldr x2, =_vectors
msr vbar_el1, x2
// change execution level to EL1
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, x1
// clear bss
ldr x1, =__bss_start
ldr w2, =__bss_size
3: cbz w2, 4f
str xzr, [x1], #8
sub w2, w2, #1
cbnz w2, 3b
// jump to C code, should not return
4: bl main
// for failsafe, halt this core too
ldr 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
// a dummy exception handler in this tutorial
exc_handler:
eret
// important, code has to be properly aligned
.align 11
_vectors:
// synchronous
.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()
{
// set up serial console and linear frame buffer
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;
}
// find out file in root directory entries
cluster = fat_getcluster("KER ELF");
if (cluster)
{
// read into memory
data = fat_readfile(cluster);
// uart_dump(fat_readfile(cluster));
}
else
{
uart_puts("there is no File\n");
}
// set up paging
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
/* a properly aligned buffer */
extern volatile unsigned int mbox[36];
#define MBOX_REQUEST 0
/* channels */
#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
/* tags */
#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
/* mailbox message buffer */
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
/**
* Make a mailbox call. Returns 0 on failure, non-zero on success
*/
int mbox_call(unsigned char ch)
{
unsigned int r = (((unsigned int)((unsigned long)&mbox) & ~0xF) | (ch & 0xF));
/* wait until we can write to the mailbox */
do
{
asm volatile("nop");
} while (*MBOX_STATUS & MBOX_FULL);
/* write the address of our message to the mailbox with channel identifier */
*MBOX_WRITE = r;
/* now wait for the response */
while (1)
{
/* is there a response? */
do
{
asm volatile("nop");
} while (*MBOX_STATUS & MBOX_EMPTY);
/* is it a response to our message? */
if (r == *MBOX_READ)
/* is it a valid successful response? */
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 位)一起写入写寄存器
image.png
需要准备 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
/* PL011 UART registers */
#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))
// get address from linker
extern volatile unsigned char _op_buf;

这里定义了 uart0 串口的地址,在 MMIO_BASE 基础上添加一个偏移量。
这些寄存器的具体含义见 BCM2837-ARM-Peripherals 的第 177 页,寄存器地址表和简介如下图所示
image.png
image.png

关于表中是 0x7E 为起始地址,但是代码中用 0x3F 作为起点,原因如下
image.png
此处在总线地址 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
/**
* Set baud rate and characteristics (115200 8N1) and map to GPIO
*/
void uart_init()
{
register unsigned int r;
/* initialize UART */
*UART0_CR = 0; // turn off UART0
/* set up clock for consistent divisor values */
mbox[0] = 9 * 4;
mbox[1] = MBOX_REQUEST;
mbox[2] = MBOX_TAG_SETCLKRATE; // set clock rate
mbox[3] = 12;
mbox[4] = 8;
mbox[5] = 2; // UART clock
mbox[6] = 4000000; // 4Mhz
mbox[7] = 0; // clear turbo
mbox[8] = MBOX_TAG_LAST;
mbox_call(MBOX_CH_PROP);
/* map UART0 to GPIO pins */
r = *GPFSEL1;
r &= ~((7 << 12) | (7 << 15)); // gpio14, gpio15
r |= (4 << 12) | (4 << 15); // alt0
*GPFSEL1 = r;
*GPPUD = 0; // enable pins 14 and 15
wait_cycles(150);
*GPPUDCLK0 = (1 << 14) | (1 << 15);
wait_cycles(150);
*GPPUDCLK0 = 0; // flush GPIO setup
*UART0_ICR = 0x7FF; // clear interrupts
*UART0_IBRD = 2; // 115200 baud
*UART0_FBRD = 0xB;
*UART0_LCRH = 0b11 << 5; // 8n1
*UART0_CR = 0x301; // enable Tx, Rx, FIFO
}

该函数初始化了 uart0 串口,将 uart0 串口 map 到 gpio 引脚 14 和 15 才能实现输入输出
image.png

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
/**
* Send a character
*/
void uart_send(unsigned int c)
{
/* wait until we can send */
do
{
asm volatile("nop");
} while (*UART0_FR & 0x20);
/* write the character to the buffer */
*UART0_DR = c;
}
/**
* Receive a character
*/
char uart_getc()
{
char r;
/* wait until something is in the buffer */
do
{
asm volatile("nop");
} while (*UART0_FR & 0x10);
/* read it and return */
r = (char)(*UART0_DR);
/* convert carrige return to newline */
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
/**
* Display a string
*/
void uart_puts(char *s)
{
while (*s)
{
/* convert newline to carrige return + newline */
if (*s == '\n')
uart_send('\r');
uart_send(*s++);
}
}
/**
* Display a binary value in hexadecimal
*/
void uart_hex(unsigned int d)
{
unsigned int n;
int c;
for (c = 28; c >= 0; c -= 4)
{
// get highest tetrad
n = (d >> c) & 0xF;
// 0-9 => '0'-'9', 10-15 => 'A'-'F'
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
/**
* Dump memory
*/
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 命令功能,效果如下:
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Display a string
*/
void uart_printf(char *fmt, ...)
{
__builtin_va_list args;
__builtin_va_start(args, fmt);
// we don't have memory allocation yet, so we
// simply place our string after our code
char *s = (char *)&_op_buf;
// use sprintf to format our string
vsprintf(s, fmt, args);
// print out as usual
while (*s)
{
/* convert newline to carrige return + newline */
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
/**
* minimal sprintf implementation
*/
unsigned int vsprintf(char *dst, char *fmt, __builtin_va_list args)
{
long int arg;
int len, sign, i;
char *p, *orig = dst, tmpstr[19];
// failsafes
if (dst == (void *)0 || fmt == (void *)0)
{
return 0;
}
// main loop
arg = 0;
while (*fmt)
{
// argument access
if (*fmt == '%')
{
fmt++;
// literal %
if (*fmt == '%')
{
goto put;
}
len = 0;
// size modifier
while (*fmt >= '0' && *fmt <= '9')
{
len *= 10;
len += *fmt - '0';
fmt++;
}
// skip long modifier
if (*fmt == 'l')
{
fmt++;
}
// character
if (*fmt == 'c')
{
arg = __builtin_va_arg(args, int);
*dst++ = (char)arg;
fmt++;
continue;
}
else
// decimal number
if (*fmt == 'd')
{
arg = __builtin_va_arg(args, int);
// check input
sign = 0;
if ((int)arg < 0)
{
arg *= -1;
sign++;
}
if (arg > 99999999999999999L)
{
arg = 99999999999999999L;
}
// convert to string
i = 18;
tmpstr[i] = 0;
do
{
tmpstr[--i] = '0' + (arg % 10);
arg /= 10;
} while (arg != 0 && i > 0);
if (sign)
{
tmpstr[--i] = '-';
}
// padding, only space
if (len > 0 && len < 18)
{
while (i > 18 - len)
{
tmpstr[--i] = ' ';
}
}
p = &tmpstr[i];
goto copystring;
}
else
// hex number
if (*fmt == 'x')
{
arg = __builtin_va_arg(args, long int);
// convert to string
i = 16;
tmpstr[i] = 0;
do
{
char n = arg & 0xf;
// 0-9 => '0'-'9', 10-15 => 'A'-'F'
tmpstr[--i] = n + (n > 9 ? 0x37 : 0x30);
arg >>= 4;
} while (arg != 0 && i > 0);
// padding, only leading zeros
if (len > 0 && len <= 16)
{
while (i > 16 - len)
{
tmpstr[--i] = '0';
}
}
p = &tmpstr[i];
goto copystring;
}
else
// string
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;
// number of bytes written
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
/* PC Screen Font as used by Linux Console */
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;
/* Scalable Screen Font (https://gitlab.com/bztsrc/scalable-font2) */
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
/**
* Set screen resolution to 800*600
*/
void lfb_init()
{
mbox[0] = 35 * 4;
mbox[1] = MBOX_REQUEST;
mbox[2] = 0x48003; // set phy wh
mbox[3] = 8;
mbox[4] = 8;
mbox[5] = 800; // FrameBufferInfo.width
mbox[6] = 600; // FrameBufferInfo.height
mbox[7] = 0x48004; // set virt wh
mbox[8] = 8;
mbox[9] = 8;
mbox[10] = 800; // FrameBufferInfo.virtual_width
mbox[11] = 600; // FrameBufferInfo.virtual_height
mbox[12] = 0x48009; // set virt offset
mbox[13] = 8;
mbox[14] = 8;
mbox[15] = 0; // FrameBufferInfo.x_offset
mbox[16] = 0; // FrameBufferInfo.y.offset
mbox[17] = 0x48005; // set depth
mbox[18] = 4;
mbox[19] = 4;
mbox[20] = 32; // FrameBufferInfo.depth
mbox[21] = 0x48006; // set pixel order
mbox[22] = 4;
mbox[23] = 4;
mbox[24] = 1; // RGB, not BGR preferably
mbox[25] = 0x40001; // get framebuffer, gets alignment on request
mbox[26] = 8;
mbox[27] = 8;
mbox[28] = 4096; // FrameBufferInfo.pointer
mbox[29] = 0; // FrameBufferInfo.size
mbox[30] = 0x40008; // get pitch
mbox[31] = 4;
mbox[32] = 4;
mbox[33] = 0; // FrameBufferInfo.pitch
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。
image.png
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
/**
* Display a string using fixed size PSF
*/
void lfb_print(char *s)
{
static int x = 0, y = 0;
// get our font
psf_t *font = (psf_t *)&_binary_font_psf_start;
// 到屏幕最右侧自动换下一行,到屏幕最底部重新回到第一行显示
if (y > 589)
{
y = 0;
}
if (x > 789)
{
y += font->height;
x = 0;
}
// draw next character if it's not zero
while (*s)
{
// get the offset of the glyph. Need to adjust this to support unicode table
unsigned char *glyph = (unsigned char *)&_binary_font_psf_start +
font->headersize + (*((unsigned char *)s) < font->numglyph ? *s : 0) * font->bytesperglyph;
// calculate the offset on screen
int offs = (y * pitch) + (x * 4);
// variables
int i, j, line, mask, bytesperline = (font->width + 7) / 8;
// handle carrige return
if (*s == '\r')
{
x = 0;
}
else
// new line
if (*s == '\n')
{
x = 0;
y += font->height;
}
else
{
// display a character
for (j = 0; j < font->height; j++)
{
// display one row
line = offs;
mask = 1 << (font->width - 1);
for (i = 0; i < font->width; i++)
{
// if bit set, we use white color, otherwise black
*((unsigned int *)(lfb + line)) = ((int)*glyph) & mask ? 0xFFFFFF : 0;
mask >>= 1;
line += 4;
}
// adjust to next line
glyph += bytesperline;
offs += pitch;
}
x += (font->width + 1);
}
// next character
s++;
}
}

打印 ASCII 编码的字符,屏幕打印的本质是根据字体信息点亮像素点,在字体结构体中根据像素点数据点亮像素点,自动换行和翻页。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* clear the frame
*/
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 个字节保留。
image.png
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
/**
* format output by frame
*/
void lfb_printf(char *fmt, ...)
{
__builtin_va_list args;
__builtin_va_start(args, fmt);
// we don't have memory allocation yet, so we
// simply place our string after our code
char *s = (char *)&_op_buf;
// use sprintf to format our string
vsprintf(s, fmt, args);
// print out as usual
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
/**
* Get the starting LBA address of the first partition
* so that we know where our FAT file system starts, and
* read that volume's BIOS Parameter Block
*/
int fat_getpartition(void)
{
unsigned char *mbr = &_fat_buf;
bpb_t *bpb = (bpb_t *)&_fat_buf;
// read the partitioning table
if (sd_readblock(0, &_fat_buf, 1))
{
// check magic
if (mbr[510] != 0x55 || mbr[511] != 0xAA)
{
uart_puts("ERROR: Bad magic in MBR\n");
return 0;
}
// check partition type
if (mbr[0x1C2] != 0xE /*FAT16 LBA*/ && mbr[0x1C2] != 0xC /*FAT32 LBA*/)
{
uart_puts("ERROR: Wrong partition type\n");
// return 0;
}
// should be this, but compiler generates bad code...
// partitionlba=*((unsigned int*)((unsigned long)&_fat_buf+0x1C6));
partitionlba = mbr[0x1C6] + (mbr[0x1C7] << 8) + (mbr[0x1C8] << 16) +
(mbr[0x1C9] << 24);
// read the boot record
if (!sd_readblock(partitionlba, &_fat_buf, 1))
{
uart_puts("ERROR: Unable to read boot record\n");
return 0;
}
// check file system type. We don't use cluster numbers for that, but
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
/**
* Find a file in root directory entries
*/
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;
// find the root directory's LBA
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)
{
// adjust for FAT32
root_sec += (bpb->rc - 2) * bpb->spc;
}
// add partition LBA
root_sec += partitionlba;
// load the root directory
if (sd_readblock(root_sec, (unsigned char *)dir, s / 512 + 1))
{
// iterate on each entry and check if it's the one we're looking for
for (; dir->name[0] != 0; dir++)
{
// is it a valid entry?
if (dir->name[0] == 0xE5 || dir->attr[0] == 0xF)
continue;
// filename match?
if (!memcmp(dir->name, fn, 11))
{
// if so, return starting cluster
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)
{
// BIOS Parameter Block
bpb_t *bpb = (bpb_t *)&_fat_buf;
// File allocation tables. We choose between FAT16 and FAT32 dynamically
unsigned int *fat32 = (unsigned int *)(&_fat_buf + bpb->rsc * 512);
unsigned short *fat16 = (unsigned short *)fat32;
// Data pointers
unsigned int data_sec, s;
unsigned char *data, *ptr;
// find the LBA of the first data sector
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)
{
// adjust for FAT16
data_sec += (s + 511) >> 9;
}
// add partition LBA
data_sec += partitionlba;
// load FAT table
s = sd_readblock(partitionlba + 1, (unsigned char *)&_fat_buf + 512, (bpb->spf16 ? bpb->spf16 : bpb->spf32) + bpb->rsc);
// end of FAT in memory
// data=ptr=&_fat_buf+512+s;
data = ptr = (char *)KERNEL_IMAGE;
// iterate on cluster chain
while (cluster > 1 && cluster < 0xFFF8)
{
// load all sectors in a cluster
sd_readblock((cluster - 2) * bpb->spc + data_sec, ptr, bpb->spc);
// move pointer, sector per cluster * bytes per sector
ptr += bpb->spc * (bpb->bps0 + (bpb->bps1 << 8));
// get the next cluster in chain
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~1G 到线性地址低端
#define PAGETABLE_HIGH_EL1 (0x200000 - 256*NUM_1k) //
0xffffff8000000000~0xffffff8040000000 映射物理地址 0~32M 到线性地址高端

页表初始化时 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
// granularity
#define PT_PAGE 0b11 // 4k granule
#define PT_BLOCK 0b01 // 2M granule
// accessibility
#define PT_KERNEL (0<<6) // privileged, supervisor EL1 access only
#define PT_USER (1<<6) // unprivileged, EL0 access allowed
#define PT_RW (0<<7) // read-write
#define PT_RO (1<<7) // read-only
#define PT_AF (1<<10) // accessed flag
#define PT_NX (1UL<<54) // no execute
// shareability
#define PT_OSH (2<<8) // outter shareable
#define PT_ISH (3<<8) // inner shareable
// defined in MAIR register
#define PT_MEM (0<<2) // normal memory
#define PT_DEV (1<<2) // device MMIO
#define PT_NC (2<<2) // non-cachable
#define TTBR_CNP 1

一些属性相关的宏定义
mmu_init
内容较长,分段解释
ttbr0 页表

1
2
3
4
5
6
7
8
/**
* Set up page translation tables and enable virtual memory
*/
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
/* create MMU translation tables at PAGETABLE_LOW_EL1 */
// TTBR0, identity L1
pagingttbr0[0] = (unsigned long)((unsigned char *)PAGETABLE_LOW_EL1 + PAGESIZE) | // physical address
PT_PAGE | // it has the "Present" flag, which must be set, and we have area in it mapped by pages
PT_AF | // accessed flag. Without this we're going to have a Data Abort exception
PT_USER | // non-privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

建立 ttbr0 的 level 1 页表第一个表项,指向下一个连续的物理页首地址,因为物理内存仅有1G,level 1 页表只占用 1 个表项

1
2
3
4
5
6
7
8
// identity L2, first 2M block
pagingttbr0[1 * 512] = (unsigned long)((unsigned char *)PAGETABLE_LOW_EL1 + 2 * PAGESIZE) | //
physical address
PT_PAGE | // we have area in it mapped by pages
PT_AF | // accessed flag
PT_USER | // non-privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

ttbr0 的 level 2 页表,第一个表项指向下一物理页的首地址,以备后续建立 4k map

1
2
3
4
5
6
7
8
9
10
11
// identity L2 2M blocks
b = MMIO_BASE >> 21;
// skip 0th, as we're about to map it by L3
for (r = 1; r < 512; r++)
pagingttbr0[1 * 512 + r] = (unsigned long)((r << 21)) | // physical address
PT_BLOCK | // map 2M block
PT_AF | // accessed flag
PT_NX | // no execute
PT_USER | // non-privileged
(r >= b ? PT_OSH | PT_DEV : PT_ISH | PT_MEM); // different attributes for device
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
// identity L3
for(r=0;r<512;r++)
pagingttbr0[2*512+r]=(unsigned long)(r*PAGESIZE) | // physical address
PT_PAGE | // map 4k
PT_AF | // accessed flag
PT_USER | // non-privileged
PT_ISH | // inner shareable
((r<0x80||r>=data_page)? PT_RW|PT_NX : PT_RO); // different for code and data

物理地址 02M 的空间采用了 4k map,位于 level 3 页表上,也就是 loader 所在的地址区间,需要区分程序的代码段和数据段,代码段位于 0x80000data_page*4k 采用只读属性,其余空间采用读写不可执行属性。

ttbr0 的页表结构如下图所示
image.png

ttbr1 页表

1
2
3
4
5
6
7
// TTBR1, kernel L1
pagingttbr1[511] = (unsigned long)((unsigned char *)PAGETABLE_HIGH_EL1 + PAGESIZE) | // physical address
PT_PAGE | // we have area in it mapped by pages
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

ttbr1 的 level 1 页表最后一项指向下一物理页,这一表项以后用于设备内存在 ttbr1 的映射。

1
2
3
4
5
6
pagingttbr1[0] = (unsigned long)((unsigned char *)PAGETABLE_HIGH_EL1 + 3 * PAGESIZE) | // physical address
PT_PAGE | // we have area in it mapped by pages
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

ttbr1 的 level 1 页表第一项指向往后第 3 张页表的起始地址,因为只给内核建立 64MB 空间,所以 level 1 页表只需要一个表项,该表项有特权属性 PT_KERNEL.

1
2
3
4
5
6
7
8
// kernel L2
pagingttbr1[1 * 512 + 511] = (unsigned long)((unsigned char *)PAGETABLE_HIGH_EL1 + 2 * PAGESIZE) | //
physical address
PT_PAGE | // we have area in it mapped by pages
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

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) | // physical address
PT_PAGE | // we have area in it mapped by pages
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_MEM; // normal memory

在 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++) // 32 * 2M = 64M
{
for (r = 0; r < 512; r++)
{
pagingttbr1[(4 + i) * 512 + r] = (unsigned long)(512 * i * PAGESIZE + r * PAGESIZE) | // physical address
PT_PAGE | // map 4k
PT_AF | // accessed flag
PT_KERNEL | // non-privileged
PT_ISH | // inner shareable
PT_RW | PT_NX; // different for code and data
}
}

ttbr1 的 level 3 页表,共 32 张物理页,每张物理页映射了 2MB 物理内存,每一个表项表示4KB 内存,覆盖了 0~64MB 的物理地址空间,这里内存属性全部暂时默认为了可读写不可执行,因为后续还要加载 kernel elf 文件,需要读写_kernel_addr 位置的内存。

1
2
3
4
5
6
7
8
// kernel L3
pagingttbr1[2 * 512] = (unsigned long)(MMIO_BASE + 0x00201000) | // physical address
PT_PAGE | // map 4k
PT_AF | // accessed flag
PT_NX | // no execute
PT_KERNEL | // privileged
PT_OSH | // outter shareable
PT_DEV; // device memory

在 ttbr1 起第三张页表的第一项建立 UART 串口读写的映射,也就是之前 uart 章节里的uart_DR 寄存器,实现用 0xffffff8000201000 地址来读写串口实现显示和输出。
image.png
image.png

enable MMU 设置

1
2
3
4
5
6
7
8
9
// check for 4k granule and at least 36 bits physical address bus */
asm volatile("mrs %0, id_aa64mmfr0_el1"
: "=r"(r));
b = r & 0xF;
if (r & (0xF << 28) /*4k*/ || b < 1 /*36 bits*/)
{
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
// first, set Memory Attributes array, indexed by PT_MEM, PT_DEV, PT_NC in our
example
r = (0xFF << 0) | // AttrIdx=0: normal, IWBWA, OWBWA, NTR
(0x04 << 8) | // AttrIdx=1: device, nGnRE (must be OSH too)
(0x44 << 16); // AttrIdx=2: non cacheable
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 寄存器准备值的代码。
image.png
在这里,我们只使用了 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
// next, specify mapping characteristics in translate control register
r = (0b00LL << 37) | // TBI=0, no tagging
(b << 32) | // IPS=autodetected
(0b10LL << 30) | // TG1=4k
(0b11LL << 28) | // SH1=3 inner
(0b01LL << 26) | // ORGN1=1 write back
(0b01LL << 24) | // IRGN1=1 write back
(0b0LL << 23) | // EPD1 enable higher half
(25LL << 16) | // T1SZ=25, 3 levels (512G)
(0b00LL << 14) | // TG0=4k
(0b11LL << 12) | // SH0=3 inner
(0b01LL << 10) | // ORGN0=1 write back
(0b01LL << 8) | // IRGN0=1 write back
(0b0LL << 7) | // EPD0 enable lower half
(25LL << 0); // T0SZ=25, 3 levels (512G)
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
// tell the MMU where our translation tables are. TTBR_CNP bit not documented, but required
// lower half, user space
asm volatile ("msr ttbr0_el1, %0" : : "r" ((unsigned long)PAGETABLE_LOW_EL1 + TTBR_CNP));
// upper half, kernel space
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 标记:
image.png

上图展示了不同应用的 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
// finally, toggle some bits in system control register to enable
page translation asm volatile("dsb ish; isb; mrs %0, sctlr_el1"
: "=r"(r));
r |= 0xC00800; // set mandatory reserved bits
r &= ~((1 << 25) | // clear EE, little endian translation tables
(1 << 24) | // clear E0E
(1 << 19) | // clear WXN
(1 << 12) | // clear I, no instruction cache
(1 << 4) | // clear SA0
(1 << 3) | // clear SA
(1 << 2) | // clear C, no cache at all
(1 << 1)); // clear A, no aligment check
r |= (1 << 0); // set M, enable MMU
asm volatile("msr sctlr_el1, %0; isb"
:
: "r"(r));

mmu_init 的最后通过设置 sctlr_el1 启动 EL0 和 EL1 的 MMU
SCTLR_EL1 是一个对整个系统(包括 memory system)进行控制的寄存器。
image.png
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 位含义介绍见下表

image.png
image.png

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++) // 32 * 2M = 64M
{
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 | // map 4k
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_RO | PT_NX; // different for code and data
}
}
}
}

这段代码显示了函数的形参,起始地址,终止地址,属性;设置了 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++) // 32 * 2M = 64M
{
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 | // map 4k
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_RO; // different for code and data
}
}
}
}
else if (attr == 2) // 读写,不可执行
{
for (int i = 0; i < 32; i++) // 32 * 2M = 64M
{
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 | // map 4k
PT_AF | // accessed flag
PT_KERNEL | // privileged
PT_ISH | // inner shareable
PT_RW |
PT_NX; // different for code and data
}
}
}
}

这两个 else if 处理了代码段和 data 段的页表属性设置,主要是读写和执行属性

1
2
3
4
5
6
7
8
else if (attr == -1) // USER 可执行读写 kernel 代码
{
pagingttbr0[1 * 512 + 1] = (unsigned long)((1 << 21)) | // physical address
PT_BLOCK | // map 2M block
PT_AF | // accessed flag
PT_USER | // non-privileged
PT_ISH; // different attributes for device memory
}

这个 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
/* The ELF file header. This appears at the start of every ELF file. */
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word 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 */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
/* Section header. */
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
/* Program segment header. */
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} 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) // 一个 program,多个 section
{
exec_elfPhdrcpy(fd, Echo_Phdr[ph_num], 1); // 进程 Program
for (sh_num = 0; sh_num < Echo_Ehdr->e_shnum; sh_num++)
{
if (Echo_Shdr[sh_num]->sh_flags == 0x6)
{ // text
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)
{ // rodata
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)
{ // bss data
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
/**
* read and load kernel.elf
*/
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 一样,不做过多叙述。

image.png
kernel 的 link.ld 里指定了 kernel.elf 的起始虚拟地址是 0xffffff8000200000

× 请我吃糖~
打赏二维码