我們在寫單片機裸機程序時,在主函數(shù)之前,會有一段啟動代碼,而啟動代碼是用匯編寫的,有些朋友可能看到匯編頭都大了,當(dāng)時要想深入研究底層架構(gòu),這快硬骨頭就必須去啃。
匯編 :匯編文件轉(zhuǎn)換為目標(biāo)文件(里面是機器碼)。
反匯編 :可執(zhí)行文件(目標(biāo)文件,里面是機器碼),轉(zhuǎn)換為匯編文件。
關(guān)于匯編的基礎(chǔ)知識,請看筆者以前的文章。
今天筆者以STM32F1的點燈程序為例,帶領(lǐng)大家進行反匯編,并閱讀反匯編后的代碼。
1 新建LED裸機程序
關(guān)于STM32裸機程序的創(chuàng)建,請看筆者博文:
https://bruceou.blog.csdn.net/article/details/78244735
但是今天這個程序非常簡單,不應(yīng)那么復(fù)雜。
1.新建文件夾
新建文件夾“STM32F1”,當(dāng)然名字也可以另取,在 STM32F1文件夾下,我們新建五個文件夾,分別為CMSIS、Listing、Output、Project、User。
其中CMSIS文件夾放啟動文件:
筆者的開發(fā)板芯片是STM32F103ZE,這個文件是根據(jù)STM32的固件庫startup_stm32f10x_md.s文件修改而來。
2.新建工程
打開Keil,在工具欄 Project->New μVision Project…新建我們的工程文件。
輸入工程名,保存即可。
窗口是讓我們選擇公司跟芯片的型號,我們用的芯片是 ST 公司的STM32F103ZE,有64K SRAM,512K Flash,屬于高集成度的芯片。按如下選擇即可。
然后點擊項目管理。
最后修改后的內(nèi)容如下:
并添加相應(yīng)的文件。
其中main.c的內(nèi)容如下所示:
/**
* @brief 延時函數(shù)
* @param d
* @retval None
*/
void delay(int d)
{
while(d--);
}
/**
* @brief main
* @param None
* @retval int
*/
int main(void)
{
unsigned int *pReg;
/* 使能GPIOB */
pReg = (unsigned int *)(0x40021000 + 0x18);
*pReg |= (1<<3);
/* 設(shè)置GPIOB0為輸出引腳 */
pReg = (unsigned int *)(0x40010C00 + 0x00);
*pReg |= (1<<0);
pReg = (unsigned int *)(0x40010C00 + 0x0C);
while (1)
{
/* 設(shè)置GPIOB0輸出1 */
*pReg |= (1<<0);
delay(1000000);
/* 設(shè)置GPIOB0輸出0 */
*pReg &= ~(1<<0);
delay(1000000);
}
}
startup.s文件的內(nèi)容如下:
;************************************ STM32F1 ************************************
;* File Name : startup.s
;* Author : BruceOu
;* Version : V1.0
;* Date : 2021-06-27
;* Description : STM32F10x Medium Density Devices vector table for MDK-ARM
;* toolchain.
;* This module performs:
;* - Set the initial SP
;* - Set the initial PC == Reset_Handler
;* - Set the vector table entries with the exceptions ISR address
;* - Configure the clock system
;* - Branches to __main in the C library (which eventually
;* calls main()).
;* After Reset the CortexM3 processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
;*******************************************************************************
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x10000)
BL main
ENDP
END
接下來還有配置工程。
選擇Output文件夾。
選擇Listing文件夾。
基本配置就這些,接下來編譯工程。
只要沒有錯誤就可以了,最后就是下載程序,筆者使用的是J-Link,最后的現(xiàn)象如下:
LED會不停閃爍。
2 Keil反匯編
接下來才是今天正題,反匯編。
在KEIL的User選項中,如下圖添加這兩項:
fromelf --bin --output=STM32F1.bin ../Output/STM32F1.axf
fromelf --text -a -c --output=STM32F1.dis ../Output/STM32F1.axf
然后重新編譯,即可得到二進制文件STM32F1.bin(以后會分析)、反匯編文件STM32F1.dis。
如下圖所示:
正常編譯過程是分為四個階段進行的,即預(yù)處理(也稱預(yù)編譯,Preprocessing)、編譯(Compilation)、匯編 (Assembly)和鏈接(Linking)。
但是反編譯是講為二進制文件反編譯成匯編文件,因此反匯編的流程如下:
3 反匯編代碼解析
接下來就是查看反編譯代碼,打開反編譯文件Project/STM32F1.dis。這里只截取一段查看,因為格式都是一樣的,知識每條內(nèi)容不同罷了。
第一列是鏈接地址,第二列是機器碼,第三列是匯編指令。
根本匯編指令,我們找到ARM?v7-M Architecture Reference Manual_DDI 0403E.d (ID070218)中的LDR指令。
我們將F8DFD004變成二進制。
這個使用的32位的Thumb2指令集。
其中b0~b11是立即數(shù),這里是4,對應(yīng)的匯編代碼的也是4,這里要注意的是,ARM指令采用流水線機制,當(dāng)前執(zhí)行地址A的指令,同時已經(jīng)在對下一條指令進行譯碼同時已經(jīng)在讀取下下一條指令:PC = A +4 (Thumb/Thumb2指令集)。
B12~b15是寄存器,這段大小是0XC,對應(yīng)的寄存器就是sp;
后面16bit除了23位意外,全是固定的,其中‘U’表示無條件執(zhí)行,這里置為1。
其他的匯編指令對應(yīng)的機器碼也是類似的,值得注意的是,不同的架構(gòu)對應(yīng)的機器碼也是不同的,這也就回答了為了不同的處理器架構(gòu)會對應(yīng)不同的指令集。
有興趣的可以對比Cortex-M系列和Cortex-A系列的的指令集。請參考以下手冊:
ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition.pdf
ARM?v7-M Architecture Reference Manual.pdf
4 反匯編代碼全解析
進入debug模式,在View下選擇disassembly window。
這樣就可將機器碼和對應(yīng)的代碼對應(yīng)起來。當(dāng)程序運行起來了,也就從異常向量表中跳轉(zhuǎn)到Reset_Handler中,然后跳轉(zhuǎn)到main函數(shù)中,而main函數(shù)是在棧中,因此需要設(shè)置占空間的起始位置。根據(jù)STM32的參考手冊,SRAM的其起始地址和大小如下:
因此棧頂為起始位置加上棧的大小即可,只要不超過SRAM即可。
值得注意的是,棧是__向低地址擴展的數(shù)據(jù)結(jié)構(gòu)__,是一塊連續(xù)的內(nèi)存區(qū)域,棧頂?shù)牡刂泛蜅5淖畲笕萘渴窃谕ㄟ^LDR設(shè)置,因此需要根據(jù)應(yīng)用需求合理分配??臻g。
接下來往下走,如果在匯編中不打斷點,會默認(rèn)進入main函數(shù)的一條指令,就從這里分析。為了分析方便,這里還有使用上一節(jié)方便出來的文件。
【C代碼33行】
從內(nèi)存地址0x0800 017c拷貝數(shù)據(jù)0x40021018到r3中,也就是
r3 = * 0x0800 017c
也就是將pReg指針保存到r3中。
【C代碼34行】
這里對應(yīng)3條指令
首先將r3拷貝到r0中,然后將r0或上1左移3位,也就是
ORR r0,r0,#8
最后將r0的值寫入r3所指地址中。
【C代碼37行】
同33行,從內(nèi)存地址0x0800 0180拷貝數(shù)據(jù)40010c00到r3中
【C代碼38行】
同34行,這里也對應(yīng)3條指令:
【C代碼40行】
和33行不同的是,這里分了兩條指令:
筆者認(rèn)為前面是編譯器優(yōu)化了。根據(jù)ARM指令采用流水線機制,當(dāng)前執(zhí)行地址A的指令,同時已經(jīng)在對下一條指令進行譯碼同時已經(jīng)在讀取下下一條指令:PC = A +4 (Thumb/Thumb2指令集)。因此前面類似的代碼被優(yōu)化了。
接下來就進入循環(huán)中。
后面就移植在死循環(huán)中,不斷操作GPIO的亮滅。
【C代碼45行】
這里是將B0設(shè)置為1,和34行類似。
【C代碼47行】
這里將進入延時函數(shù)。
進入延時函數(shù):
NOP是字節(jié)對齊,減少指令的內(nèi)存訪問次數(shù)。首先將變量d保存到r0,然后將r0賦給r1,接著是r0自減1,緊接著是r1與0比較,如果r1等于0,則會返回,否則,又從頭開始,值得注意的是,這里先比較,然后r0才自減的。
為了進一步說明,可以看--d的匯編代碼。
這里就是相當(dāng)于r1先減1,然后再比較的。
【C代碼50行】
這行代碼對應(yīng)一下指令,很簡單。
5 總結(jié)
在前面使用Keil進行了反匯編,也對相應(yīng)的C代碼進行了分析。我們看到的反匯編代碼如下:
根據(jù)反匯編的代碼,可將其對應(yīng)到Flash,在Flash上的內(nèi)容如下表所示:
地址 | Flash****內(nèi)容 |
---|---|
0x08000000 | 00000000 |
0x08000004 | 08000009 |
0x08000008 | f8dfd004 |
0x0800000c | f000f80c |
… | … |
最后總結(jié)下點燈的流程:
第一步:設(shè)置棧 :CPU會從0x08000000讀取值,用來設(shè)置SP。
第二步:跳轉(zhuǎn) :CPU從0x08000004得到地址值,根據(jù)它的BIT0切換為ARM狀態(tài)或Thumb狀態(tài),然后跳轉(zhuǎn)。
__第三步:__對于cortex M3/M4,它只支持Thumb狀態(tài),所以0x08000004上的值bit0必定是1,0x08000004上的值 = Reset_Handler + 1。從Reset_Handler繼續(xù)執(zhí)行。
第四步 :然后進入到主函數(shù)中執(zhí)行相應(yīng)C代碼。
-
單片機
+關(guān)注
關(guān)注
6030文章
44489瀏覽量
631998 -
匯編
+關(guān)注
關(guān)注
2文章
214瀏覽量
25890 -
編譯
+關(guān)注
關(guān)注
0文章
649瀏覽量
32775 -
反編譯
+關(guān)注
關(guān)注
1文章
14瀏覽量
8503 -
Cortex-M
+關(guān)注
關(guān)注
2文章
227瀏覽量
29708
發(fā)布評論請先 登錄
相關(guān)推薦
評論