riscv64 裸機(jī)編程實踐與分析
-
1.概述
-
2.最小工程的構(gòu)成
-
3. 鏈接腳本
-
4.可執(zhí)行的程序源代碼分析
-
5.編譯與運行
-
5.1 編譯
-
5.2 運行
-
5.3 調(diào)試
-
-
6.總結(jié)
1.概述
任何芯片在啟動之前都需要有一段匯編代碼,從這段匯編代碼上就可以體現(xiàn)一些架構(gòu)設(shè)計的特點。往往做嵌入式底層開發(fā)都需要關(guān)注這段匯編代碼的含義,這樣在使用的時候才能全面的了解啟動時做了什么事情,在后續(xù)的程序中遇到問題也能復(fù)盤推演。
本文就針對riscv64的最開始的啟動部分代碼進(jìn)行分析,從最小的一個裸機(jī)代碼開始分析,徹底的弄清楚riscv啟動的流程。
本次使用的環(huán)境是riscv64 qemu,而編譯器是通過下面的地址進(jìn)行下載:
https://www.sifive.com/software
2.最小工程的構(gòu)成
一個最小的工程包含兩個東西:鏈接腳本以及源代碼。
源代碼就是可以讓cpu執(zhí)行的代碼,通過交叉編譯工具鏈編譯生成可執(zhí)行的二進(jìn)制程序。
鏈接腳本文件則可以告訴程序的布局,比如代碼段,函數(shù)的入口等等。有了這兩個文件將編譯出來的程序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),往往都是規(guī)定如何把輸入的文件按照特定的地址放到內(nèi)存中。
其中就上面的腳本而言:
OUTPUT_ARCH("riscv")
:表示輸入文件的架構(gòu)是riscv。
OUTPUT_FORMAT("elf64-littleriscv")
:表示elf64小端。一般arm,riscv,x86都是小端,小端是比較主流的。
ENTRY( _start )
:表示函數(shù)入口是_start
。
然后開始進(jìn)行代碼段的布局,起始地址開始處為0x80000000
。然后依次放代碼段、數(shù)據(jù)段、只讀數(shù)據(jù)段、全局?jǐn)?shù)據(jù)段,debug段等等。
這里需要注意:
.+=0x8000;
stack_top=.;
這里說明,棧頂預(yù)留了0x8000個字節(jié)空間作為程序的??臻g,因為棧是向上增長的,所以這里預(yù)留了一些??臻g。
通過反匯編來查看生成程序的布局情況
#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.可執(zhí)行的程序源代碼分析
前面已經(jīng)描述了鏈接腳本的布局,也就是給程序指定了執(zhí)行的地址,每個函數(shù)以及函數(shù)入口在什么地址都已經(jīng)規(guī)劃好了,那么具體的入口函數(shù)該如何寫呢?
看看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.
"
根據(jù)匯編語言的規(guī)則
.align2
表示入口程序以2^2
也就是4字節(jié)對齊。
.equUART_BASE,0x10010000
.equUART_REG_TXFIFO,0
定義了UART的寄存器的基地址。
接著主要從_start:
開始分析。
csrrt0,mhartid#readhardwarethreadid(`hart`standsfor`hardwarethread`)
bnezt0,halt#runonlyonthefirsthardwarethread(hartid==0),haltalltheotherthreads
根據(jù)riscv的設(shè)計,如果一個部件包含一個獨立的取指單元,那么該部件被稱為核心(core)。
一個RiscV兼容的核心能夠通過多線程技術(shù)(或者說超線程技術(shù))支持多個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 架構(gòu)來說,a0寄存器表示第一個參數(shù)賦值,接著跳轉(zhuǎn)到puts
函數(shù)中。
此時傳遞過去的參數(shù)為a0
,也就是
.section.rodata
msg:
.string"Hello.
"
指向一個只讀的字符串結(jié)構(gòu)的數(shù)據(jù)。
puts的實現(xiàn)
通過匯編來描述一個串口驅(qū)動程序的編寫是比較重要的。
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
寄存器將參數(shù)傳遞過來,然后從1:
開始,讀取字符串,beqz t1, 3f
表示當(dāng)t1 == 0時,跳轉(zhuǎn)到3:
之前。此時會跳出2:
循環(huán)。
2:
則是向串口FIFO送數(shù)的過程。
到這里一個字符串輸出就可以正常的執(zhí)行了。
5.編譯與運行
5.1 編譯
上述程序分析完成會,可以將其進(jìn)行編譯。
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攜帶的參數(shù)。
-march
:可以指定編譯出來的架構(gòu),比如rv32或者rv64等等。
-static
:表示靜態(tài)編譯。
-mabi=lp64
:數(shù)據(jù)模型和浮點參數(shù)傳遞規(guī)則
數(shù)據(jù)模型:
- | int字長 | long字長 | 指針字長 |
---|---|---|---|
ilp32/ilp32f/ilp32d | 32bits | 32bits | 32bits |
lp64/lp64f/lp64d | 32bits | 64bits | 64bits |
浮點傳遞規(guī)則
- | 需要浮點擴(kuò)展指令? | float參數(shù) | double參數(shù) |
---|---|---|---|
ilp32/lp64 | 不需要 | 通過整數(shù)寄存器(a0-a1)傳遞 | 通過整數(shù)寄存器(a0-a3)傳遞 |
ilp32f/lp64f | 需要F擴(kuò)展 | 通過浮點寄存器(fa0-fa1)傳遞 | 通過整數(shù)寄存器(a0-a3)傳遞 |
ilp32d/lp64d | 需要F擴(kuò)展和D擴(kuò)展 | 通過浮點寄存器(fa0-fa1)傳遞 | 通過浮點寄存器(fa0-fa1)傳遞 |
-mcmodel=medany
:對于-mcmodel=medlow
與-mcmodel=medany
。
-mcmodel=medlow
使用 LUI 指令取符號地址的高20位。LUI 配合其它包含低12位立即數(shù)的指令后,可以訪問的地址空間是 -2GiB ~ 2GiB。
對于 RV64 而言,能訪問的就是 0x0000000000000000 ~ 0x000000007FFFFFFF,以及 0xFFFFFFFF800000000 ~ 0xFFFFFFFFFFFFFFFF 這兩個區(qū)域,前一個區(qū)域即 +2GiB 的地址空間,后一個區(qū)域即 -2GiB 的地址空間。其它地址空間就訪問不到了。
-mcmodel=medany
使用 AUIPC 指令取符號地址的高20位。AUIPC 配合其它包含低12位立即數(shù)的指令后,可以訪問當(dāng)前 PC 的前后2GiB
(PC - 2GiB ~ PC + 2GiB)的地址空間。
對于RV64,取決于當(dāng)前 PC 值,能訪問到是 PC - 2GiB 到 PC + 2GiB 這個地址空間。假設(shè)當(dāng)前 PC 是 0x1000000000000000,那么能訪問的地址范圍是 0x0000000080000000 ~ 0x100000007FFFFFFF。假設(shè)當(dāng)前 PC 是 0xA000000000000000,那么能訪問的地址范圍是0x9000000080000000~0xA00000007FFFFFFF。
-fvisibility=hidden
:動態(tài)庫部分需要對外顯示的函數(shù)接口顯示出來。
-nostdlib
:不連接系統(tǒng)標(biāo)準(zhǔn)啟動文件和標(biāo)準(zhǔn)庫文件,只把指定的文件傳遞給連接器。
-nostartfiles
:不帶main函數(shù)的入口程序。
-Thello.ld
:加載鏈接地址。
5.2 運行
輸入下面的命令即可看到Hello.
字符串輸出。
#qemu-system-riscv64-nographic-machinesifive_u-biosnone-kernelhello
Hello.
5.3 調(diào)試
調(diào)試過程比較只需在運行的后面加-s -S
,即
qemu-system-riscv64-nographic-machinesifive_u-biosnone-kernelhello-s-S
另外再開一個終端輸入
riscv64-unknown-elf-gdbhello
接著輸入target remote localhost:1234
即可。
通過b _start
打斷點,并且通過si
進(jìn)行單步跳轉(zhuǎn)可實現(xiàn)程序的單步運行。
6.總結(jié)
riscv64最小裸機(jī)程序的運行很好理解,主要梳理清楚其啟動地址與鏈接文件即可。還有就是注意gcc的編譯參數(shù),這些對于riscv的啟動來說也是非常關(guān)鍵的部分。
責(zé)任編輯:xj
原文標(biāo)題:riscv64 裸機(jī)編程實踐與分析
文章出處:【微信公眾號:嵌入式IoT】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
編程
+關(guān)注
關(guān)注
88文章
3616瀏覽量
93740 -
RISC
+關(guān)注
關(guān)注
6文章
462瀏覽量
83735
原文標(biāo)題:riscv64 裸機(jī)編程實踐與分析
文章出處:【微信號:Embeded_IoT,微信公眾號:嵌入式IoT】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論