cisen / blog

Time waits for no one.
135 stars 20 forks source link

opensbi #1100

Open cisen opened 2 years ago

cisen commented 2 years ago

总结

RISC-V架构中,存在着定义于操作系统之下的运行环境。这个运行环境不仅将引导启动RISC-V下的操作系统, 还将常驻后台,为操作系统提供一系列二进制接口,以便其获取和操作硬件信息。 RISC-V给出了此类环境和二进制接口的规范,称为“操作系统二进制接口”,即“SBI”。

SBI的实现是在M模式下运行的特定于平台的固件,它将管理S、U等特权上的程序或通用的操作系统。

OpenSBI的简要分析: 20200811161534723

20200811161458440

cisen commented 2 years ago

https://cloud.tencent.com/developer/article/1770529

opensbi下的riscv64裸机系列编程1(串口输出)

1.说明

前面的文章中已经提到了opensbi的作用不仅仅是一个引导作用,还提供了M模式转换到S模式的实现,同时在S-Mode下的内核可以通过这一层访问一些M-Mode的服务。

本文会从最小系统角度出发,利用opensbi的M-Mode的服务在控制台上输出Hello

2.opensbi的编译

opensbi提供了三种引导启动模式

那么这三种模式有什么区别呢?

FW_PAYLOAD

这种模式会直接将Opensbi固件与uboot等绑定在一起。

可以说这种模式是需要bootloader的。

FW_JUMP

这种模式会直接跳转到bootloader去执行。

这个对于qemu的启动模式来说十分的有用。

FW_DYNAMIC

这种模式跳转的时候会传递动态的参数

这里是通过寄存器a2传递了fw_dynamic_info结构体信息。

为了简化模型,目前只通过FW_JUMP方式进行跳转。

下载opensbi的代码

git clone https://github.com/riscv/opensbi.git

进行编译

export CROSS_COMPILE=riscv64-unknown-elf-
make PLATFORM=generic clean
make PLATFORM=generic FW_JUMP_ADDR=0x80200000

注意FW_JUMP_ADDR=0x80200000是指定的跳转地址。当然可以指定固件跳转到其他的地址。

生成fw_jump.elf位于platform/generic/firmware/fw_jump.elf

3.基本环境的准备

3.1 准备qemu

可以到官网下载最新的qemu

https://www.qemu.org

解压后进行安装与编译。

tar xvf qemu-5.2.0.tar.xz
./configure --target-list=riscv64-softmmu
make
sudo make install

3.2 准备交叉编译工具链

可以到官网上下载对应的交叉编译工具链

https://www.sifive.com/software

准备交叉编译工具链

export PATH=$PATH:/opt/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/

4.工程完善

相关的实验代码已经放到仓库

https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/01_startup

工程的目录结构如下:

.
├── build.sh        ## 编译脚本
├── entry.s         ## 入口函数
├── fw_bin   ## 可执行的固件脚本
│   ├── fw_jump.elf         ## opensbi
│   ├── hello.elf   ## 编译完成的固件
│   └── run.sh    ## 直接运行的脚本
├── link.ld      ## 链接文件
├── main.c   ## 主函数
├── readme.md
└── sbi.h   ## sbi调用api

首先是编译脚本

build.sh

目前为了简化工程,暂时没有使用makefile文件。

riscv64-unknown-elf-gcc -nostdlib -c entry.s -o entry.o
riscv64-unknown-elf-gcc -nostdlib -c main.c -o main.o
riscv64-unknown-elf-ld -o fw_bin/hello.elf -Tlink.ld entry.o main.o

编译了entry.smain.c文件,并通过link.ld文件进行链接。

link.ld

链接脚本规定了程序的布局

OUTPUT_ARCH( "riscv" )
OUTPUT_FORMAT("elf64-littleriscv")
ENTRY( _start )
SECTIONS
{
  /* text: test code section */
  . = 0x80200000;
  start = .;

  .text : {
     stext = .;
        *(.text.entry)
        *(.text .text.*)
        . = ALIGN(4K);
        etext = .;
  }

  .data : {
        sdata = .;
        *(.data .data.*)
        edata = .;
  }

  .bss : {
        sbss = .;
        *(.bss .bss.*)
        ebss = .;
  }
  PROVIDE(end = .);
}

整体的链接脚本写在SECTION{ }包含的结构中。

其中*代表通配符,而.则表示当前的地址。当链接脚本需要使用的时候,可将其通过-T进行参数的传递。

entry.s

该文件描述了执行的入口函数。

    .section .text.entry
    .globl _start
_start:
    /* setup stack */
    la    sp, stack_top           # setup stack pointer
    call main
