首页 > 编程语言 >【翻译】RISC-V裸机编程指南(Bare metal programming with RISC-V guide)

【翻译】RISC-V裸机编程指南(Bare metal programming with RISC-V guide)

时间:2024-04-25 23:35:26浏览次数:35  
标签:riscv64 bios programming RISC 裸机 a0 QEMU hello

RISC-V裸机编程指南(Bare metal programming with RISC-V guide)

作者: Follow @popovicu94

原文链接: https://popovicu.com/posts/bare-metal-programming-risc-v/

今天,我们将探讨如何为RISC-V架构的机器编写一个裸机程序。为了确保可复现,目标平台选择为QEMU riscv64 virt虚拟机。

我们将简要介绍RISC-V机器启动过程的初始阶段,并且说明你可以在何处插入自己的代码来对裸机进行编程!

在本文结束时,我们将为RISC-V机器编写一个裸机程序,该程序能够在不依赖操作系统和其它库函数的情况下,向用户发送字符串'hello'。

机器启动以及运行初始化软件(Machine bootup and running the initial software)

总体概念(General concepts)

如果您熟悉计算机启动过程,您可以选择跳过这部分内容。

当一台真实的计算机开机时,硬件首先执行健康检查,然后将要运行的第一条指令加载到内存中。一旦指令加载完毕,处理器核心初始化其寄存器,其中程序计数器会指向第一条指令所在的地址。从那一刻起,软件就可以开始运行了。

在像小型微控制器这样的简单设备中,初始化软件只是一块单一的二进制指令块,处理器接下来将只执行这些指令。而在像笔记本电脑或手机这样的更复杂的设备中,启动过程有更多的阶段。

在这些更复杂的设备中,首先执行的程序是BIOS(基本输入输出系统),BIOS通常会完成硬件自检、设备初始化等一系列启动步骤,最后会将引导程序载入内存,并将控制权交给引导程序。引导程序通常体积小,易于加载到内存中,处理器可以轻松运行其代码。引导程序会将操作系统内核加载到内存中(不过,实现引导程序本身就是一门学问)。

每台机器都有自己加载初始软件的独特方式。例如,BIOS可以被存储在一块独立的存储芯片上,当机器通电启动时,存储芯片中的内容会被直接填充到内存中的某个固定地址处,然后处理器就会从那个地址处开始执行指令。

QEMU 启动(QEMU bootup)

即使riscv64 virt虚拟机是虚拟化的,仍然有其自己的启动顺序。它会经历多个阶段,目前我们并不会全部深入探究。敬请期待后续详细介绍这些细节的文章。

显然,riscv64 virt虚拟机无法从物理意义上的芯片中读取初始化软件(它是虚拟的),所以是QEMU以某种方式模拟了这一点。你可能在之前的QEMU示例中见过-bios标志,现在你可能对其作用有了深刻的理解。如果你猜测这正是在虚拟RISC-V核心启动时执行的第一批指令,那么你的答案几乎是正确的。

零阶段引导加载器(The Zero Stage Bootloader(ZSBL))

当你启动这款虚拟机时,QEMU会在0x1000地址处填充一些指令,并将程序计数器的值设置为该地址。这相当于真实机器在主板上有一些硬编码的ROM固件(隐藏在某个芯片中),并在启动时将固件内容复制到RAM中。你无法控制这些指令,即它们不属于你的软件镜像,通常情况下,我也找不到你需要覆盖这些指令的理由,而且实际上它们对于更复杂的设置非常有用(我保证我们将在后续文章中详细讲解)。对于好奇的人来说,这些少量指令就是零阶段引导加载器(ZSBL)。ZSBL会设置几个寄存器(现在你基本可以忽略这些寄存器设置),然后跳转到地址0x80000000,真正有意义的操作便从此处开始!

QEMU-bios标志(QEMU -bios flag)

# 启动qemu示例
qemu-system-riscv64 -machine virt -bios hello

在启动QEMU时,如果通过-bios标志指定了自己的程序,虚拟机启动后会将指定的程序加载到0x80000000 处,故0x80000000是QEMU首次执行用户提供指令的地方。如果没有提供任何自定义程序,QEMU将会使用默认设置并加载一个名为OpenSBI的软件。本博客的下一篇文章将详细介绍RISC-V架构中SBI的概念以及OpenSBI究竟是什么。值得注意的是,RISC-V上的SBI并不完全等同于BIOS,但二者功能相似。我的个人猜测是,QEMU的作者只是重新利用了在其他架构(如x86)上表示BIOS的可用标志。无论如何,请记住SBI在功能上与BIOS非常相似,更重要的是,它是可以定制的。

