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