前言
本系列文中很多内容都来自老师的指导书,我只是在指导书的基础上添加了一些我碰到的问题和知识。
初次实验 我使用的平台是Oracle VirtualBox,Ubuntu的版本是20.04,内存要设置大一点(我设的四十,完成计算机系统和操作系统的实验后用了三十多GB)
再次实验我用的VMware,Ubuntu的版本也是是20.04。(所以有的图机器名不一样)
刚开始因为VMware一直无法连网我差点放弃,后来找到了解决方法:按win+r打开services.msc,找到VMware NAT Service和VMware DHCP Service将其启用。
第一次做这个实验的时候是出现了依赖库的问题的,但重装虚拟机后没有出现,所以我没有把相关问题的解决方法贴上来。如果同学们有出现其他的问题,可以自行搜索解决或者查看课程讨论区有没有类似的问题。
PS:下一届及以后的同学们实验可能会有些变化,不过内容应该差不多,还是可以参考一下的
PPS:我之前很奇怪为什么要把.h文件和.c文件放一起,所以我自己实验的时候就把所有.h文件放在include目录下了。后来问了老师才知道是有的头文件是内部的,所以才放在bsp目录下。但我懒得改了你们还是按老师的指导书来。
安装增强功能
增强功能能实现主机和虚拟机之间的复制粘贴,强烈建议安装,非常方便使用。
安装工具链
安装交叉编译工具链 (aarch64)
AArch64 是随 ARMv8 ISA 一起引入的 64 位架构,用于执行 A64 指令的计算机。而且在 AArch64 状态下执行的代码只能使用 A64 指令集。,而不能执行 A32 或 T32 指令。但是,与 AArch32 中不同,在64位状态下,指令可以访问 64 位和 32 位寄存器。
aarch64-linux-gnu-gcc 是一个交叉编译工具链,可以在其他架构的系统中,编译安装 64 位 arm 架构的程序。常用在嵌入式代码的移植中。aarch64-linux-gnu-gcc 是由 Linaro 公司基于 GCC 推出的的 ARM 交叉编译工具。可用于交叉编译 ARMv8 64 位目标中的裸机程序、u-boot、Linux kernel、filesystem 和 App 应用程序。aarch64-linux-gnu-gcc 交叉编译器必须安装在 64 位主机上,才能编译目标代码。
(这是我从别的博主的文章里摘的介绍)
下载工具链,以下载工具链版本为11.2,宿主机为x86 64位 Linux机器为例
wget https://developer.arm.com/-/media/Files/downloads/gnu/11.2-2022.02/binrel/gcc-arm-11.2-2022.02-x86_64-aarch64-none-elf.tar.xz
解压工具链
tar -xf gcc-arm-(按Tab键补全)
重命名工具链目录
mv gcc-arm-(按Tab键补全) aarch64-none-elf
老师指导书原话,照做即可。
如果碰到问题先找一下讨论区。
配置环境变量:
建议永久添加环境变量,否则后续实验每次都要配置环境变量。
永久添加环境变量:
1.打开终端,输入
gedit ~/.bashrc
打开文件,然后在文件末尾输入
export PATH="$PATH:/path/to/your/aarch64-none-elf/bin"
/path/to/your/aarch64-none-elf/bin要改成你自己下载的工具链所在的实际目录
更改完成以后,在终端输入
source ~/.bashrc
使配置生效。
查看目录
打开你下载的aarch64-none-elf文件,进入bin文件夹,在这里右键打开终端,然后输入指令:
pwd
就能得到实际目录,将其复制到.bashrc文件里即可。
测试工具链是否安装成功
在终端输入指令
aarch64-none-elf-gcc --version
进行测试。
安装QEMU模拟器
QEMU是一个开源的模拟器和虚拟化器,它支持多种硬件架构,包括AArch64。
根据老师的指导书安装QEMU模拟器。
sudo apt-get update
sudo apt-get install qemu
sudo apt-get install qemu-system
没有出现报错。(这个信息比较多我就没有截全)
安装CMake
sudo apt-get install cmake
没有出现报错。(这个也没截全)
安装完成后使用--version查看版本,如果版本过低可能需要更新
创建裸机(Bare Metal)程序
创建项目
创建指令
创建单级目录:
mkdir 目录名
创建多级目录:
mkdir -p 一级目录/二级目录/……
创建文件:
touch 文件名.后缀
参考这个目录结构进行创建 :
main.c源码:
#include "prt_typedef.h"
#define UART_REG_WRITE(value, addr) (*(volatile U32 *)((uintptr_t)addr) = (U32)value)
S32 main(void){
char out_str[] = "AArch64 Bare Metal";
int length = sizeof(out_str) / sizeof(out_str[0]);
// 逐个输出字符for (int i = 0; i < length - 1; i++) {
UART_REG_WRITE(out_str[i], 0x9000000);
}
}
在老师的指导书内下载prt_typedef.h:
/*
* Copyright (c) 2009-2022 Huawei Technologies Co., Ltd. All rights reserved.
*
* UniProton is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* Create: 2009-12-22
* Description: 定义基本数据类型和数据结构。
*/
#ifndef PRT_TYPEDEF_H
#define PRT_TYPEDEF_H#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif /* __cpluscplus */
#endif /* __cpluscplus */typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;
typedef unsigned long long U64;
typedef signed char S8;
typedef signed short S16;
typedef signed int S32;
typedef signed long long S64;typedef void *VirtAddr;
typedef void *PhyAddr;#ifndef OS_SEC_ALW_INLINE
#define OS_SEC_ALW_INLINE
#endif#ifndef INLINE
#define INLINE static __inline __attribute__((always_inline))
#endif#ifndef OS_EMBED_ASM
#define OS_EMBED_ASM __asm__ __volatile__
#endif/* 参数不加void表示可以传任意个参数 */
typedef void (*OsVoidFunc)(void);#define ALIGN(addr, boundary) (((uintptr_t)(addr) + (boundary) - 1) & ~((uintptr_t)(boundary) - 1))
#define TRUNCATE(addr, size) ((addr) & ~((uintptr_t)(size) - 1))#ifdef YES
#undef YES
#endif
#define YES 1#ifdef NO
#undef NO
#endif
#define NO 0#ifndef FALSE
#define FALSE ((bool)0)
#endif#ifndef TRUE
#define TRUE ((bool)1)
#endif#ifndef NULL
#define NULL ((void *)0)
#endif#define OS_ERROR (U32)(-1)
#define OS_INVALID (-1)#ifndef OS_OK
#define OS_OK 0
#endif#ifndef OS_FAIL
#define OS_FAIL 1
#endif#ifndef U8_INVALID
#define U8_INVALID 0xffU
#endif#ifndef U12_INVALID
#define U12_INVALID 0xfffU
#endif#ifndef U16_INVALID
#define U16_INVALID 0xffffU
#endif#ifndef U32_INVALID
#define U32_INVALID 0xffffffffU
#endif#ifndef U64_INVALID
#define U64_INVALID 0xffffffffffffffffUL
#endif#ifndef U32_MAX
#define U32_MAX 0xFFFFFFFFU
#endif#ifndef S32_MAX
#define S32_MAX 0x7FFFFFFF
#endif#ifndef S32_MIN
#define S32_MIN (-S32_MAX-1)
#endif#ifndef LIKELY
#define LIKELY(x) __builtin_expect(!!(x), 1)
#endif#ifndef UNLIKELY
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#endif#ifdef __cplusplus
#if __cplusplus
}
#endif /* __cpluscplus */
#endif /* __cpluscplus */#endif /* PRT_TYPEDEF_H */
prt_typedef.h内主要是一些宏定义。
start.S源码:
.global OsEnterMain
.extern __os_sys_sp_end
/* 声明 OsEnterMain 和 __os_sys_sp_end 是外部定义的符号,其中 OsEnterMain 在 prt_reset_vector.S 中定义, __os_sys_sp_end 在链接脚本 aarch64-qemu.ld 定义。 */
.type start, function.section .text.bspinit, "ax"
/* 声明这部分代码段(section)的名字是 .text.bspinit */
.balign 4
.global OsElxState.type OsElxState, @function
OsElxState:
/* 系统入口,系统一启动就会执行从这里开始的代码 */
MRS x6, CurrentEL // 把系统寄存器 CurrentEL 的值读入到通用寄存器 x6 中
/* CurrentEL 是 AArch64 架构的系统寄存器。这些寄存器不能直接操作,需要通过 MRS 指令(把系统寄存器的值读入到通用寄存器)或 MSR 指令(把通用寄存器的值写入到系统寄存器)借助通用寄存器来访问。 */
MOV x2, #0x4 // CurrentEL EL1: bits [3:2] = 0b01
CMP w6, w2
BEQ Start // 若 CurrentEl 为 EL1 级别,跳转到 Start 处执行,否则死循环。/* 上面四条指令检测当前CPU的 Exception Level 是否为 EL1 (将在 实验四 异常处理 部分详细解释),如果是 EL1 则通过BEQ Start 跳转到标号Start处开始执行,否则执行接下来的指令,接下来的两条指令一起构成死循环。
OsEl2Entry:B OsEl2Entry
Start:LDR x1, =__os_sys_sp_end // 符号在ld文件中定义
BIC sp, x1, #0xf // 设置栈指针
/* 用链接文件定义的地址初始化栈指针 sp,然后 L24 跳转到 prt_reset_vector.S 的 OsEnterMain 处开始执行。 */
B OsEnterMain
OsEnterReset:B OsEnterReset
prt_reset_vector.S 源码:
DAIF_MASK = 0x1C0
/* 定义了一个名为DAIF_MASK的常量,其值为0x1C0。这个值用于设置系统寄存器DAIF(Debug, Abort, IRQ, FIQ)来禁用SError Abort、IRQ和FIQ中断。在AArch64架构中,DAIF寄存器的每一位都控制一种中断或异常的屏蔽状态。在这里,位7(SError)、位6(IRQ)和位5(FIQ)被设置为1(禁用状态),而位8(Debug)保持为0(通常是由硬件控制的,不能通过软件直接修改)。 */
.global OsVectorTable.global OsEnterMain
.section .text.startup, "ax"/* 定义了一个名为
.text.startup
的代码段,并带有属性"ax"
(可执行和可分配)。 */OsEnterMain:
BL main
/* 通过 BL main 跳转到main.c中的main函数执行,main函数执行完后会回到这里继续执行下一条指令。 */
MOV x2, DAIF_MASK // bits [9:6] disable SError Abort, IRQ, FIQMSR DAIF, x2 // 把通用寄存器 x2 的值写入系统寄存器 DAIF 中
/* MOV指令将DAIF_MASK的值(0x1C0)移动到通用寄存器x2中。然后,MSR(Move to System Register)指令将x2的值写入系统寄存器DAIF`中,从而禁用SError Abort、IRQ和FIQ中断。因为中断处理尚未设置,详细参见 实验四 异常处理 */
EXITLOOP:B EXITLOOP
/* 定义了一个标签
EXITLOOP
,并使用B
(Branch)指令跳转到它自身,形成一个无限循环。在嵌入式系统中,这通常用于在初始化完成后“挂起”处理器,等待外部事件或中断。*//* 在上面两个汇编文件中出现了两种不同的跳转指令 B 和 BL,其中 B 跳转后不返回调用位置, BL 跳转后执行完函数后会回到调用位置继续执行。*/
在老师的指导书内下载完整的aarch64-qemu.ld 脚本:
aarch64-qemu.ld 脚本是为在QEMU上模拟的AArch64环境中运行的程序或系统映像提供自定义内存布局和配置的链接器脚本。
aarch64-qemu.ld中需要了解的部分:
ENTRY(__text_start) /* 指明系统入口为 __text_start */
_stack_size = 0x10000; /* 指定栈的大小为64kb */_heap_size = 0x10000; /* 指定堆的大小为64kb */
MEMORY{
IMU_SRAM (rwx) : ORIGIN = 0x40000000, LENGTH = 0x800000
/* 内存区域 ,rwx可读可写可执行,起始地址(ORIGIN)为 0x40000000,长度为 0x800000(即8MB)。*/
MMU_MEM (rwx) : ORIGIN = 0x40800000, LENGTH = 0x800000
/* 内存区域,起始地址为0x40800000 */
}
SECTIONS{
text_start = .;
/* 定义一个名为text_start的符号,并将其设置为当前位置(
.
表示当前位置)。通常用于在链接脚本的后续部分引用该位置。*/.start_bspinit :
{
__text_start = .;
/* 将 __text_start 符号设置为.start_bspinit 段的起始地址。 */
KEEP(*(.text.bspinit))
/* 确保链接器包含所有名为 .text.bspinit 的输入段。KEEP 关键字确保这些段不会被优化掉。 */
} > IMU_SRAM /* 指示 .start_bspinit 段应被放置在 IMU_SRAM 内存区域中。*/
... ... .../* 定义堆和栈在内存中的位置和大小 */
/* NOLOAD 表示这些段在最终的可执行文件中不占用空间,但链接器会计算它们的大小和位置。这通常用于在程序运行时动态地分配这些区域。*/
.heap (NOLOAD) :{
. = ALIGN(8);
/* 确保地址对齐到8字节边界,这是ARM处理器架构对栈和堆地址的要求。 */
PROVIDE (__HEAP_INIT = .);
/* 定义了一个符号,该符号的值是当前位置(.)。这允许程序在运行时知道堆和栈的起始和结束地址。 */
. = . + _heap_size;
/* 增加当前位置的值以创建堆和栈的空间 */
. = ALIGN(8);
PROVIDE (__HEAP_END = .);
} > IMU_SRAM
.stack (NOLOAD) :{
. = ALIGN(8);
PROVIDE (__os_sys_sp_start = .);
. = . + _stack_size; /* 栈空间 */
. = ALIGN(8);
PROVIDE (__os_sys_sp_end = .);
} > IMU_SRAM
end = .;
... ... ...}
工程构建
使用CMake构建系统,可以先学习一下CMake,对后续实验很有帮助
CMakeLists.txt
src/下的CMakeLists.txt
注意修改 set(TOOLCHAIN_PATH “/usr/local/aarch64-none-elf”) 中的目录
cmake_minimum_required(VERSION 3.12)#指定了CMake的最低版本要求为3.12
set(CMAKE_SYSTEM_NAME "Generic") # 目标系统(baremental): cmake/tool_chain/uniproton_tool_chain_gcc_arm64.cmake 写的是Linuxset(CMAKE_SYSTEM_PROCESSOR "aarch64") # 目标系统CPU
set(TOOLCHAIN_PATH "/usr/local/aarch64-none-elf") # 修改为交叉工具链实际所在目录 build.py config.xml中定义# 分别设置C编译器、C++编译器、汇编编译器和链接器的路径:
set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-g++)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-gcc)
set(CMAKE_LINKER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-ld)
# CC_OPTION, AS_OPTION, LD_OPTION 分别定义了C编译器、汇编器和链接器的选项。这些选项通常用于优化、调试、指定特定的编译行为等set(CC_OPTION "-Os -Wformat-signedness -Wl,--build-id=none -fno-PIE -fno-PIE --specs=nosys.specs -fno-builtin -fno-dwarf2-cfi-asm -fomit-frame-pointer -fzero-initialized-in-bss -fdollars-in-identifiers -ffunction-sections -fdata-sections -fno-aggressive-loop-optimizations -fno-optimize-strlen -fno-schedule-insns -fno-inline-small-functions -fno-inline-functions-called-once -fno-strict-aliasing -finline-limit=20 -mlittle-endian -nostartfiles -funwind-tables")
set(AS_OPTION "-Os -Wformat-signedness -Wl,--build-id=none -fno-PIE -fno-PIE --specs=nosys.specs -fno-builtin -fno-dwarf2-cfi-asm -fomit-frame-pointer -fzero-initialized-in-bss -fdollars-in-identifiers -ffunction-sections -fdata-sections -fno-aggressive-loop-optimizations -fno-optimize-strlen -fno-schedule-insns -fno-inline-small-functions -fno-inline-functions-called-once -fno-strict-aliasing -finline-limit=20 -mlittle-endian -nostartfiles -funwind-tables")
set(LD_OPTION " ")
#将选项应用到CMake变量
set(CMAKE_C_FLAGS "${CC_OPTION} ") # 设置了C文件的编译标志。
set(CMAKE_ASM_FLAGS "${AS_OPTION} ") # 设置了汇编文件的编译标志。
set(CMAKE_LINK_FLAGS "${LD_OPTION} -T ${CMAKE_CURRENT_SOURCE_DIR}/aarch64-qemu.ld") # 指定链接脚本
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPTION} -T ${CMAKE_CURRENT_SOURCE_DIR}/aarch64-qemu.ld") # 指定链接脚本
set (CMAKE_C_LINK_FLAGS " ")
set (CMAKE_CXX_LINK_FLAGS " ")
set(HOME_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(APP "miniEuler") # APP变量,后面会用到 ${APP}project(${APP} LANGUAGES C ASM) # 工程名及所用语言
set(CMAKE_BUILD_TYPE Debug) # 生成 Debug 版本
# include 目录 包含的头文件目录,不是很懂为什么这里要把bsp放进去,并且后面的实验也有把头文件放在bsp目录下的,如果后面的实验完全按照老师的路径放,不要改这里,但如果像我一样把所有的.h文件都放在include目录下,可以把第二行删掉(我觉得这样构建没那么复杂)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/bsp
)
add_subdirectory(bsp) # 包含子文件夹的内容
list(APPEND OBJS $<TARGET_OBJECTS:bsp>)add_executable(${APP} main.c ${OBJS}) # 可执行文件
src/bsp/下的CMakeLists.txt
set(SRCS start.S prt_reset_vector.S )
add_library(bsp OBJECT ${SRCS}) # OBJECT类型只编译生成.o目标文件,但不实际链接成库
编译运行
编译
在lab1目录下新建目录build,进入build目录,打开终端执行指令:
cmake ../src
cmake --build .
注意第二个指令最后有一个“ . ”。
运行
在目录lab1下执行
qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler -s
qemu-system-aarch64参数解释:
- -machine virt:指定了模拟的机器类型为 virt,这是一个 QEMU 为 ARM 架构提供的通用虚拟机器类型。
- ,gic-version=2:附加参数,用于指定 Generic Interrupt Controller (GIC) 的版本为 2。GIC 是 ARM 架构中的一个组件,用于处理中断。
- -m 1024M:指定了模拟机的内存大小为 1024MB。
- -cpu cortex-a53:指定了模拟机将模拟的 CPU 类型为 ARM Cortex-A53。Cortex-A53 是一个低功耗的 ARMv8-A 架构的 CPU。
- -nographic:告诉 QEMU 不要启动图形界面。也就是说,模拟机将不会显示任何图形或窗口,而是完全在命令行环境中运行。
- -kernel build/miniEuler:指定了模拟机启动时将要加载的内核镜像文件,位于 build/miniEuler。
- -s:启用 GDB 服务器模式。 QEMU 将在 1234 端口上启动一个 GDB 服务器,允许你使用 GDB(GNU 调试器)来调试模拟机中的代码。
退出qemu:同时按下左ctrl键和a键,然后再输入x
调试支持
GDB简单调试
启动调试服务器
通过QEMU运行程序并启动调试服务器,默认端口1234
qemu-system-aarch64 -machine virt,gic-version=2 -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler -s -S
与上面运行程序的差别在于命令中加入了 -S 参数。
- -S:在启动后冻结 CPU。通常与 -s 一起使用,以便在启动后立即开始调试,而不是让模拟机立即开始执行代码。
启动调试服务器后界面如下:(像是卡住了,但这是正常的)
不要关闭这个终端,另外再打开一个终端:
启动调试客户端
aarch64-none-elf-gdb build/miniEuler
设置调试参数,开始调试
(gdb) target remote localhost:1234
使用这个指令连接到 QEMU 的 GDB 服务器。
接下来就可以用gdb的命令开始调试了。
将调试集成到vscode
这个建议大家学习一下vscode的使用方法再进行操作
步骤:
- 文件->打开文件夹->选中lab1->打开
- 打开main.c文件:
- 终端->配置任务->gcc生成活动文件
- 运行->添加配置->将老师提供的launch.json文件复制进来,注意修改选中行地址为自己的交叉调试器gdb对应位置
- 启动qemu调试客户端
- 运行与调试->开始调试(绿色三角形)
- 开始调试:在调试控制台输入-exec +gdb指令
自动化脚本
在lab1目录下新建 makeMiniEuler.sh 和 runMiniEuler.sh 两个文件
makeMiniEuler.sh
# sh makeMiniEuler.sh 不打印编译命令
# sh makeMiniEuler.sh -v 打印编译命令等详细信息
rm -rf build/*
mkdir build
cd build
cmake ../src
cmake --build . $1
runMiniEuler.sh
sh runMiniEuler.sh 直接运行
sh runMiniEuler.sh -S 启动后在入口处暂停等待调试
echo qemu-system-aarch64 -machine virt,gic-version=2 -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler -s $1
qemu-system-aarch64 -machine virt,gic-version=2 -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler -s $1
打开终端执行命令,即可编译运行:
sh makeMiniEuler.sh
sh runMiniEuler.sh
PS::如果没有把目录build删掉,就会多出如下一行,不过这并不影响结果。
作业
学习如何调试项目
多多学习使用,熟悉gdb常用调试指令,然后根据需要进行调试。多调试几次就会了。