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的意思。
上面的就包含一个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】欢迎添加关注!文章转载请注明出处。
-
编程
+关注
关注
88文章
3614浏览量
93686 -
RISC
+关注
关注
6文章
462浏览量
83708
原文标题:riscv64 裸机编程实践与分析
文章出处:【微信号:Embeded_IoT,微信公众号:嵌入式IoT】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论