-bios 标志的参数是一个ELF文件,ELF文件中包含指令,同时也可能包含一些数据,这些数据以段(section)为单位进行组织。 ELF是Linux的标准二进制格式,而ELF文件格式的详细信息已经超出了本文的范畴,但是这里应有的基本认识是ELF其实就是一个键值映射,其中键是段(section)的起始地址,值是需要被加载到该内存地址中的一系列字节。因此,我们提供给-bios标志的ELF文件应该从0x80000000开始填充内存(QEMU默认的OpenSBI image就是这样做的)。

关于-kernel标志的说明(A note on the -kernel flag)

如果你之前用QEMU启动过操作系统(例如Linux),你可能使用过-kernel标志。它基本上与-bios标志是相同的:你可以传递给它一个ELF镜像,该镜像覆盖某些其他内存区域,从概念上讲,它将直接将字节转储到内存中。我们今天不会使用这个标志,我们将在接下来的文章中介绍它的使用方法。

在启动过程中,ELF文件如何被使用?(How can ELF files be used during the bootup?)

从概念上看,ELF文件只是填充内存的一种方式,但这个过程确实不简单,你不太可能一个下午就写出一个解析器来。有些细心的读者可能会想,我们的机器是如何知道从ELF文件中解析出映射到某个地址0x12345678的内容,并用这些东西去填充内存的呢?这的确是一个很好的观察。在我们这个例子中,我们使用的是虚拟机,基本上是在模拟一个从概念上就拥有智能数字电路或者复杂的初始软件引导程序的机器,这种程序在开机时就预装在机器的内存中。当然,真实的机器并不会这样操作。真实的机器在开机时加载的软件是作为一个平坦的二进制大块存储在机器存储设备中的,开机时这些数据就直接被转储到内存里,实际上并没有进行任何解析。但是,由于我们现在处理的是虚拟机,所以我们可以尽情发挥,我们并不受到制造执行此类操作的硬件复杂性的约束。

为RISC-V编写一个自定义的"BIOS"(Writing a custom “BIOS” for RISC-V)

我们已经知道了0x80000000是机器执行的第一条用户指定指令所在的位置。我提到这一点作为一个既定事实,如果你想了解为什么是这样的,可以从这里了解更多细节。简而言之,DRAM在地址空间中被映射至从0x80000000开始(如果你不清楚这是什么意思,不必担心,这在本文剩余部分不太重要)。

让我们首先创建一个ELF文件,该文件将从地址0x80000000开始放置一些指令,用于向用户显示消息“hello”!

通过 UART 与用户交互(Interacting with the user via UART)

过去从事过嵌入式系统编程的人肯定对UART(通用异步收发传输器)这一概念非常熟悉。UART是一种极其简单的设备,用于最基本的输入/输出:它有一根用于输入(接收,称为RX)的线和一根用于输出(发送,称为TX)的线,每次只有一位数据在电线上传输。如果你要将两台设备通过UART连接起来进行通信,那么一台设备的TX端口就是另一台设备的RX端口,反之亦然。如果您正在阅读本文且之前未曾接触过UART,我强烈建议您至少购买一个最便宜的Arduino,并让它通过USB-to-UART线缆与您的计算机进行通信。这里的概念与我们正在做的事情完全相同,但您是在真实环境中操作,因此会更有意义,因为我们当前所讨论的场景完全是虚拟化的。

QEMU在虚拟机中模拟了一个UART设备,我们的软件可以访问它。当你打开QEMU的串行端口(UART)时,大体会发生如下情况是:当你按下键盘上的某个键时,该键的编码会从主机的TX端口发送到VM的RX端口;而当VM在其TX端口输出数据时,这些数据会被图形化地呈现在终端中(这样你就无需解析从模拟板上接收到的电信号),例如,如果VM发送出代表数字65的8位数据,QEMU会将其渲染为字符'a',因为这是该字符的ASCII码。

QEMU将UART映射到地址0x10000000处(你可以在其源代码中了解到这一信息),在此虚拟化的设备是NS16550A,具体细节在这里并不重要。对于本文而言,如果你在程序中向地址0x10000000发送一个8位的数据,该数据将通过虚拟UART设备的TX线发送出去。实际上,这意味着如果你打开QEMU的串行端口,你写入到0x10000000的字符将会在你的控制台中显示。

将所有内容编码在一起!(Coding it all together!)

基于上述所有知识,我们现在可以编写代码了。我们将要构建的ELF文件将在0x80000000处放置一些指令,依次将字符'h'、'e'、'l'、'l'和'o'打印到地址0x10000000。最后,代码应该无限循环(以防止QEMU因任何不明原因崩溃,同时我们可以查看输出)。

# 指示_start为全局符号
.global _start
# 标识代码段
.section .text.bios

