0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

riscv64裸机编程实践与分析

嵌入式IoT 来源:嵌入式IoT 作者:嵌入式IoT 2020-12-31 10:54 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

riscv64 裸机编程实践与分析

  • 1.概述

  • 2.最小工程的构成

  • 3. 链接脚本

  • 4.可执行的程序源代码分析

  • 5.编译与运行

    • 5.1 编译

    • 5.2 运行

    • 5.3 调试

  • 6.总结

1.概述

任何芯片在启动之前都需要有一段汇编代码,从这段汇编代码上就可以体现一些架构设计的特点。往往做嵌入式底层开发都需要关注这段汇编代码的含义,这样在使用的时候才能全面的了解启动时做了什么事情,在后续的程序中遇到问题也能复盘推演。

本文就针对riscv64的最开始的启动部分代码进行分析,从最小的一个裸机代码开始分析,彻底的弄清楚riscv启动的流程。

本次使用的环境是riscv64 qemu,而编译器是通过下面的地址进行下载:

https://www.sifive.com/software

2.最小工程的构成

一个最小的工程包含两个东西:链接脚本以及源代码。

源代码就是可以让cpu执行的代码,通过交叉编译工具链编译生成可执行的二进制程序。

链接脚本文件则可以告诉程序的布局,比如代码段,函数的入口等等。有了这两个文件将编译出来的程序loader到板子上运行即可。

3. 链接脚本

下面看一下hello.ld文件。

OUTPUT_ARCH("riscv")
OUTPUT_FORMAT("elf64-littleriscv")
ENTRY(_start)
SECTIONS
{
/*text:testcodesection*/
.=0x80000000;
.text:{*(.text)}
/*data:Initializeddatasegment*/
.gnu_build_id:{*(.note.gnu.build-id)}
.data:{*(.data)}
.rodata:{*(.rodata)}
.sdata:{*(.sdata)}
.debug:{*(.debug)}
.+=0x8000;
stack_top=.;

/*Endofuninitalizeddatasegement*/
_end=.;
}

对于链接脚本(linker script),往往都是规定如何把输入的文件按照特定的地址放到内存中。

其中就上面的脚本而言:

OUTPUT_ARCH("riscv"):表示输入文件的架构是riscv。

OUTPUT_FORMAT("elf64-littleriscv"):表示elf64小端。一般arm,riscv,x86都是小端,小端是比较主流的。

ENTRY( _start ):表示函数入口是_start

然后开始进行代码段的布局,起始地址开始处为0x80000000。然后依次放代码段、数据段、只读数据段、全局数据段,debug段等等。

这里需要注意:

.+=0x8000;
stack_top=.;

这里说明,栈顶预留了0x8000个字节空间作为程序的栈空间,因为栈是向上增长的,所以这里预留了一些栈空间。

通过反汇编来查看生成程序的布局情况

#riscv64-unknown-elf-objdump-dhello

hello:fileformatelf64-littleriscv


Disassemblyofsection.text:

0000000080000000<_start>:
80000000:f14022f3csrrt0,mhartid
80000004:00029c63bnezt0,8000001c
80000008:00008117auipcsp,0x8
8000000c:04410113addisp,sp,68#8000804c<_end>
80000010:00000517auipca0,0x0
80000014:03450513addia0,a0,52#80000044
80000018:008000efjalra,80000020

000000008000001c:
8000001c:0000006fj8000001c

0000000080000020:
80000020:100102b7luit0,0x10010
80000024:00054303lbut1,0(a0)
80000028:00030c63beqzt1,80000040
8000002c:0002a383lwt2,0(t0)#10010000
80000030:fe03cee3bltzt2,8000002c
80000034:0062a023swt1,0(t0)
80000038:00150513addia0,a0,1
8000003c:fe9ff06fj80000024
80000040:00008067ret

对于qemu来说,sifive_u的起始地址为0x80000000,将代码段的入口放在此处。

4.可执行的程序源代码分析

前面已经描述了链接脚本的布局,也就是给程序指定了执行的地址,每个函数以及函数入口在什么地址都已经规划好了,那么具体的入口函数该如何写呢?

看看hello.s编程代码:

.align 2
.equ UART_BASE,         0x10010000
.equ UART_REG_TXFIFO,   0

.section .text
.globl _start

