1、前言
最近有客戶詢問(wèn),能否使用 STM32CubeIDE 在編譯時(shí)通過(guò)設(shè)置某個(gè)編譯選項(xiàng),讓STM32 應(yīng)用與存儲(chǔ)位置無(wú)關(guān)。這樣的優(yōu)勢(shì)是能使同一個(gè)固件被燒在 STM32 Flash 里的不同位置, 而在系統(tǒng) Bootloader 里只需要跳到相應(yīng)的位置就可以正常執(zhí)行固件代碼??蛻粝M鸖TM32 代碼從 Flash 里執(zhí)行,不復(fù)制到 RAM 里;客戶希望是一個(gè)完整的映像,而不僅僅是其中某個(gè)函數(shù)做到了位置無(wú)關(guān)。
2、分析
在嵌入式場(chǎng)景下,不一定有操作系統(tǒng)。即使有操作系統(tǒng),一般也是 RTOS。一般 RTOS沒(méi)有一個(gè)通用的程序加載器。因此,存儲(chǔ)位置無(wú)關(guān)的需求,在這時(shí)可以說(shuō)無(wú)關(guān)緊要。但是,如果客戶需要進(jìn)行在線固件更新,例如 IoT 應(yīng)用的固件升級(jí),那么位置無(wú)關(guān)就存在價(jià)值了。位置無(wú)關(guān)之后,對(duì)于不同的軟件版本,不需要頻繁的為燒寫位置的反復(fù)改變而修改編譯鏈接腳本。也不需要在代碼里顯式的在兩個(gè) Bank 之間進(jìn)行切換。
最簡(jiǎn)單的情況是所有的代碼都復(fù)制到內(nèi)存執(zhí)行。因?yàn)?Flash 的功能只是進(jìn)行存儲(chǔ),自然對(duì) Flash 的位置沒(méi)有任何要求。但大部分 MCU 用戶面臨的真實(shí)案例都是 Flash 比較大,例如 ,1M 字節(jié) ;RAM 比較小,例如,128K 字節(jié)。在這種情況下,代碼在 Flash 原地執(zhí)行就是一個(gè)必須的選擇。Flash 位置改變,會(huì)影響從 Bootloader 跳轉(zhuǎn)之后的固件執(zhí)行時(shí)的 PC 指針,也就是 PC指針值會(huì)發(fā)生相應(yīng)的變化。位置無(wú)關(guān)的原理,是讓應(yīng)用程序經(jīng)過(guò)編譯后所生成的映像,其中的代碼和數(shù)據(jù),都是基于相對(duì)代碼的位置進(jìn)行引用。那么,當(dāng)應(yīng)用被搬到不同位置時(shí),他們的相對(duì)位置不變,從而執(zhí)行不受影響。
代碼和數(shù)據(jù)基于絕對(duì)地址還是基于相對(duì)地址,是由編譯器所決定。以客戶要求的STM32CubeIDE 編譯工具為例,我們可以看到在[Project]->[Properties]->[C/C++ Build]->[Settings]->[Tool Settings]->[MCU GCC Compiler]->[Miscellaneous]已經(jīng)有一項(xiàng)[Position Independent Code (-fPIC)]。
是否只要選一下-fPIC 選項(xiàng)就大功告成了呢?答案是沒(méi)有那么簡(jiǎn)單。
事實(shí)上,對(duì)于完整應(yīng)用程序工程,用戶應(yīng)該經(jīng)過(guò)這些步驟將其變成位置無(wú)關(guān):
? ? ? ?? 選擇正確的編譯器選項(xiàng)
? 去掉或者替換掉那些包含絕對(duì)位置的庫(kù)文件
? 修改代碼中的 Flash 絕對(duì)地址(這里以 STM32H7 的 CRC_Example 例程為例,
其他情況下有可能要修改更多) o 在 startup_xxx.s 匯編代碼里的 sidata
o 在 system_xxx.c 里的 SCB->VTOR 以及中斷向量表內(nèi)容
o GOT
對(duì)于完整工程,要正確的跳轉(zhuǎn)到應(yīng)用程序進(jìn)行執(zhí)行,還需要由 Bootloader 向應(yīng)用程序提供或者由應(yīng)用程序在鏈接時(shí)自身解析計(jì)算,得到以下信息:
? Flash 偏移量
? 中斷向量表的開(kāi)始以及結(jié)束地址
? GOT 的開(kāi)始以及結(jié)束地址
我們接下來(lái)就舉例說(shuō)明這些步驟。
3、步驟
3.1. 選擇正確的編譯器選項(xiàng)
如果我們不使用任何編譯選項(xiàng),編出來(lái)的代碼會(huì)怎么樣?我們可以通過(guò).list 文件進(jìn)行查看。.list 文件在 STM32 例程中默認(rèn)生成,如果沒(méi)有請(qǐng)勾選如下選項(xiàng), 在 [Project]->[Properties]->[C/C++ Build]->[Settings]->[Tool Settings]->[MCU Post Build outputs]->[Generate list file],可參考下圖。
我們看到代碼中直接使用了變量的絕對(duì)地址,例如 0x2000 0028。我們不要被 literal pool 文字池的使用所迷惑,那個(gè)基于 PC 的操作只是為了取變量的絕對(duì)地址,例如, 0x2000 0028,并沒(méi)有將絕對(duì)地址變成相對(duì)地址。
當(dāng)然大家說(shuō)這里是 RAM 地址,沒(méi)有關(guān)系。我們選擇這個(gè)函數(shù)來(lái)說(shuō)明,是因?yàn)槲恢脽o(wú)關(guān)的編譯器選項(xiàng)是不區(qū)分 RAM 還是 Flash 里的變量,而這個(gè)函數(shù)最簡(jiǎn)單容易理解。如果我們查看另外一個(gè)復(fù)雜一點(diǎn)的函數(shù),例如,HAL_RCC_ClockConfig,我們可以看到以下對(duì)Flash 里變量的直接使用。這就不妙了,因?yàn)橐坏└淖兞?Flash 下載的位置,在絕對(duì)地址處就取不出變量的真實(shí)內(nèi)容了。
我們沒(méi)有辦法一個(gè)一個(gè)查找修改所有的變量。當(dāng)然這里的變量是指全局變量。如果要修改,我們希望編譯器能把他們集中在一起。對(duì)于此,編譯器提供了多個(gè)編譯選項(xiàng)。例如,PIC 是位置無(wú)關(guān)代碼, PIE 是位置無(wú)關(guān)執(zhí)行。PIC 和 PIE 這兩者類似,但是存在一個(gè)顯著的差異是 PIE 會(huì)對(duì)部分全局變量?jī)?yōu)化。我們可以觀察到用兩種不同編譯選項(xiàng)的效果。
其中 80004C0 地址處包含的是 GOT 自身的偏移量,存在 r2 里,要在兩次取全局變量 uwTickFreq 和 uwTick 時(shí)引用。GCC 編譯器引入 GOT 全局偏移量表來(lái)解決全局變量的絕對(duì)地址的問(wèn)題。在之前對(duì)絕對(duì)地址的直接使用,現(xiàn)在被轉(zhuǎn)化成先取得 GOT 入口相對(duì)于 PC 的偏移,再獲得實(shí)際變量相對(duì)于 GOT 入口的偏移,從而得到實(shí)際變量的地址。計(jì)算公式如下:
實(shí)際變量的絕對(duì)地址=PC + GOT 相對(duì)于 PC 的偏移 + 變量地址相對(duì)于 GOT 的偏移
GOT 只有一個(gè),如果代碼放在不同的位置,代碼自身就可以根據(jù) Bootloader 傳遞過(guò)來(lái)的信息,或者自行計(jì)算來(lái)對(duì) GOT 進(jìn)行更新。這樣變量的地址就和新的 Flash 偏移相匹配。
這里可以看到 80004c0 對(duì)應(yīng)的 uwTick(可以從 str 指令結(jié)合 C 語(yǔ)言源代碼快速知道它對(duì)應(yīng)于 uwTick)不再使用 GOT 偏移,而是相對(duì)于 PC 的偏移(與前文相比,多了一條指令 “add r3,pc”)。換句話說(shuō),PIE 對(duì)局部的全局變量做了優(yōu)化。這個(gè)優(yōu)化顯然不是我們所需要的。因?yàn)槿绱艘詠?lái),RAM 變量的地址就會(huì)隨著 PC 的不同而不同。而我們則希望所有對(duì)RAM 的用法不發(fā)生變化。
為了能夠修改 GOT 內(nèi)容,我們選擇將 GOT 最終存放在 RAM 中,導(dǎo)致代碼中對(duì) GOT的尋址也是使用了相對(duì)于 PC 的偏移。而因?yàn)?RAM 有限,或者因?yàn)闆](méi)有虛擬內(nèi)存的緣故,我們不希望 RAM 的用法有所不同,否則,可能代價(jià)很大。這時(shí),一旦 Flash 代碼位置發(fā)生變化引起 PC 指針變化,GOT 就無(wú)法找到。因此,即使我們不使用 PIE,PIC 也沒(méi)有辦法單獨(dú)使用。為了確保沒(méi)有任何存放在 RAM 里的變量的位置是相對(duì)于 PC 的偏移。我們應(yīng)該使用如下所有編譯選項(xiàng),single-pic-base 讓系統(tǒng)只使用一個(gè) PIC 基址,就是下文反匯編中看到
r9;no-pic-data-is-text-relative 則讓編譯器不要讓任何變量相對(duì)于 PC 尋址。
這樣實(shí)際變量的絕對(duì)地址,就變成實(shí)際變量的絕對(duì)地址=PIC 基址 + GOT 相對(duì)于 PIC 基址的偏移 + 變量地址相對(duì)于 GOT的偏移使用以上編譯選項(xiàng),這樣我們看到 HAL_IncTick 就如下所示:
這樣所有在 RAM 里的全局變量都是相對(duì)于 GOT 的偏移。注意,這個(gè)時(shí)候你編譯出來(lái)的代碼現(xiàn)在沒(méi)有辦法進(jìn)行測(cè)試,盡管你只是改了編譯選項(xiàng)。這是因?yàn)?PIC 的基址需要你通過(guò)寄存器 r9 顯式指定。在本例中,我們?cè)阪溄幽_本里如下定義 GOT 的位置:
因此,我們可以很容易的從.map 文件中獲得 GOT_START 的 RAM 地址,0x2000 0000,它就是 PIC 的基址。如果想測(cè)試編譯器選項(xiàng)是否如我們所期望,我們可以在Reset_Handler 開(kāi)始部分加上如下語(yǔ)句(參考后文內(nèi)存布局的代碼):?
經(jīng)過(guò)測(cè)試,我們可以確信,編譯器選項(xiàng)的改動(dòng)對(duì)我們最終執(zhí)行結(jié)果沒(méi)有影響。
值得注意的是,STM32 用戶的代碼,例如 RTOS 的移植, 也可能使用寄存器 r9。在這種情況,用戶應(yīng)當(dāng)解決沖突。一般情況寄存器 r9 對(duì)應(yīng)用程序并不是必要的。
3.2. 去掉或者替換掉那些包含絕對(duì)位置的庫(kù)文件
我們要將位置無(wú)關(guān)的庫(kù)去掉或者替換掉。在 STM32 參考代碼里,我們需要
startup_xxx.s 里 C 庫(kù)調(diào)用去掉。示例如下:
3.3. 修改 Flash 絕對(duì)地址
3.3.1. 內(nèi)存布局
如果要對(duì)代碼中的 Flash 絕對(duì)地址進(jìn)行修改,我們需要知道存放 Flash 絕對(duì)地址的 RAM起始和結(jié)束地址,以及需要增加或減少的 Flash 偏移量。存放 Flash 絕對(duì)地址的 RAM 起始和結(jié)束地址,在編譯時(shí)可以讓應(yīng)用代碼本身借助自身鏈接腳本在鏈接時(shí)導(dǎo)出的變量得到,然后由應(yīng)用程序在運(yùn)行時(shí)存放在 RAM 中的固定位置;也可以在編譯后從.map 文件或使用工具解析 elf 文件獲得,然后作為應(yīng)用程序一部分的元信息,例如,給應(yīng)用程序加個(gè)頭部存放元信息,由 Bootloader 下載并解析,將其放入到 RAM 固定位置。
我們規(guī)劃在一段 RAM 里按如下順序存放如下元信息,它可以是應(yīng)用程序本身在最初階段自我存放在這里,也可以簡(jiǎn)單的由 Bootloader 解析元信息后,跳轉(zhuǎn)到應(yīng)用程序之前就存放在這里。
我們?cè)谇拔囊呀?jīng)在鏈接腳本中定義了 GOT_START 和 GOT_END,我們還需要在鏈接腳本中定義 VT_START 和 VT_END。如下圖所示:
如果我們希望 Bootloader 僅僅是做簡(jiǎn)單的跳轉(zhuǎn),我們可以將規(guī)劃這段內(nèi)存的工作,交給應(yīng)用程序的初始化部分(在 “l(fā)dr sp, =_estack”之前)。假定 0x0 處對(duì)應(yīng)為 0x2400 0000,參考代碼如下:
3.3.2. 匯編代碼
3.3.2.1. _sidata
在默認(rèn)的 STM32 工程中,還有一些對(duì)變量絕對(duì)地址的使用。在 startup_xxx.s 有許多地方使用絕對(duì)地址,它們不能被編譯器收集到 GOT 中。其中,默認(rèn)在鏈接腳本里的_sidata,標(biāo)志 flash 里 RAM 數(shù)據(jù)區(qū)的 Flash 位置,需要修改。
注意,變量絕對(duì)地址本身不是個(gè)問(wèn)題,而對(duì)它解應(yīng)用,取它的內(nèi)容才會(huì)發(fā)生錯(cuò)誤。而這里的 _sidata 是要被初始化代碼使用,目的是將 Flash 的內(nèi)容搬移到 RAM 里。我們顯然要對(duì)_sidata 進(jìn)行修改,否則無(wú)法取得正確的內(nèi)容到 RAM 里。
根據(jù)前文的內(nèi)存布局,我們可以把 Flash 的偏移量從內(nèi)存中放置在寄存器 r8 里,例如:
則我們只需要一行簡(jiǎn)單的代碼 “add r3,r8” 就可以修正_sidata 的地址。
3.3.3. C 代碼
3.3.3.1. 公共函數(shù)
如果一段內(nèi)存的數(shù)據(jù)都是硬編碼,我們只需要一個(gè)公共函數(shù)就可以對(duì)其循環(huán)進(jìn)行修正。我們需要知道什么樣的地址之外不是 Flash 地址,那么就對(duì)這樣的值不做修改。例如,我們定義 0x1fff ffff 之外的就不是 Falsh 地址,相應(yīng)的宏定義如下:
3.3.3.2. SCB->VTOR
在 C 語(yǔ)言中如果使用賦值語(yǔ)句進(jìn)行硬編碼,編譯器也無(wú)法進(jìn)行收集。例如在
system_stm32xxxx.c 中的 SystemInit 有如下語(yǔ)句:
中斷向量表相關(guān)的內(nèi)容需要修改,包括兩部分:
? 中斷向量表的內(nèi)存位置
? 中斷向量表的內(nèi)容
我們應(yīng)該將中斷向量表復(fù)制到 RAM 里,通過(guò) UpdateOffset 函數(shù)修正其中包含的所有Flash 絕對(duì)地址的值,同時(shí)通過(guò)對(duì) SCB->VTOR 賦值來(lái)將中斷向量表的位置指向我們修改過(guò)內(nèi)容的 RAM 地址。注意,VTOR 所指向的地址 VT_RAM_START 要按照 ARM 要求,根據(jù)中斷總大小向上進(jìn)行 2 的冪次對(duì)齊,例如,37 個(gè)字大小要使用 64 個(gè)字對(duì)齊。另外,中斷向量表的內(nèi)容,也包含有 RAM 地址,對(duì)此,我們并不需要修改。當(dāng)然,UpdateOffset 函數(shù)已
經(jīng)考慮到這一點(diǎn),所以我們可以直接使用它。更新中斷向量表以及 VTOR 的參考代碼如下:
3.3.3.3. GOT
編譯器已經(jīng)將 C 語(yǔ)言中所有全局變量的地址都收集到 GOT 中,因此我們很容易對(duì)其Flash 地址的內(nèi)容進(jìn)行修正,參考代碼如下:
4、總結(jié)
除非你僅僅是運(yùn)行一小塊代碼,否則開(kāi)發(fā)位置無(wú)關(guān)的 STM32 完整工程,不僅僅要設(shè)置正確的編譯器選項(xiàng),還要保證它所鏈接的預(yù)編譯的庫(kù)不含有絕對(duì)地址引用,要保證所有源代碼里沒(méi)有對(duì)絕對(duì)地址的硬編碼,包括修改 data 區(qū)的 Flash 起始地址,中斷向量表的內(nèi)容與位置,以及 GOT 的內(nèi)容。
審核編輯:湯梓紅
?
評(píng)論
查看更多