halt:   j     halt                    # enter the infinite loop

loop:
    j loop

    .section .bss.stack
    .align 12
    .global stack_top
stack_top:
    .space 4096 * 4
    .global stack_top

最关键的是两点:

stack_top:
    .space 4096 * 4
    .global stack_top

将栈顶设置,通过call跳转到c语言的main函数。

main.c

#include "sbi.h"
void main()
{
    SBI_PUTCHAR('H');
    SBI_PUTCHAR('e');
    SBI_PUTCHAR('l');
    SBI_PUTCHAR('l');
    SBI_PUTCHAR('o');
    SBI_PUTCHAR('\n');
    while(1) {}
}

这个程序会调用opensbi的函数,此时可以在S-Mode访问M-Mode的串口输出服务。

5.封装的sbi接口

可以通过下面的官方文档来了解其使用。

https://github.com/riscv/riscv-sbi-doc/blob/master/riscv-sbi.adoc

在进行M-Mode服务访问的时候,采用了ECALL进行系统调用。

在系统调用过程中,ecall会使用a0与a7寄存器。其中a7寄存器保留的是系统的调用号,而a0寄存器则保存系统的调用参数。返回值则会保存在a0寄存器中。

需要注意的是在RISCV的设计上,S模式不直接控制时钟中断和软件中断,而是使用ecall指令请求M模式设置定时器或在代理处理器中断。

所以opensbi在提供M-Mode服务的时候,到目前为止,opensbi提供的sbi服务接口有如下的表示:

Function Name | FID | EID | Replacement EID -- | -- | -- | -- sbi_set_timer | 0 | 0x00 | 0x54494D45 sbi_console_putchar | 0 | 0x01 | N/A sbi_console_getchar | 0 | 0x02 | N/A sbi_clear_ipi | 0 | 0x03 | N/A sbi_send_ipi | 0 | 0x04 | 0x735049 sbi_remote_fence_i | 0 | 0x05 | 0x52464E43 sbi_remote_sfence_vma | 0 | 0x06 | 0x52464E43 sbi_remote_sfence_vma_asid | 0 | 0x07 | 0x52464E43 sbi_shutdown | 0 | 0x08 | 0x53525354 RESERVED |   | 0x09-0x0F |  

这里只使用了sbi_console_putchar接口。

接着看看具体的ecall的实现:

#define SBI_ECALL(__num, __a0, __a1, __a2)                           \
({                                                                  \
    register unsigned long a0 asm("a0") = (unsigned long)(__a0);    \
    register unsigned long a1 asm("a1") = (unsigned long)(__a1);    \
    register unsigned long a2 asm("a2") = (unsigned long)(__a2);    \
    register unsigned long a7 asm("a7") = (unsigned long)(__num);   \
    asm volatile("ecall"                                            \
                 : "+r"(a0)                                         \
                 : "r"(a1), "r"(a2), "r"(a7)                        \
                 : "memory");                                       \
    a0;                                                             \
})

根据上述的解释,ecall采用的是内嵌汇编函数。

ecall
 ii a0,101
 li a1,0
 li a2,0
 li a7,1

这个内嵌汇编的展开形式如上面所示,a0a1a2表示传递的参数,a7表示系统调用号。

而根据内嵌汇编的语法,有着如下的格式

asm(assembler template
    : /* output operands */
    : /* input operands */
    : /* clobbered registers list */
);

对于C语言来说,其函数的调用规则是处理器规定的,而编译器可以按照这种规则进行翻译代码。riscv的函数调用规则可以按照下面的文档进行操作。

https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf

而对于main函数中的SBI_PUTCHAR其展开为

#define SBI_CONSOLE_PUTCHAR 1
#define SBI_PUTCHAR(__a0) SBI_ECALL_1(SBI_CONSOLE_PUTCHAR, __a0)
#define SBI_ECALL_1(__num, __a0) SBI_ECALL(__num, __a0, 0, 0)

可以看到通过ecall只传递一个参数。

6.程序运行

fw_bin文件夹下输入./run.sh就可以运行看到效果了。

而这条操作的代码如下:

qemu-system-riscv64 -M  sifive_u -bios fw_jump.elf -kernel hello.elf -nographic

对应的machine是sifive_u。bios是fw_jump.elf

7.printf函数的实现

对于printf函数的使用很容易,但是深入了解其实现机制,发现并不简单,因为可变参数的特性使得其变得复杂起来。

实验代码如下:

https://github.com/bigmagic123/riscv64_opensbi_baremetal/tree/master/02_printf

看一个glibc中的prinf的实现机制。

#include <ansidecl.h>
#include <stdarg.h>
#include <stdio.h>