_start:
        csrr  t0, mhartid             # read hardware thread id (`hart` stands for `hardware thread`)
        bnez  t0, halt                   # run only on the first hardware thread (hartid == 0), halt all the other threads

        la    sp, stack_top           # setup stack pointer

        la    a0, msg                 # load address of `msg` to a0 argument register
        jal   puts                    # jump to `puts` subroutine, return address is stored in ra regster

halt:   j     halt                    # enter the infinite loop

puts:                                 # `puts` subroutine writes null-terminated string to UART (serial communication port)
                                      # input: a0 register specifies the starting address of a null-terminated string
                                      # clobbers: t0, t1, t2 temporary registers

        li    t0, UART_BASE           # t0 = UART_BASE
1:      lbu   t1, (a0)                # t1 = load unsigned byte from memory address specified by a0 register
        beqz  t1, 3f                  # break the loop, if loaded byte was null

                                      # wait until UART is ready
2:      lw    t2, UART_REG_TXFIFO(t0) # t2 = uart[UART_REG_TXFIFO]
        bltz  t2, 2b                  # t2 becomes positive once UART is ready for transmission
        sw    t1, UART_REG_TXFIFO(t0) # send byte, uart[UART_REG_TXFIFO] = t1

        addi  a0, a0, 1               # increment a0 address by 1 byte
        j     1b

3:      ret

.section .rodata
msg:
     .string "Hello.
"

根据汇编语言的规则

.align2

表示入口程序以2^2也就是4字节对齐。

.equUART_BASE,0x10010000
.equUART_REG_TXFIFO,0

定义了UART的寄存器的基地址。

接着主要从_start:开始分析。

csrrt0,mhartid#readhardwarethreadid(`hart`standsfor`hardwarethread`)
bnezt0,halt#runonlyonthefirsthardwarethread(hartid==0),haltalltheotherthreads

根据riscv的设计,如果一个部件包含一个独立的取指单元,那么该部件被称为核心(core)。

一个RiscV兼容的核心能够通过多线程技术(或者说超线程技术)支持多个RiscV兼容硬件线程(harts),harts这儿就是指硬件线程, hardware thread的意思。

ba4f8054-4ad0-11eb-8b86-12bb97331649.png


上面的就包含一个E51的核和4个U54的核。

而这段汇编就是将其他的核挂起,只运行hartid == 0的核。

紧接着

lasp,stack_top#setupstackpointer

这里将栈指针sp赋值,sp此时指向栈顶。

laa0,msg#loadaddressof`msg`toa0argumentregister
jalputs#jumpto`puts`subroutine,returnaddressisstoredinraregster

对于riscv 架构来说,a0寄存器表示第一个参数赋值,接着跳转到puts函数中。

此时传递过去的参数为a0,也就是

.section.rodata
msg:
.string"Hello.
"

指向一个只读的字符串结构的数据。

puts的实现

通过汇编来描述一个串口驱动程序的编写是比较重要的。

puts:#`puts`subroutinewritesnull-terminatedstringtoUART(serialcommunicationport)
#input:a0registerspecifiesthestartingaddressofanull-terminatedstring
#clobbers:t0,t1,t2temporaryregisters

lit0,UART_BASE#t0=UART_BASE
1:lbut1,(a0)#t1=loadunsignedbytefrommemoryaddressspecifiedbya0register
beqzt1,3f#breaktheloop,ifloadedbytewasnull

#waituntilUARTisready
2:lwt2,UART_REG_TXFIFO(t0)#t2=uart[UART_REG_TXFIFO]
bltzt2,2b#t2becomespositiveonceUARTisreadyfortransmission
swt1,UART_REG_TXFIFO(t0)#sendbyte,uart[UART_REG_TXFIFO]=t1

addia0,a0,1#incrementa0addressby1byte
j1b

3:ret

首先刚才通过a0寄存器将参数传递过来,然后从1:开始,读取字符串,beqz t1, 3f表示当t1 == 0时,跳转到3:之前。此时会跳出2:循环。

2:则是向串口FIFO送数的过程。

到这里一个字符串输出就可以正常的执行了。

5.编译与运行

5.1 编译

上述程序分析完成会,可以将其进行编译。

riscv64-unknown-elf-gcc-march=rv64g-mabi=lp64-static-mcmodel=medany-fvisibility=hidden-nostdlib-nostartfiles-Thello.ld-Isifive_uhello.s-ohello

上述编译过程可以生成hello程序。

