本书中大部分的实验代码是基于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文件转换为可执行的二进制文件。