



本书中大部分的实验代码是基于BenOS来实现的。BenOS是一个基于ARM64体系结构的小型操作系统。本书的实验会从最简单的裸机程序开始,逐步扩展和丰富,让其具有进程调度、系统调用等现代操作系统的基本功能。
本节介绍最简单的BenOS的代码体系结构,目前它仅仅只有串口显示功能,类似于逻辑程序。
由于我们写的是裸机程序,因此需要手动编写Makefile和链接脚本。对于任何一种可执行程序,不论是.elf还是.exe文件,都是由代码(.text)段、数据(.data)段、未初始化数据(.bss)段等段(section)组成的。链接脚本最终会把一大堆编译好的二进制文件(.o文件)整合为二进制可执行文件,也就是把所有二进制文件整合到一个大文件中。这个大文件由总体的.text/.data/.bss段描述。下面是本实验中的一个链接文件,名为link.ld。
1 SECTIONS
2 {
3 . = 0x80000;
4 .text.boot : { *(.text.boot) }
5 .text : { *(.text) }
6 .rodata : { *(.rodata) }
7 .data : { *(.data) }
8 . = ALIGN(0x8);
9 bss_begin = .;
10 .bss : { *(.bss*) }
11 bss_end = .;
12 }
在第1行中,SECTIONS是LS(Linker Script)语法中的关键命令,用来描述输出文件的内存布局。SECTIONS命令告诉链接文件如何把输入文件的段映射到输出文件的各个段,如何将输入段整合为输出段,以及如何把输出段放入程序地址空间和进程地址空间。
在第3行中,“.”非常关键,它代表位置计数(Location Counter,LC),这里把.text段的链接地址设置为0x80000,这里的链接地址指的是加载地址(load address)。
在第4行中,输出文件的.text.boot段内容由所有输入文件(其中的“*”可理解为所有的.o文件,也就是二进制文件)的.text.boot段组成。
在第5行中,输出文件的.text段内容由所有输入文件(其中的“*”可理解为所有的.o文件,也就是二进制文件)的.text段组成。
在第6行中,输出文件的.rodata段由所有输入文件的.rodata段组成。
在第7行中,输出文件的.data段由所有输入文件的.data段组成。
在第8行中,设置为按8字节对齐。
在第9~11行中,定义了一个.bss段。
因此,上述链接文件定义了如下几个段。
●.text.boot段:启动首先要执行的代码。
●.text段:代码段。
●.rodata段:只读数据段。
●.data段:数据段。
●.bss段:包含未初始化的全局变量和静态变量。
下面开始编写启动用的汇编代码,将代码保存为boot.S文件。
1 #include "mm.h" 2 3 .section ".text.boot" 4 5 .globl _start 6 _start: 7 mrs x0, mpidr_el1 8 and x0, x0,#0xFF 9 cbz x0, master 10 b proc_hang 11 12 proc_hang: 13 b proc_hang 14 15 master: 16 adr x0, bss_begin 17 adr x1, bss_end 18 sub x1, x1, x0 19 bl memzero 20 21 mov sp, #LOW_MEMORY 22 bl start_kernel 23 b proc_hang
启动用的汇编代码不长,下面做简要分析。
在第3行中,把boot.S文件编译链接到.text.boot段中。我们可以在链接文件link.ld中把.text.boot段链接到这个可执行文件的开头,这样当程序执行时将从这个段开始执行。
在第6行中,_start为程序的入口点。
在第7行中,由于树莓派4B有4个CPU内核,但是本实验的裸机程序不希望4个CPU内核都运行,我们只想让第一个CPU内核运行起来。mpidr_el1寄存器是表示处理器内核的编号。
在第8行中,and指令用于完成与操作。
第9行,cbz为比较并跳转指令。如果X0寄存器的值为0,则跳转到master标签处。若X0寄存器的值为0,则表示第1个CPU内核。其他CPU内核则跳转到proc_hang标签处。
在第12和13行,proc_hang标签这里是死循环。
在第15行,对于master标签,只有第一个CPU内核才能运行到这里。
在第16~19行,初始化.bss段。
在第21行中,使SP指向内存的4 MB地址处。树莓派至少有1 GB内存,我们这个裸机程序用不到那么大的内存。
在第22行中,跳转到C语言的start_kernel函数,这里最重要的一步是设置C语言运行环境,即栈。
总之,上述汇编代码还是比较简单的,我们只做了3件事情。
●只让第一个CPU内核运行,让其他CPU内核进入死循环。
●初始化.bss段。
●设置栈,跳转到C语言入口。
接下来,编写C语言的start_kernel函数。本实验的目的是输出一条欢迎语句,因而这个函数的实现比较简单。将代码保存为kernel.c文件。
#include "mini_uart.h"
void start_kernel(void)
{
uart_init();
uart_send_string("Welcome BenOS!\r\n");
while (1) {
uart_send(uart_recv());
}
}
上述代码很简单,主要操作是初始化串口和向串口中输出欢迎语句。
接下来,实现一些简单的串口驱动代码。树莓派有两个串口设备。
●PL011串口,在BCM2711芯片手册中简称UART0,是一种全功能的串口设备。
●Mini串口,在BCM2711芯片手册中简称UART1。
本实验使用PL011串口设备。Mini串口设备比较简单,不支持流量控制(flow control),在高速传输过程中还有可能丢包。
BCM2711芯片里有不少片内外设复用相同的GPIO接口,这称为GPIO可选功能配置(GPIO Alternative Function)。GPIO14和GPIO15可以复用UART0与UART1串口的TXD引脚和RXD引脚,如表2.4所示。关于GPIO可选功能配置的详细介绍,读者可以查阅BCM2711芯片手册。在使用PL011串口之前,我们需要通过编程来使能TXD0和RXD0引脚。
表2.4 GPIO可选功能配置
BCM2711芯片提供了 GFPSEL n 寄存器来设置GPIO可选功能配置,其中 GPFSEL0用来配置GPIO0~GPIO9,而GPFSEL1用来配置GPIO10~GPIO19,以此类推。其中,每个GPIO使用3位来表示不同的含义。
●000:表示GPIO配置为输入
●001:表示GPIO配置为输出。
●100:表示GPIO配置为可选项0。
●101:表示GPIO配置为可选项1。
●110:表示GPIO配置为可选项2。
●111:表示GPIO配置为可选项3。
●011:表示GPIO配置为可选项4。
●010:表示GPIO配置为可选项5。
首先,在include/asm/base.h头文件中加入树莓派寄存器的基地址。
#ifndef _P_BASE_H #define _P_BASE_H #ifdef CONFIG_BOARD_PI3B #define PBASE 0x3F000000 #else #define PBASE 0xFE000000 #endif #endif /*_P_BASE_H */
下面是PL011串口的初始化代码。
void uart_init ( void )
{
unsigned int selector;
selector = readl(GPFSEL1); selector &= ~(7<<12);
/* 为GPIO14设置可选项0*/
selector |= 4<<12;
selector &= ~(7<<15);
/* 为GPIO15设置可选项0 */
selector |= 4<<15;
writel(selector, GPFSEL1);
上述代码把 GPIO14 和 GPIO15 设置为可选项0,也就是用作PL011 串口的RXD0 和TXD0引脚。
/*设置gpio14/15为下拉状态*/ selector = readl(GPIO_PUP_PDN_CNTRL_REG0); selector |= (0x2 << 30) | (0x2 << 28); writel(selector, GPIO_PUP_PDN_CNTRL_REG0);
通常GPIO引脚有3个状态——上拉(pull-up)、下拉(pull-down)以及连接(connect)。连接状态指的是既不上拉也不下拉,仅仅连接。上述代码已把GPIO14和GPIO15设置为连接状态。
下列代码用来初始化PL011串口。
/* 暂时关闭串口 */ writel(0, U_CR_REG); /* 设置波特率 */ writel(26, U_IBRD_REG); writel(3, U_FBRD_REG); /* 使能FIFO设备 */ writel((1<<4) | (3<<5), U_LCRH_REG); /* 屏蔽中断 */ writel(0, U_IMSC_REG); /* 使能串口,打开收发功能 */ writel(1 | (1<<8) | (1<<9), U_CR_REG);
接下来,实现如下几个函数以收发字符串。
void uart_send(char c)
{
while (readl(U_FR_REG) & (1<<5))
;
writel(c, U_DATA_REG);
}
char uart_recv(void)
{
while (readl(U_FR_REG) & (1<<4))
;
return(readl(U_DATA_REG) & 0xFF);
}
uart_send()和uart_recv()函数分别用于在while循环中判断是否有数据需要发送和接收,这里只需要判断U_FR_REG寄存器的相应位即可。
接下来,编写Makefile文件。
board ?= rpi3
ARMGNU ?= aarch64-linux-gnu
COPS += -DCONFIG_BOARD_PI4B
QEMU_FLAGS += -machine raspi4
COPS += -g -Wall -nostdlib -nostdinc -Iinclude
ASMOPS = -g -Iinclude
BUILD_DIR = build
SRC_DIR = src
all : benos.bin
clean :
rm -rf $(BUILD_DIR) *.bin
$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
mkdir -p $(@D)
$(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@
$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
$(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@
C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)
DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)
benos.bin: $(SRC_DIR)/linker.ld $(OBJ_FILES)
$(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/benos.elf $(OBJ_FILES)
$(ARMGNU)-objcopy $(BUILD_DIR)/benos.elf -O binary benos.bin
QEMU_FLAGS += -nographic
run:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin
debug:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin -S -s
ARMGNU用来指定编译器,这里使用aarch64-linux-gnu-gcc。
COPS和ASMOPS用来在编译C语言与汇编语言时指定编译选项。
●-g:表示编译时加入调试符号表等信息。
●-Wall:表示打开所有警告信息。
●-nostdlib:表示不连接系统的标准启动文件和标准库文件,只把指定的文件传递给连接器。这个选项常用于编译内核、bootloader等程序,它们不需要标准启动文件和标准库文件。
●-nostdinc:表示不包含C语言的标准库的头文件。
上述文件最终会被编译、链接成名为benos.elf的.elf文件,这个.elf文件包含了调试信息,最后使用objcopy命令把elf文件转换为可执行的二进制文件。