#readelf-hhello
ELFHeader:
Magic:7f454c46020101000000000000000000
Class:ELF64
Data:2'scomplement,littleendian
Version:1(current)
OS/ABI:UNIX-SystemV
ABIVersion:0
Type:EXEC(Executablefile)
Machine:RISC-V
Version:0x1
Entrypointaddress:0x80000000
Startofprogramheaders:64(bytesintofile)
Startofsectionheaders:4680(bytesintofile)
Flags:0x0
Sizeofthisheader:64(bytes)
Sizeofprogramheaders:56(bytes)
Numberofprogramheaders:1
Sizeofsectionheaders:64(bytes)
Numberofsectionheaders:7
Sectionheaderstringtableindex:6

可以分析一下gcc携带的参数。

-march:可以指定编译出来的架构,比如rv32或者rv64等等。

-static:表示静态编译。

-mabi=lp64:数据模型和浮点参数传递规则

数据模型:

- int字长 long字长 指针字长
ilp32/ilp32f/ilp32d 32bits 32bits 32bits
lp64/lp64f/lp64d 32bits 64bits 64bits

浮点传递规则

- 需要浮点扩展指令? float参数 double参数
ilp32/lp64 不需要 通过整数寄存器(a0-a1)传递 通过整数寄存器(a0-a3)传递
ilp32f/lp64f 需要F扩展 通过浮点寄存器(fa0-fa1)传递 通过整数寄存器(a0-a3)传递
ilp32d/lp64d 需要F扩展和D扩展 通过浮点寄存器(fa0-fa1)传递 通过浮点寄存器(fa0-fa1)传递

-mcmodel=medany:对于-mcmodel=medlow-mcmodel=medany

-mcmodel=medlow

使用 LUI 指令取符号地址的高20位。LUI 配合其它包含低12位立即数的指令后,可以访问的地址空间是 -2GiB ~ 2GiB。

对于 RV64 而言,能访问的就是 0x0000000000000000 ~ 0x000000007FFFFFFF,以及 0xFFFFFFFF800000000 ~ 0xFFFFFFFFFFFFFFFF 这两个区域,前一个区域即 +2GiB 的地址空间,后一个区域即 -2GiB 的地址空间。其它地址空间就访问不到了。

-mcmodel=medany

使用 AUIPC 指令取符号地址的高20位。AUIPC 配合其它包含低12位立即数的指令后,可以访问当前 PC 的前后2GiB (PC - 2GiB ~ PC + 2GiB)的地址空间。

对于RV64,取决于当前 PC 值,能访问到是 PC - 2GiB 到 PC + 2GiB 这个地址空间。假设当前 PC 是 0x1000000000000000,那么能访问的地址范围是 0x0000000080000000 ~ 0x100000007FFFFFFF。假设当前 PC 是 0xA000000000000000,那么能访问的地址范围是0x9000000080000000~0xA00000007FFFFFFF。

-fvisibility=hidden:动态库部分需要对外显示的函数接口显示出来。

-nostdlib:不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器

-nostartfiles:不带main函数的入口程序。

-Thello.ld:加载链接地址。

5.2 运行

输入下面的命令即可看到Hello.字符串输出。

#qemu-system-riscv64-nographic-machinesifive_u-biosnone-kernelhello
Hello.

5.3 调试

调试过程比较只需在运行的后面加-s -S,即

qemu-system-riscv64-nographic-machinesifive_u-biosnone-kernelhello-s-S

另外再开一个终端输入

riscv64-unknown-elf-gdbhello

接着输入target remote localhost:1234即可。

通过b _start打断点,并且通过si进行单步跳转可实现程序的单步运行。

6.总结

riscv64最小裸机程序的运行很好理解,主要梳理清楚其启动地址与链接文件即可。还有就是注意gcc的编译参数,这些对于riscv的启动来说也是非常关键的部分。

责任编辑:xj

原文标题:riscv64 裸机编程实践与分析

文章出处:【微信公众号:嵌入式IoT】欢迎添加关注!文章转载请注明出处。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编程
    +关注

    关注

    90

    文章

    3707

    浏览量

    96765
  • RISC
    +关注

    关注

    6

    文章

    485

    浏览量

    86098

原文标题:riscv64 裸机编程实践与分析