_start: 
# 把一个立即数移动到a1寄存器
# 0x10000000是串口的映射地址,在该地址写入8个字节数据将会被发送到串口
li a1, 0x10000000

# addi: 立即数加法指令, a0: 通用寄存器 x0: 0寄存器,其值总是0
# 1. 把0x68移动到x0寄存器,然后把x0寄存器的值移动到a0寄存器
addi a0, x0, 0x68
# sb store byte: 从a0寄存器移动一个字节的内容到a1寄存器中存储的内存地址中
# 即将0x68移动到内存0x10000000处
sb a0, (a1) # 'h'

addi a0, x0, 0x65
sb a0, (a1) # 'e'

addi a0, x0, 0x6C
sb a0, (a1) # 'l'

addi a0, x0, 0x6C
sb a0, (a1) # 'l'

addi a0, x0, 0x6F
sb a0, (a1) # 'o'

loop: 
j loop

您可以将此文件保存为hello.s。让我们将此文件汇编为机器代码。对于我来说(很可能也包括你),我使用的是跨平台工具链,这意味着我的开发平台与目标平台不同,具体来说,我正在x86机器上开发此软件,并为其构建riscv64版本。

为了汇编该文件,运行下方的命令:

# riscv64-linux-gnu-as在不同的操作系统上可能不同,这取决于具体安装的riscv工具链
riscv64-linux-gnu-as -march=rv64i -mabi=lp64 -o hello.o -c hello.s

具体的命令可能会根据您所使用的riscv64汇编器有所不同,以下是我通过Debian系统包管理器获取到的工具。我留给读者自行获取适用于构建riscv64软件的正确工具链,通常只需从互联网上获取相应的软件包即可。

现在,代码仅完成了汇编,意味着我们已将汇编文件转换为机器码格式,但该二进制文件尚未准备好作为我们的"BIOS"使用。我们需要使用链接器,并通过链接器脚本来控制其行为,以确保生成的指令按照预期布局在0x80000000处。下面来编写链接器脚本。

MEMORY {
    /* 
     1. 定义一块内存区域,权限为读写执行 
     2. 区域起始地址为0x80000000
     3. 大小为128K
    */
    dram_space (rwx) : ORIGIN = 0x80000000, LENGTH = 128
}

SECTIONS {
    /*
     定义text段的布局规则, 把hello.o中.text.bios段的内容放到.text段中
     将text段中的内容定位到dram_space区域中,即0x80000000中
     这段链接脚本的作用是配置链接器将程序的代码段(.text)定位到一个具有读写执行权限的、从0x80000000开始的128KB DRAM区域中。
    */
    .text : {
        hello.o(.text.bios)
    } > dram_space
}

将上述内容保存到hello.ld文件中,然后运行下方的命令,会生成最终的可执行文件。

# riscv64-linux-gnu-ld在不同的操作系统上可能不同,这取决于具体安装的riscv工具链
riscv64-linux-gnu-ld -T hello.ld --no-dynamic-linker -m elf64lriscv -static -nostdlib -s -o hello hello.o

我们不会详细介绍这些内容的具体含义,简而言之,我们现在有了将这些指令精确放置到所需位置的方法。接下来,我们用objdump来验证这一点。

riscv64-linux-gnu-objdump -D hello
Disassembly of section .text:

0000000080000000 <.text>:
    80000000:	06800513          	li	a0,104
    80000004:	100005b7          	lui	a1,0x10000
    80000008:	00a58023          	sb	a0,0(a1) # 0x10000000
    8000000c:	06500513          	li	a0,101
    80000010:	00a58023          	sb	a0,0(a1)
    80000014:	06c00513          	li	a0,108
    80000018:	00a58023          	sb	a0,0(a1)
    8000001c:	06c00513          	li	a0,108
    80000020:	00a58023          	sb	a0,0(a1)
    80000024:	06f00513          	li	a0,111
    80000028:	00a58023          	sb	a0,0(a1)
    8000002c:	0000006f          	j	0x8000002c

在QEMU中运行这个"fake BIOS"(Running the “fake BIOS” on QEMU)

现在可以通过运行以下命令启动QEMU:

qemu-system-riscv64 -machine virt -bios hello

要查看UART上的情况,请点击顶部菜单中的view按钮,然后点击serial0选项。输出应如下所示:

image

image

我只想运行代码(I just want to run the code!)

前往本文的GitHub仓库,运行make命令后,会生成hello文件,即本文创建的"fake BIOS"。然后,你可以运行qemu-system-riscv64 -machine virt -bios hello启动QEMU。

注:

本文所需要使用的工具: qemu-system-riscv64RISC-V Compiler Toolchain

