STM32 的 SystemInit() 和 __main

Author by [YuCloud](https://www.cnblogs.com/yucloud/)

上篇文章 STM32啟動程式碼分析及其彙編學習-ARM 分析了 .S 啟動檔案 ,這次來研究一下 .S 啟動檔案之後執行到 main() 的流程

STM32 總體啟動順序

.s啟動檔案 -> 中斷處理函式外部定義 -> SystemInit() -> __main -> SystemCoreClockUpdate -> SetSysClock -> main()

其中

  • 截圖裡的中斷處理函式外部定義位於 stm32f4xx_it.c(當然這個什麼名字都可以,只要在 Keil 專案裡且函式名為啟動檔案裡定義的中斷處理符號名
  • SystemInit()SystemCoreClockUpdateSetSysClock 三個函式均位於 system_stm32f4xx.c

SystemInit()

用 VSCode+Keil 程式設計環境,在專案裡全域性搜尋 SystemInit

找到4個檔案有用的

  1. 這是個 map 檔案,裡面是連結編譯後對應的索引/對映

  1. 這是 list 檔案,也就是連結後的彙編資訊列表

  1. 這是標頭檔案的宣告

  1. 上面標頭檔案對應的C原始檔,那麼函式定義就在這裡了

其中還呼叫了 SetSysClock

SystemInit 很簡單,檢視一下 STM32 手冊裡的暫存器即可

__main

回顧一下

這裡的

startup_stm32f401xx.o(.text) refers to entry.o(.ARM.Collect$$$$00000000) for __main

是個 .text 段,應該是 Keil 通過ARM彙編成 entry.o 連結到二進位制檔案裡

總之這個 __main 會呼叫 system_stm32f4xx.c 裡的 SystemCoreClockUpdate 函式,然後再呼叫我們c語言裡的 main()。

但由於網上根本搜不到中文資料,因此我決定逆向

把Object目錄裡需要的檔案複製出來,用 IDA 開啟axf檔案,格式選ARM小端(STM32是小端的)

確認後會提示ARM有兩種指令集,並告訴你如何用 IDA 操作 (ALT+G切換模式)

找到 Reset_Handler,這裡二進位制丟失了彙編的Label資訊,所以反彙編後,名字有所不同

Enter 進入檢視函式定義,由於上面有SystemInit()的原始碼,所以用不著反編譯,我們來看 __main 也就是這裡的 _main_stk 函式的定義

__main本體

雖然是 __main ,反編譯名為 _main_stk()。

因為二進位制並沒有包含彙編所有東西,所以反彙編的時候,有些名字只能通過智慧推斷。

LDR.W           SP, =BuildAttributes$$THM_ISAv4$E$P$D$K$B$S$7EM$VFPi3$EXTD16$VFPS$VFMA$PE$A_L22UL41UL21$X_L11$S22US41US21$IEEE1$IW$USESV6$_STKCKD$USESV7$_SHL$OTIME$ROPI$EBA8$MICROLIB$REQ8$PRES8$EABIv2

人為斷句一下,方便閱讀

BuildAttributes$
$THM_ISAv4
$E$P$D
$K$B$S
$7EM
$VFPi3
$EXTD16
$VFPS
$VFMA
$PE
$A_L22UL41UL21
$X_L11
$S22US41US21
$IEEE1
$IW
$USESV6
$_STKCKD
$USESV7
$_SHL
$OTIME
$ROPI
$EBA8
$MICROLIB
$REQ8
$PRES8
$EABIv2

看起來是一些編譯巨集的拼接,Enter 一下,發現是這些:

F5 反編譯成c原始碼(當然反編譯只是根據反彙編結果再反編譯成c,彙編裡可沒有保留c的所有東西(變數名、指標都是沒有的)

這裡介紹一下:IDA 的標籤頁名: IDA View 就是反彙編結果(按空格可切換流程圖和文字模式),Pseudocode 就是反編譯結果(c原始碼模式)

main_init(v0)

繼續看

這裡 IDA 給我們註釋了,是把 __scatterload_rt2 程式的返回值寫入 R0暫存器,

__scatterload_rt2

用 Enter 進入不了 __scatterload_rt2 程式,IDA直接顯示彙編給我們,也就是說這是一段彙編

這裡還呼叫了兩個程式 Region$$Table$$BaseRegion$$Table$$Limit

The DCD directive allocates one or more words of memory, aligned on four-byte boundaries, and defines the initial runtime contents of the memory.

DCD 指令:為一或多個 Word 分配記憶體,四位元組對齊,並定義初始執行時的記憶體內容(也就是向記憶體填充 4位元組32位 的內容)

又根據網上的資料和上文,

main is your main procedure form main.c file, once __main is an internal procedure created by Keil toolchain which is calling at the end your main, but before it is initializing all variables (copying variables from FLASH to proper positions in RAM). In gcc it is seen explicitly, in Keil you can see it within debug process.

__main 是由 Keil 工具鏈建立的內部過程,它初始化所有變數(將變數從 FLASH 複製到 RAM 中的適當位置),並在最後呼叫您的 main,

在 gcc 中它是明確可見的,在 Keil 中你可以在除錯過程中看到它

Keil 也是用了gcc,因此我們參考 GCC 的文件-Initialization

If no init section is available, when GCC compiles any function called main (or more accurately, any function designated as a program entry point by the language front end calling expand_main_function), it inserts a procedure call to __main as the first executable code after the function prologue. The __main function is defined in libgcc2.c and runs the global constructors.

The compiler in Arm Compiler 6 is based on Clang and LLVM technology. As such, it provides a high degree of compatibility with GCC.

當然 Keil 也可以使用 GCC,見 Home » Creating Applications » Tips and Tricks » GNU C Compiler Support

也就是說,__main 是庫自帶的東西,在編譯時會由編譯器連結到二進位制程式裡

猜測:Keil 把用到的變數編譯在了 entry.o,然後把其二進位制直接用DCD寫入記憶體(因此我們看不到其原始資訊)。

官方文件 https://www.keil.com/support/man/docs/armclang_intro/armclang_intro_pge1362066004603.htm

這個 Region$$Table 正是包含了本節提到的兩個看不懂的程式

根據網上的資料,得出 Region$$Table$$Base 是Region$$Table的起始地址, Region$$Table$$Limit是Region$$Table的末尾地址。這兩個共同組合了 Region$$Table 本體。

關於 Region$$Table 本體的說明

  • Region$$Table section

    • [英] containing the addresses of the code and data to be copied or decompressed.
    • [中] 包含了要被複制或解壓縮的程式碼和資料的地址

一些概念:

Code (程式碼段)

ZI (Zero-Inintialize Data段)

RO (ReadOnly Data段)

RW (ReacWrite Data段)

佔用計算:

FLASH 儲存:Code + RO + RW

RAM 記憶體: RW + ZI

在專案的 Object 目錄下的 sct 檔案可見:

其中 InRoot$$Sections 包含了 Region$$Table

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; ************************************************************* LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00018000 { ; RW data
.ANY (+RW +ZI)
}
}

總之這部分超出了我的能力,本文純作拋磚引玉,這裡提供一些關鍵資料:

只能說和 RW Data 壓縮息息相關,DCD那裡應該就是ARM的壓縮指令。

實在著急著秋招,沒心思研究這些指令的含義了...

_main_init

反彙編:

反編譯:

反編譯名字有些不一樣,容易混淆,但根據呼叫流程來看,

這裡的 a2,a3是R1 R2暫存器用於存放和傳遞主函式引數,a1 是 R0暫存器用於函式呼叫

main()

也就是我們 C語言世界的 main 函式,你自己寫的包含 main() 的 c原始碼檔案

當然我寫完了,才發現網上也有類似的文章

https://blog.csdn.net/hgsdfghdfsd/article/details/103812484

但是我的和它不太一樣,區別在於版本

In RVCT v2.0 and earlier, only the __main section and the region tables had to be placed in a root region.

In RVCT v2.1 and above, RW data compression requires that additional sections (such as __dc*.o sections) be placed in a root region.

see: https://developer.arm.com/documentation/dui0206/h/Bhccgbbe

可能因為我是從 STM32 官方手動下載STM32F4xx DSP and Standard Peripherals Library 1.4.0 庫,而不是 Keil 自帶最新的,問題不大,都是對的