/* Write formatted output to stdout from the format string FORMAT.  */
/* VARARGS1 */
int printf(const char *format,...)
{
  va_list arg;
  int done;

  va_start(arg, format);
  done = vprintf(format, arg);
  va_end(arg);

  return done;
}

对于上述的定义

int printf(const char *format,...)

format表示固定的参数,...表示可变的参数。

主要的实现过程利用三个函数进行

va_start(p,format) //将指针p移到第一个变量参数
var=va_arg(p,变量类型)//已知变量的情况下,移到下个参数变量
va_end(p)//结束参数使用等价于p=NULL

这里为了实现方便,我直接使用开源的tinyprintf

https://github.com/cjlano/tinyprintf

移植的过程也很容易,在main.c文件中作如下的实现:

#include "sbi.h"
#include "tinyprintf.h"
#define UNUSED(x) (void)(x)
static void stdout_putc(void *unused,char *ch)
{
        SBI_PUTCHAR(ch);
}
void main()
{
    init_printf(0, stdout_putc);

    tfp_printf("hello world\n");
    while(1) {}
}

只需要移植init_printf接口就可以使用tfp_printf进行串口输出了。

结果如下:

8.小结

第一阶段实现了opensbi的启动流程,同时通过系统调用访问串口输出。已经实现了S-Mode下访问M-Mode的初步计划,并且通过串口进行基本的输出过程。随着工程的不断增加,后续会增加makefile工程组织,riscv下的中断处理、以及定时器中断的实现,下篇文章主要介绍这些。

cisen commented 2 years ago

RISC-V64 opensbi启动过程 https://cloud.tencent.com/developer/article/1758282?from=article.detail.1770529 RISC-V64 opensbi启动过程 1.说明 2.环境准备 2.1 交叉编译工具链 2.2 源代码准备 3.riscv架构 gdb调试方法 4.opensbi底层初始化流程 4.1 从qemu的加载执行开始 4.2 opensbi底层初始化 4.2 opensbi设备初始化 4.3 二级boot的跳转 5.小结 1.说明 最近有一些riscv的项目做,虽然以前也用过例如k210之类的riscv架构的芯片,但是都止于能够做一些应用,并未特别关注其芯片的体系架构方面的东西,但是随着接触的芯片架构的种类的逐渐的增加,发现要想使用一款好芯片的,仅仅做上层应用并不能完全发挥出特定架构芯片的全部优势。比如aarch64的el层级和虚拟化的模型,mips的mmu特性,以及sparc的窗口寄存器等等,芯片架构的特点要是能够完全的发挥出来,写起应用起来,那真是觉得很爽的事情。

目前在工作上做一些riscv项目,发现自己的积累的知识不够了,还是需要深入到底层去理解,于是需要疯狂的恶补相关的知识,看文档、读代码、每天就这样深入其中,看的多了,想法也很多,很容易就忘记了,有时也做做笔记,晚上下班后再将资料整理一下,如果觉得有些价值的东西,就编写成文章,分享经验。

学习使用riscv64的芯片的架构,首先可以了解学习opensbi,作为芯片启动的Bios,其作用不言而喻。工欲善其事,必先利其器。一个良好高效的开发环境将会使得分析代码变得得心应手。本文在Ubuntu18.04环境下进行测试,在riscv64的qemu上进行gdb的单步调试,主要分析的阶段是qemu启动后,执行到opensbi,直到启动uboot的阶段。

opensbi是研究和学习riscv底层的一个比较优秀的项目,代码量小,质量也很高,很值得推荐的一个开源项目。

关于opensbi与qemu的环境搭建,我前面的文章中已经提及,这里就不赘述了。

riscv64 qemu上进行Linux环境搭建与开发记录

2.环境准备 2.1 交叉编译工具链 如果按照之前的文章下载的Linux版本的交叉编译工具链是不带有gdb工具,所以可以下载一个bare/rtos版本的gcc。建议下载sifive的riscv的交叉编译工具链

https://www.sifive.com/software 也可以到网盘下载:

https://pan.baidu.com/s/1_C-cFBD3ADVjVFm94bYzNw 提取码: v38x 下载完成后解压至自定义的文件夹中即可。

2.2 源代码准备 1.qemu最新版

2.opensbi

3.uboot

这些都可以参考文章:

riscv64 qemu上进行Linux环境搭建与开发记录

3.riscv架构 gdb调试方法 首先需要编译安装完成qemu-system-riscv64。

编译uboot,进入uboot:

make CROSS_COMPILE=riscv64-linux- qemu-riscv64_smode_defconfig make CROSS_COMPILE=riscv64-linux- -j4 可见在uboot目录生成u-boot.bin文件。

