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