带中文注释的示例程序仓库: https://github.com/wfenfeng/riscv-bare-metal-fake-bios-with-comments

标签:riscv64,bios,programming,RISC,裸机,a0,QEMU,hello
From: https://www.cnblogs.com/fenfeng9/p/18158918

相关文章

  • 2022 China Collegiate Programming Contest (CCPC) Mianyang | 2022 CCPC 绵阳(MAED
    搬运自本人知乎文章。https://zhuanlan.zhihu.com/p/588646549M.Rock-Paper-ScissorsPyramid题目链接Problem-M-Codeforces题意有一个长度为\(n\)的石头剪刀布序列,每个元素是RPS(石头、布、剪刀)中的一个,我们需要用这个序列构造一个三角,三角的底层为这个序列,第\(i(......
  • The 18-th Beihang University Collegiate Programming Contest (BCPC 2023) - Final
    https://codeforces.com/gym/104883A#include<bits/stdc++.h>usingnamespacestd;usingi32=int32_t;usingi64=longlong;usingvi=vector<int>;i32main(){ios::sync_with_stdio(false),cin.tie(nullptr);i64n,sum=0;c......
  • POI2011PRO-Programming Contest
    POI#Year2011#Dinic#网络流#贪心容易想到拆点的费用流做法,但是二分再拆点的时间复杂度是不可接受的考虑因为每个的时间\(r\)是定值,所以不可能出现做题个数差超过\(1\)的情况所以每一轮每个分配一个,用\(Dinic\)在推进一次,知道满流//Author:xiaruizeconstintN=......
  • The 18th Zhejiang Provincial Collegiate Programming Contest
    目录写在前面AMLCIJFGD写在最后写在前面比赛地址:https://codeforces.com/gym/103055。以下按个人难度向排序。唐唐唐唐唐,又是死在数学手上的一次。妈的为什么上个大学这么累好相似A签到。///*By:Luckyblock*/#include<bits/stdc++.h>#defineLLlonglong//=======......
  • The 2023 ICPC Asia Hong Kong Regional Programming Contest (The 1st Universal Cup
    Preface不知道VP什么就继续找往年的区域赛真题来打了这场题挺合我们队口味的,开场2h就开出了5题(徐神110min的时候就过相对最难的C题),而且手上还有3个会写的题最后中间虽然因为F题卡常(CF评测机太慢导致的)浪费了快1h时间,但索性时间剩余很多还是4h下班了后面的题感觉都不太可做,遂光......
  • The 2022 ICPC Asia Hangzhou Regional Programming Contest
    目录写在前面FDCKAGM写在最后写在前面比赛地址:https://codeforces.com/gym/104090。以下按个人难度向排序。最上强度的一集,然而金牌题过了铜牌题没过,唐!去年杭州似在一道树上痛失Au呃呃,vp2022树题过了然而铜牌题没过呃呃F签到。大力模拟。codebydztle:#include<bit......
  • RISCV
    RISCVriscv通用寄存器riscv通用寄存器寄存器调用名字用途x0zero常数0x1ra返回地址x2sp栈指针x3gp全局指针x4tp线程指针x5-x7t0-t2临时存储x8s0/fp保存用寄存器/帧指针(配合栈指针界定一个函数的栈)x9s1保存用寄存器x10-x11a0-a1函数参数/返回值x12-x17a2-a7函数参数x......
  • 2022 China Collegiate Programming Contest (CCPC) Guilin Site
    目录写在前面AMCELGJ写在最后写在前面比赛地址:https://codeforces.com/gym/104008。以下按个人向难度排序。三月初vp,vp完就去打华为软挑了,拖到现在才补题解呃呃。唉华为软挑打得也是一拖,感觉没有活着的价值。A签到。///*By:Luckyblock*/#include<bits/stdc++.h>#d......
  • RISC-V CPU流水线仿真
    RISC-VCPU流水线仿真1.简介RISC-V是一个开源体系结构和指令集标准,源于伯克利。该项目要求您实现一个基于标准的五级管道。您将需要实现指令的子集来自RISC-V规范2.2中指定的RV32I指令集。实施完整的CPU模拟器可以有效地锻炼系统编程能力加深对建筑相关知识的理解。2.项目简介......
  • 【RISC-V 指令集】RISC-V 向量V扩展指令集介绍(八)- 向量整数算术指令
     1.引言以下是《riscv-v-spec-1.0.pdf》文档的关键内容:这是一份关于向量扩展的详细技术文档,内容覆盖了向量指令集的多个关键方面,如向量寄存器状态映射、向量指令格式、向量加载和存储操作、向量内存对齐约束、向量内存一致性模型、向量算术指令格式、向量整数和浮点算术指......