文章出处:【微信号:Embeded_IoT,微信公众号:嵌入式IoT】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    【CIE全国RISC-V创新应用大赛】+MUSE Pi Pro RiscV UEFI固件开发

    /edk2-non-osi 1.4 RiscV64工具链的前缀定义 为RiscV64工具链指定前缀变量,参考如下命令: #define RISCV64 prefix export
    发表于 11-13 00:20

    riscv底层原理分析gd32vf103的中断行为

    riscv底层原理分析gd32vf103的中断行为 1.概述 2.中断向量表初始化 3.详细分析一下irq_entry 4.关于gd32vf103中断编程模型的理解 1.概
    发表于 10-31 08:04

    全志D1开发板(哪吒 RISCV64)开箱评测

    riscv的生态建设远远没有arm强大,但是也在开源思想的引领下,开始逐步走向大众的视野。 这块哪吒 RISCV64的板子,从主要的核的特性上来看,与目前市面上可见的riscv开发板相比,特性主要有以下几点
    发表于 10-31 07:50

    riscv实现自定义指令并用qemu运行

    利用qemu模拟硬件平台,实现特定指令解析,同时写裸机代码来测试该指令的运行情况。当然,如果实现的很好,是需要修改riscv的gcc的,让自己的扩展指令加入。这里不做修改,后面会详细描述细节。 自定义
    发表于 10-31 07:37

    d1哪吒开发板的启动流程分析

    的可以研究很深的开发板。本文主要从研究D1启动流程的角度出发,探索一下D1的裸机开发实践。对于研究D1的底层裸机开发,首先需要知道可以玩那些东西,也可以对RISCV相关的软件生态有比较
    发表于 10-29 06:44

    用哪吒D1开发板体验riscv向量底层编程

    1.前言 2.机器模式处理器状态寄存器(MSTATUS) 3.编译选项支持V扩展 4.RISCV向量计算的原理 5.通过实例分析RISCV V扩展的运作机制 6.RVV使用体验 1.前言
    发表于 10-29 06:21

    目前最新版的userapps如何安装D1s环境?

    /get_toolchain.py和 smart-env.sh,按照最新代码的Readme.md去安装,在xmake f -a riscv64时hui’chu’xian’bao’cu会出现报错,报错信息如下
    发表于 09-25 07:33

    riscv virt64编译后 ls无法运行怎么解决?

    用仓库里的默认配置编译qemu-virt64-riscv 生成后运行,显示 [E/DBG] virtio-blk0 mount failed ls看不到文件夹 msh />ls No such directory
    发表于 09-22 06:38

    【Milk-V Duo S 开发板免费体验】2 - 安装编译环境并测试Hello world!

    ARM64 2. RISCV64 Which would you like: 我使用的是 RISCV 系统,则选择 2。 这个脚本会克隆GitHub - milkv-duo/host-tools,不过只有
    发表于 07-07 21:18

    【VisionFive 2单板计算机试用体验】RISC-V架构卡片计算机初体验

    前言 说实话,板子收到一周了才发布第一篇试用报告有点过分,主要是之前没接触过RISCV64的板子,原先在ARM板上积累的经验貌似在这边不太行得通,再加上貌似国内大厂和高校的源对于RISCV架构的支持
    发表于 07-06 23:28

    【Milk-V Duo S 开发板免费体验】应用开发环境搭建

    选择 2。 由于 DuoS 支持 RISCV 和 ARM 两种架构,还需要按提示继续选择: Select Arch: 1. ARM64 2. RISCV64 Which would you like
    发表于 07-01 21:22

    【M-K1HSE开发板免费体验】超高性能与颜值RISCV64位8核视美泰M-K1HSE开发板

    【超高性能与颜值RISCV64位8核视美泰M-K1HSE开发板】 https://www.bilibili.com/video/BV1dQKZzsERi/?share_source
    发表于 06-26 23:14

    请问Openvino是否支持 Risc-V (riscv64) 架构?

    在spacemit k1型板(bpi-f3)上编译OpenVINO™,但失败。 使用 riscv64 构建OpenVINO™并崩溃。
    发表于 06-24 07:26

    使用msys2 mingw64编译nuclei openocd源码出错怎么解决?

    :msys64homeAdministratorbuildnuclei-riscv-openocdbuild/../src/jtag/drivers/mpsse.c:358:(.text+0xc71): undefined reference
    发表于 03-07 15:04

    铁电存储器SF24C64对标FM24C64性能、应用和成本分析

    铁电存储器SF24C64对标FM24C64性能、应用和成本分析
    的头像 发表于 03-03 10:25 1240次阅读
    铁电存储器SF24C<b class='flag-5'>64</b>对标FM24C<b class='flag-5'>64</b>性能、应用和成本<b class='flag-5'>分析</b>