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文章
3521瀏覽量
93273 -
RISC
+關注
關注
6文章
460瀏覽量
83566
原文標題:riscv64 裸機編程實踐與分析
文章出處:【微信號:Embeded_IoT,微信公眾號:嵌入式IoT】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論