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