编译opensbi,进入opensbi:

git clone https://github.com/riscv/opensbi.git export CROSS_COMPILE=riscv64-linux- make PLATFORM=generic FW_PAYLOAD_PATH=/u-boot.bin 可以生成build/platform/generic/firmware/fw_payload.elf文件。

在控制台输入

../riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/riscv64-unknown-elf-gdb build/platform/generic/firmware/fw_payload.elf -s -S 即可进入调试模式。其中需要知道的是-s -S,如果不加这两个参数,系统会直接运行起来。可作为环境搭建是否成功的判断依据。

当进入调试模式后,当前代码会hold住,可以ctrl+t另外开一个窗口,输入

../riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/riscv64-unknown-elf-gdb build/platform/generic/firmware/fw_payload.elf 然后输入

(gdb) target remote localhost:1234 如下图所示:

gdb的命令很多,这里就不展开进行细致的描述了,这里只演示一些基本的操作。

首先可以看到当程序加载完成后,已经hold等待gdb命令进行操作。

输入list可以列出当前的代码,每次list可以展示10行源代码。

输入si可以进行汇编级别的单步调试。

直接回车是继续执行上一条执行的命令。

继续向下跳转可以看到函数的入口和函数的名称。

输入info all-registers可以看到riscv所有寄存器的值的状态,这里刚到入口处寄存器怎么会有初值呢?这里就是很关键的问题,后面再分析代码的时候,会详细分析这些问题。另外可以分析得出当前的函数的入口0x80000000。

另外经常使用的就是打断点

b 10 表示打断点在第10行。

b main 表示断点在main函数处。

输入c可以让程序连续运行,直到遇到断点才停下来。

这些功能在跟踪代码的运行流程的时候比较实用,gdb还有许多功能,这里就不介绍了。

4.opensbi底层初始化流程 上面做了这么多环境搭建方面的工作,目的就是为了方便的分析opensbi的底层初始化步骤和流程。从而更加深刻的了解riscv的架构和初始化流程。

4.1 从qemu的加载执行开始 首先需要从qemu的源代码开始进行加载分析起,当前qemu-system-riscv64支持下面的开发板:

而在qemu的源代码中也有一些

这里我们就拿virt进行分析。

根据hw/riscv/virt.c来看,首先可以分析得到外设分布的地址。

上述可以得到DRAM的地址空间是从0x80000000处开始的,而大小是我们传递参数时传递进去的。另外也记录了一些外设的布局。

qemu加载程序之前的时候,与具体板子相关的部分,首先进入了hw/riscv/virt.c的virt_machine_init。

1.注册PLIC中断设备

这里需要注意的是VIRT_PLIC,在PLIC core这部分与riscv的中断处理相关。

2.注册系统内存

这部分的内存大小由外部传递

3.创建设备树

qemu也使用fdt创建了设备树,该设备树用于opensbi和uboot,这里的设备树放在qemu分配的内存的尾部。并且会将该参数传递,这就是为什么前面进行gdb调试时,入口处会发现寄存器上有参数。

根据riscv的寄存器的规则

寄存器a0-a7是用于传递函数参数的。另外这个设备树的参数会直接传递到opensbi进行解析和适配。同时uboot也会使用这个设备树。

4.2 opensbi底层初始化 最先进来的是opensbi/firmware/fw_payload.S的_start函数。

1.判断hart id

在riscv模式中会将riscv的core称为hart

2.代码重定位

会判断_load_start与_start是否一致,若不一致,则需要将代码重定位,该项目不用重定位。

3.清除寄存器值

这里会清除sp、gp、tp、t1-t6、s0-s11、a3-a7。注意保存设备数地址的a1、a2不会清除。

5.清除bss段

如果要想c语言执行起来,必须要做的事情有两个,一个是设置sp栈地址,另外就是清除bss段。

6.设置sp栈指针

这里栈的指针的地址也很有意思,设置的bss结尾,由于栈是向上增加的,所以预留栈的空间大小为2000。

7.读取设备树中的设备信息

执行call fw_platform_init函数,前面分析过该函数的原型在opensbi/platform/generic/platform.c。

带四个参数。

unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4) { const char model; void fdt = (void *)arg1; u32 hartid, hart_count = 0; int rc, root_offset, cpus_offset, cpu_offset, len;

    root_offset = fdt_path_offset(fdt, "/");
    if (root_offset < 0)
            goto fail;

    fw_platform_lookup_special(fdt, root_offset);

    model = fdt_getprop(fdt, root_offset, "model", &len);
    if (model)
            sbi_strncpy(platform.name, model, sizeof(platform.name));

    if (generic_plat && generic_plat->features)
            platform.features = generic_plat->features(generic_plat_match);

    cpus_offset = fdt_path_offset(fdt, "/cpus");

携带四个参数,根据汇编规则,正好传递a0,a1,a2,a3这四个参数,a0是0,a1是设备树地址,a2是设备树大小,a3是0。

正好将这些信息利用起来了,然后从设备树中解析qemu中设定相关的信息。

这样的好处是只要入口地址一致,就算设备地址不一样,也不用重新编译opensbi了。这就是有了设备树的好处。

8.fdt重定位

按照riscv的寄存器使用规则,a0-a7都是用于存放C语言函数参数的,下次执行c语言参数就清除掉了,所以需要把设备树从定位,从而让uboot也知道。

9.跳转到sbi_init

到这里,一些底层初始化的关键性细节就结束了,进入sbi的正式的程序中去了。

首先执行的opensbi/lib/sbi/sbi_init.c的sbi_init函数

4.2 opensbi设备初始化 在进入sbi_init会首先判断是通过S模式还是M模式启动,这里先知道在qemu的设备树中是以S模式启动。

所以直接会执行init_coldboot(scratch, hartid);,该函数的实现在opensbi/lib/sbi/sbi_scratch.c中。

看一下冷启动会做那些初始化工作。主要关注

1.sbi_domain_init

初始化动态加载的镜像的模块

2.sbi_platform_early_init

平台的早期初始化

3.sbi_console_init

控制台初始化,从这里开始,就可以使用串口输出了。

4.sbi_platform_irqchip_init

irq中断初始化

5.sbi_ipi_init

核间中断初始化

6.sbi_tlb_init

mmu的tlb表的初始化

7.sbi_timer_init

timer初始化

8.sbi_hsm_prepare_next_jump

准备下一级的boot

4.3 二级boot的跳转 上述条件为二级boot的跳转准备了环境,此时可以加载uboot,或者rtos,或者Linux了。

5.小结 对于riscv的opensbi的大致的执行流程就分析到这里,基本上概括了一个整体的执行的流程,具体的细节可以通过阅读代码,加上代码注释进行理解。本文只是一个map,大致的流程图,很多细节没有涉及到。如果去专研具体的细节部分,感觉还是有很多东西值得学习。opensbi是一个很好的开源项目,对于研究riscv的底层实现,以及代码的通用性上都很值得借鉴和学习。

cisen commented 2 years ago

riscv64 qemu上进行Linux环境搭建与开发记录

https://mp.weixin.qq.com/s?__biz=MzI4MDQ3MzU1MQ==&mid=2247484931&idx=1&sn=c3e2a344d30d34869093ce3dc8d15ed9&chksm=ebb6bca3dcc135b5c9a32c0b7eee242844646411eff5d824a128d11790122fa9f66b78001594&scene=21#wechat_redirect

https://mp.weixin.qq.com/s?__biz=MzI4MDQ3MzU1MQ==&mid=2247484931&idx=1&sn=c3e2a344d30d34869093ce3dc8d15ed9&chksm=ebb6bca3dcc135b5c9a32c0b7eee242844646411eff5d824a128d11790122fa9f66b78001594&scene=21#wechat_redirect

luojia65 commented 2 years ago

为什么不试试看rustsbi呢?

cisen commented 2 years ago

为什么不试试看rustsbi呢?

调试过了,研究了一下rustsbi发现要搞懂rv的汇编和寄存器,又去研究了rv的指令集,研究完指令集现在又切到了cranelift,搞不清楚rust的编译器去写rust很痛苦

luojia65 commented 2 years ago

为什么不试试看rustsbi呢?

调试过了,研究了一下rustsbi发现要搞懂rv的汇编和寄存器,又去研究了rv的指令集,研究完指令集现在又切到了cranelift,搞不清楚rust的编译器去写rust很痛苦

就和研究别的语言一样,只要会写代码就可以了,不用挖到编译器里面,你看你写c语言的时候就没有把gcc或者clang看个遍。这是你认为所谓“复杂”的来源吗?

cisen commented 2 years ago

为什么不试试看rustsbi呢?

调试过了,研究了一下rustsbi发现要搞懂rv的汇编和寄存器,又去研究了rv的指令集,研究完指令集现在又切到了cranelift,搞不清楚rust的编译器去写rust很痛苦

就和研究别的语言一样,只要会写代码就可以了,不用挖到编译器里面,你看你写c语言的时候就没有把gcc或者clang看个遍。这是你认为所谓“复杂”的来源吗?

看个人需要吧,我现在急切需要搞懂这个