對于嵌入式開發者來說,了解匯編語言和內核寄存器是對內核深入理解的基礎 從開始寫起也沒想到內容有這么多,其中有很多干貨的東西,希望自己能夠說明到了。
開頭直接來看幾個簡單的匯編指令:
MOV R0,R1MOV PC,R14 上面的指令中使用了匯編 MOV指令,但是其中的 R0,R1,R14,PC分別是什么?哪來的?怎么用? 要講 ARM 匯編語言,必須得先了解ARM的內核寄存器,內核處理所有的指令計算,都需要用到內核寄存器,所以ARM匯編里面指令大都是基于寄存器的操作。 文章前推薦韋東山老師的單片機核心視頻,視頻可以在韋東山老師官網里面找到:百問網 ARM版本簡單介紹: 內核(架構)版本 處理器版本
?
? | ? |
ARMv1 | ARM1 |
ARMv2 | ARM2、ARM3 |
ARMv3 | ARM6、 |
ARMv4 | ARM7、StrongARM |
ARMv5 | ARM9、ARM10E |
ARMv6 | ARM11 |
ARMv7 | ARM Cortex-A、ARM Cortex-M、ARM Cortex-R |
ARMv8 | ARM Cortex-A30、ARM Cortex-A50、ARM Cortex-A70 |
?
一、ARM內核寄存器
內核寄存器與外設寄存器: 內核寄存器與外設寄存器是完全不同的概念。內核寄存器是指 CPU 內部的寄存器,CPU處理所有指令數據需要用到這些寄存器保存處理數據;外設寄存器是指的 串口,SPI,GPIO口這些設備有關的寄存器。 在我的另一篇博文:FreeRTOS記錄(三、FreeRTOS任務調度原理解析_Systick、PendSV、SVC)內核中斷管理 章節講到過Cortex-M的寄存器的相關內容,這里我們再簡單說明一下:
1.1 M3/M4內核寄存器
對于M3/M4而言:
R13,棧指針(Stack Pointer)
R13寄存器中存放的是棧頂指針,M3/M4 的棧是向下生長的,入棧的時候地址是往下減少的。
裸機程序不會用到PSP,只用到MSP,需要運行RTOS的時候才會用到PSP。
堆棧主要是通過POP,PUSH指令來進行操作。在執行 PUSH 和 POP 操作時, SP 的地址寄存器,會自動調整。
R14 ,連接寄存器(Link Register)
LR 用于在調用子程序時存儲返回地址。例如,在使用 BL(分支并連接, Branch and Link)指令時,就自動填充 LR 的值(執行函數調用的下一指令),進而在函數退出時,正確返回并執行下一指令。如果函數中又調用了其他函數,那么LR將會被覆蓋,所以需要先將LR寄存器入棧。
保存子程序返回地址。使用BL或BLX時,跳轉指令自動把返回地址放入r14中;子程序通過把r14復制到PC來實現返回
當異常發生時,異常模式的r14用來保存異常返回地址,將r14如??梢蕴幚砬短字袛?/p>
R15,程序計數器(Program Count)
在Cortex-M3中指令是3級流水線,出于對Thumb代碼的兼容的考慮,讀取pc時,會返回當前指令地址+4的值。
讀 PC 時返回的值是當前指令的地址+4,關于M3、M4 和 A7的 PC值的問題需要單獨來解釋一下
其中程序狀態寄存器 ?XPSR:
程序狀態寄存器,該寄存器由三個程序狀態寄存器組成應用PSR(APSR) :包含前一條指令執行后的條件標志,比較結果:大于等于,小于,進位等等;中斷PSR(IPSR ) :包含當前ISR的異常編號執行PSR(EPSR) :包含Thumb狀態位
1.2 A7內核寄存器
對于 A7 而言:
(上圖取自原子教材,此圖在官方文檔《ARM Cortex-A(armV7)編程手冊V4.0》中第3章.ARM Processor Modes and Registers 部分有英文原版,這里用中文版本更容易理解) A7的 R13、R14、R15 的作用和 M3/4類似。 需要注意的一點就是,對于A7而言**R15,程序計數器(Program Count)**:
讀 PC 時返回的值是當前指令的地址+8, PC 指向當前指令的下兩條指令地址。
由于ARM指令總是以字對齊的,故PC寄存器 bit[1:0] 總是00。
A7內核的程序狀態寄存器 ?CPSR:
1.3 ARM中的PC指針的值
因為ARM指令采用三級流水線機制,所以PC指針的值并不是當前執行的指令的地址值:
當前執行地址A的指令,
同時已經在對下一條指令進行譯碼,
同時已經在讀取下下一條指令:PC = A +4 (Thumb/Thumb2指令集)、PC = A + 8 (ARM指令集)
在文檔《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》中對于 PC 的值有明確的說明:M3/M4/M0:
PC的值 = 當前地址 + 4;
下面是一個 STM32F103 反匯編程序,找了一段有[pc,#0]的代碼,方便判斷:
A7:
PC的值 = 當前地址 + 8;
二、ARM匯編語言
ARM芯片屬于精簡指令集計算機(RISC:Reduced Instruction Set Computing),具體說明在下面這篇博文5.4小結有過說明:
STM32的內存管理相關(內存架構,內存管理,map文件分析)
2.1 ARM匯編基礎
2.1.1 ARM指令集說明
最初,ARM公司發布了兩類指令集:
ARM指令集,32位的ARM指令,每條指令占據32位,高效,但是太占空間;
Thumb指令集,16位的Thumb指令,每條指令占據16位,節省空間;
比如:MOV R0,R1 這條指令,可能是16位的,也可能是32位的
那么在匯編中是如何在 ARM 指令 和 Thumb 指令之間切換呢:
?
?
/*ARM指令?與?Thumb?指令?的切換*/ CODE16??;(表示下面是?Thumb?指令) ... ... ;(調用下面的B函數) bx??B_addr;(B的地址B_addr的bit0?=?0,表示跳轉過去執行?ARM?指令) ;A?函數 ... CODE32??;(表示下面是?ARM?指令) ... ... ;B?函數 ;(回到上面的A函數) bx??A_addr?+?1?;(A的地址A_addr的bit0?=?1,表示跳轉過去執行?Thumb?指令) ... /**********************/
?
?
對于A7、ARM7、ARM9 內核而言它們支持 16位的Thumb 指令集 和 32位的 ARM 指令集
對于M3、M4 內核而言它們支持的是 Thumb2 指令集,它支持16位、32位指令混合編程
對于內核來說使用的是 ARM指令集 還是 Thumb指令集,就是在 XPSR 和 CPSR
在M3/M4中, XPSR 寄存器的 T(bit24):1表示 Thumb指令集根據上面所述,M3是使用的 Thumb2 指令集,所以會有 T 總是 1.
在A7中 CPSR中的:T(bit5) :控制指令執行狀態,表明本指令是 ARM 指令還是 Thumb 指令,通常和 J(bit24)一起表明指令類型
?
J(bit24) | T(bit5) | 指令集 |
---|---|---|
0 | 0 | ARM |
0 | 1 | Thumb |
1 | 1 | ThumbEE ?-- ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?提供從Thumb-2而來的一些擴充性,在所處的運行環境下,使得指令集能特別適用于運行階段的編碼產生(例如實時編譯)。Thumb-2EE是專為一些語言如Limbo、Java、C#、Perl和Python,并能讓實時編譯器能夠輸出更小的編譯碼卻不會影響到性能。 |
1 | 0 | Jazelle |
?
回到開始的指令 MOV R0,R1
?
?
code?16??;(表示下面指令是16位的?Thumb?指令) MOV?R0,R1 code?32??;(表示下面指令是32位的?ARM?指令) MOV?R0,R1 Thumb????;(編譯器會根據指令自動識別是32位還是16位的?Thumb2) MOV?R0,R1
?
?
2.1.2 ARM匯編格式
編碼格式:
不同指令集的編碼格式(以 LDR 為例),摘自《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》:以“數據處理”(其他的還有內存訪問,分支跳轉等)指令為例,UAL匯編格式為:Operation
表示各類匯編指令,比如 ADD、MOV;cond表示conditon,即該指令執行的條件,如 EQ,NE 等;S表示該指令執行后,是否會影響CPSR寄存器的值, 是否影響CPSR 寄存器的值,書寫時影響CPSR,否則不影響;Rd
為目的寄存器,用來存儲運算的結果;Rn第一個操作數的寄存器Operand2第二個操作數 ,其可以有3種操作源:1-- 立即數2-- 寄存器3-- 寄存器移位
其指令編碼格式如下(32位):|bit 31-28 ?|27-25 ?|24-21 ?|20 ?|19-16 | 15-12 |11-0 ?||--|--|--|--|--|--|--|--|--||cond ?| 001 |Operation |S ?|Rn |Rd ?| Operand2 |
舉個例子:
?
?
... CMP?R0,R2??????;比較R0和R2的值 MOV?EQ?R0,R1??;加上EQ,如果上面R0的值和R2的值相等的話,才執行此語句 ...
?
?
對于“數據處理”處理指令中的Operation ?,指令集如下:對于其中的條件cond ,如下:
2.1.3 立即數
在一條ARM數據處理指令中,除了要包含處理的數據值外,還要標識ARM命令名稱,控制位,寄存器等其他信息。這樣在一條ARM數據處理指令中,能用于表示要處理的數據值的位數只能小于32位;
在上面的ARM匯編格式中我們介紹過,ARM在指令格式中設定,只能用指令機器碼32位中的低12位來表示要操作的常數。
那么對于指令MOV R0, #value(把value的值存入R0寄存器)而言,value 的值也不能是任意的值,其值只能是符合某些規定的數,在官方文檔中 value 的值需要滿足如下條件:什么是立即數?
滿足上圖中條件的數我們稱之為 立即數,立即數就是符合一定規矩的數。
立即數表示方式:每個立即數由一個8位的常數循環右移偶數位得到。其中循環右移的位數由一個4位二進制的兩倍表示。
立即數 = ?一個8位的常數 ?循環位移 ?偶數位
一個8bit常數循環右移(Y*2 = {0,2,4,6,8, ...,26, 28, 30})就得到一個立即數了;(為什么是0到30的偶數下面解釋)
如果需要深入理解立即數,推薦一篇博文:深刻認識 -->> 立即數
ARM處理器是按32位來處理數據的,ARM處理器處理的數據是32位,為了擴展到32位,因此使用了構造的方法,在12位中用8位表示基本數據值,用4位表示位移值,通過用8位基本數據值往右循環移動4位位移值*2次,來表示要操作的常數。
這里要強調最終的循環次數是4位位移值乘以2得到的,所以得到的最終循環次數肯定是一個偶數,為什么要乘以2呢,實質還是因為范圍不夠,4位表示位移次數,最大才15次(移位0,等于沒有循環),加上8位數據還是不夠32位,這樣只能通過ALU的內部結構設計將4位位移次數乘以2,這樣就能用12位表示32位常數了。
所以 12bit 數據存放格式如下:|bit 11-8 ?|7-0 ||--|--|--|--|--|--|--|--|--||移位 1111b (0~15) | 8bit常數 |
但是我們去判斷一個數是否立即數,實在是太麻煩了,但是我們想把任意數值賦給 R0 寄存器,怎么辦?? 這就需要用到偽指令了,下面說一說什么是偽指令。
2.2 匯編偽指令
匯編語言分成兩塊:標準指令集和非標準指令集。偽指令屬于非標準指令集。
什么是偽指令?
類似于宏的東西,把復雜的有好幾天指令進行跳轉的完成的小功能級進行新的標簽設定,這就是偽指令。
類似于學c語言的時候的預處理,在預處理的時候把它定義于一堆的宏轉化為真正的c語言的代碼。同樣,偽指令是在定義好之后的匯編,匯編的時候會把它翻譯成標準指令,也許一條簡單的偽指令可以翻譯成很多條標準的匯編指令集,所以這就是偽指令最重要的作用。
我們前面說的 CODE16 CODE32也是偽指令,用來指定其后的代碼格式。
偽指令的作用?
基本的指令可以做各類操作了,但操作起來太麻煩了。偽指令定義了一些類似于帶參數的宏,能夠更好的實現匯編程序邏輯。(比如我現在要設置一個值給寄存器R0,但下次我修改了寄存器R0之后又需要讀出來剛才的值,那我們就要先臨時保存值到SPSR,CPSR,然后不斷切換。)
偽指令只是在匯編器之前作用,匯編以后翻譯為標準的匯編令集。
偽指令的類別偽指令可分為ARM匯編偽指令和GNU匯編偽指令
ARM匯編偽指令是ARM公司的,GNU匯編偽指令是GNU平臺的。他們有自己的匯編器,不同的匯編器的解釋語法可以設成不同。
在這里插入圖片描述
2.2.1 GNU匯編偽指令
這里列出部分偽指令說明,具體的偽指令可以結合 ARM匯編偽指令分析:
?
bit 11-8 | 7-0 |
---|---|
.word | 分配一個4字節空間 |
.byte | 定義單字節數據 |
.short | 定義雙字節數據 |
.long | 定義一個4字節數據 |
.equ | 賦值語句:.equ a, 0x11 |
.align | 數據字節對齊:.align 4 (4字節對齊) |
.global | 定義全局符號:.global Default_Handler |
.end | 源文件結束 |
?
2.2.2 ARM匯編偽指令
在我的另一篇博文:STM32的啟動過程(startup_xxxx.s文件解析)
里面有過一些對偽指令意思的的說明,下面也列出部分說明:
AREA:
用于定義一個代碼段或數據段。屬性字段表示該代碼段(或數據段)的相關屬性,多個屬性用逗號分隔。其中,段名若以數字開頭,則該段名需用?“?|?”?括起來:
ALIGN:
ALIGN?偽指令可通過添加填充字節的方式,使當前位置滿足一定的對其方式。其中,表達式的值用于指定對齊方式,可能的取值為2的冪,如?1?、2?、4?、8?、16?等。若未指定表達式,則將當前位置對齊到下一個字的位置。
CODE16和CODE32:
指定其后面的指令為 ARM 指令還是?Thumb?指令,前面介紹過。
ENTRY:
用于指定匯編程序的入口點。在一個完整的匯編程序中至少要有一個?ENTRY?(也可以有多個,當有多個?ENTRY?時,程序的真正入口點由鏈接器指定),但在一個源文件里最多只能有一個?ENTRY。
在startup_stm32f103xg.s里面就沒有。
END:
用于通知編譯器已經到了源程序的結尾。IMPORT 和 EXPORT:
IMPORT ?定義表示這是一個外部變量的標號,不是在本程序定義的EXPORT 表示本程序里面用到的變量提供給其他模塊調用的
2.2.3 LDR 和 ADR
LDR偽指令:
簡單介紹了偽指令基礎,回到上一小結留下的問題,想要把任意值復制給 R0,怎么處理,我們使用偽指令: LDR R0, =value
編譯器會把“偽指令”替換成真實的指令:
LDR R0, =0x12
0x12是立即數,那么替換為:MOV R0, #0x12
LDR R0, =0x123456780x12345678不是立即數,那么替換為:LDR R0, [PC, #offset] ? ? ? ? // 2. 使用Load Register讀內存指令讀出值,offset是鏈接程序時確定的……Label DCD 0x12345678 ? ?// 1. 編譯器在程序某個地方保存有這個值
ADR偽指令:
ADR的意思是:address,用來讀某個標號的地址:ADR{cond} Rd, labe1
?
?
ADR??R0,??Loop ... Loop ????ADD??R0,?R0,?#1 ????;(它是“偽指令”,會被轉換成某條真實的指令,比如:) ADD?R0,?PC,?#val???;?loop的地址等于PC值加上或者減去val的值,val的值在鏈接時確定, ... Loop ????ADD??R0,?R0,?#1
?
?
2.3 ARM匯編指令集
在《ARM Cortex-M3與Cortex-M4權威指南》一文中第5章節有詳細的指令集說明:匯編指令可以分為幾大類:數據處理、內存訪問、跳轉、飽和運算、其他指令。
數據傳輸命令 MOV
MOV指令,用于將數據從一個寄存器拷貝到另外一個寄存器,或者將一個立即數傳遞到寄存器。
MOV指令的格式為:MOV{條件}{S} 目的寄存器,源操作數
?
?
MOV?R0,R1?????;@將寄存器R1中的數據傳遞給R0,即R0=R1 MOV?R0,?#0X12??;@將立即數0X12傳遞給R0寄存器,即R0=0X12
?
?
狀態寄存器訪問 MRS 和 MSR
MRS指令,用于將特殊寄存器(如CPSR和SPSR)中的數據傳遞給通用寄存器。
MSR指令,和MRS相反,用來將普通寄存器的數據傳遞給特殊寄存器。
?
?
;M3/M4 MRS??R0,?APSR??;單獨讀APSR MRS??R0,??PSR??;?讀組合程序狀態 ;A7 MRS??R0,?CPSR??;?讀組合程序狀態 ... MSR?CPSR,R0???;傳送R0的內容到CPSR
?
?
存儲器訪問 LDR 和 STR
LDR:
LDR 指令用于從存儲器中將一個32位的字數據傳送到目的寄存器中。該指令通常用于從存儲器中讀取32位的字數據到通用寄存器,然后對數據進行處理。
指令的格式為:LDR{條件} 目的寄存器,<存儲器地址>
當程序計數器PC作為目的寄存器時,指令從存儲器中讀取的字數據被當作目的地址,從而可以實現程序流程的跳轉。
LDRB: 字節操作
LDRH: 半字操作
?
?
LDR?Rd,?[Rn?,?#offset]?;從存儲器Rn+offset的位置讀取數據存放到Rd中。 ... LDR?R0,?=0X02077004?;偽指令,將寄存器地址?0X02077004?加載到?R0?中,即?R0=0X02077004 LDR?R1,?[R0]????????;讀取地址?0X02077004?中的數據到?R1?寄存器中 ... LDR R0,[R1,R2]??????;將存儲器地址為R1+R2的字數據讀入寄存器R0。 LDR??R0,[R1,#8]?????;將存儲器地址為R1+8的字數據讀入寄存器R0。 ... LDR??R0,[R1,R2,LSL#2]!?;將存儲器地址R1+R2×4的字數據讀入寄存器R0,并將新地址R1+R2×4寫入R1。 LDR??R0,[R1],R2,LSL#2??;將存儲器地址R1的字數據讀入寄存器R0,并將新地址R1+R2×4寫入R1。 ... LDRH R0,[R1]??????;將存儲器地址為R1的半字數據讀入寄存器R0,并將R0的高16位清零。
?
?
STR:
STR 指令用于從源寄存器中將一個32位的字數據傳送到存儲器中。該指令在程序設計中比較常用,且尋址方式靈活多樣,使用方式可參考指令LDR。
指令的格式為:STR{條件} 源寄存器,<存儲器地址>
STRB: 字節操作,從源寄存器中將一個8位的字節數據傳送到存儲器中。該字節數據為源寄存器中的低8位。
STRH: 半字操作,從源寄存器中將一個16位的半字數據傳送到存儲器中。該半字數據為源寄存器中的低16位。
?
?
STR?Rd,?[Rn,?#offset]?;將Rd中的數據寫入到存儲器中的Rn+offset位置。 ... LDR?R0,?=0X02077004?;將寄存器地址?0X02077004?加載到?R0?中,即?R0=0X02077004 LDR?R1,?=0X2000060c?;R1?保存要寫入到寄存器的值,即?R1=0X2000060c STR?R1,?[R0]????????;將?R1?中的值寫入到?R0?中所保存的地址中 ... STR?R0,[R1],#8??;將R0中的字數據寫入以R1為地址的存儲器中,并將新地址R1+8寫入R1。 STR?R0,[R1,#8]??;將R0中的字數據寫入以R1+8為地址的存儲器中。 ...
?
?
壓棧和出棧 PUSH 和 POP
PUSH :
壓棧,將寄存器中的內容,保存到堆棧指針指向的內存上面,將寄存器列表存入棧中。
PUSH < reg list >
POP :
出棧,從棧中恢復寄存器列表
POP < reg list >
?
?
push?{R0,?R1}???;保存R0,R1 push?{R0~R3,R12}?;保存?R0~R3?和?R12,入棧 pop?{R0~R3}???????;恢復R0?到?R3?,出棧
?
?
以M3內核來舉個例子:
假設當前 MSP 值為 ?0x2000 2480;寄存器 R0 的值為 0x3434 3434寄存器 R1 的值為 0x0000 1212寄存器 R2 的值為 0x0000 0000
執行push {R0, R1,R2}之后,
內存地址的數據為:0x2000 2474的值為: 0x3434 3434 ?(R0的值)0x2000 2478的值為: 0x0000 1212 ?(R1的值)0x2000 247C的值為: 0x0000 0000 (R2的值)MSP 的值變成 ?0x2000 2474
高位寄存器保存到高地址,先入棧,如果是POP,數據先出到低位寄存器
跳轉指令 B 和 BL
B :
ARM 處理器將立即跳轉到指定的目標地址,不再返回原地址。
B指令的格式為:B{條件} 目標地址
注意存儲在跳轉指令中的實際值是相對當前PC值的一個偏移量,而不是一個絕對地址,它的值由匯編器來計算。
?
?
//設置棧頂指針后跳轉到C語言 _start: ldr?sp,=0X80200000??;設置棧指針 b?main??????????;跳到?main?函數
?
?
BL :
BL 跳轉指令,在跳轉之前會在寄存器LR(R14)中保存當前PC寄存器值,所以可以通過將LR 寄存器中的值重新加載到PC中來繼續從跳轉之前的代碼處運行,是子程序調用的常用的方法。
?
?
BL?loop??;跳轉到標號loop處執行時,同時將當前的PC值保存到R14中
?
?
BLX:
該跳轉指令是當子程序使用Thumb指令集,而調用者使用ARM指令集時使用。
BLX指令從ARM指令集跳轉到指令中所指定的目標地址,并將處理器的工作狀態有ARM狀態切換到Thumb狀態,該指令同時將PC的當前內容保存到寄存器R14中。
BX:
BX指令跳轉到指令中所指定的目標地址,目標地址處的指令既可以是ARM指令,也可以是Thumb指令。
算數運算指令
算數運算指令和下面的邏輯運算指令表格摘自《【正點原子】I.MX6U嵌入式Linux驅動開發指南》
邏輯運算指令
在這里插入圖片描述
三、代碼反匯編簡析
匯編匯編文件轉換為目標文件(里面是機器碼,機器碼是給CPU使用的,燒錄保存在Flash空間的就是機器碼)。
反匯編可執行文件(目標文件,里面是機器碼),轉換為匯編文件。
3.1 不同編譯器的反匯編
3.1.1 Keil下面生成反匯編文件
fromelf –text -a -c –output=(改成你想生成的反匯編名字一般是工程名字).dis (需要的axf文件,根據你工程生成axf的路徑填寫).axf設置好以后編譯之后就會生成反匯編.dis文件:
打開如下所示:對于上圖中的紅色圈出來的語句,我們可以根據本文 第 二 章節的第2小節 ARM匯編格式中的介紹來分析一下:
簡單分析如下(立即數就不分析了= =?。?img src="https://file1.elecfans.com/web2/M00/CB/B6/wKgaomYfTWuAdFc0AAQjAZF3cTI713.png" alt="43f08fd0-fbac-11ee-a297-92fbcf53809c.png" />
3.1.2 gcc下生成反匯編文件
在X86架構下的電腦上生成ARM架構的匯編代碼有兩種方式:
使用交叉編譯工具鏈 指定-S選項可以生成匯編中間文件。ex:gcc -S test.c
使用 objdump 反匯編 arm二進制文件。
上述兩種方法的區別為:
(1)反匯編可以生成ARM指令操作碼,-S生成的匯編沒有指令碼(2)反匯編的代碼是經過編譯器優化過的。(3)反匯編代碼量很大。
對于ARM Cortex-M,使用的是 arm-none-eabi-objdump,常用指令如下:
arm-none-eabi-objdump -d -S(可省) a1.o ? 查看a1.o反匯編可執行段代碼
arm-none-eabi-objdump -D -S(可省) a1.o ? 查看a1.o反匯編所有段代碼
arm-none-eabi-objdump -D -b binary -m arm ab.bin ?查看ab.bin反匯編所有代碼段
對于使用 arm-none-eabi-gcc ?工具鏈(以STM32CUbeMX)的內核來說,使用如下方式生成反匯編文件:
$(OBJDUMP) -D -b binary -m arm ?(需要的elf文件,一般是工程名字).elf ?> (改成你想生成的反匯編名字,一般是工程名字).dis ? ? ? # OBJDUMP = arm-none-eabi-objdump
-D表示對全部文件進行反匯編,-b表示二進制,-m表示指令集架構
Makefile修改如下:
?
?
... TARGET?=?D6TPir ####################################### #?paths ####################################### #?Build?path BUILD_DIR?=?build ... PREFIX?=?arm-none-eabi- ... OBJDUMP?=?$(PREFIX)objdump dis: ?$(OBJDUMP)?-D?-b?binary?-m?arm?$(BUILD_DIR)/$(TARGET).elf?>?$(BUILD_DIR)/$(TARGET).dis #?$(OBJDUMP)?-D?-b?binary?-m?arm?$(BUILD_DIR)/$(TARGET).bin?>?$(BUILD_DIR)/$(TARGET).dis
?
?
執行 make ?dis 即可生成 .dis 文件:打開文件查看,發現怎么這個匯編語言有點不一樣:經過研究了一段時間,加上了-M force-thumb后稍微有點樣子了:在網上有各種參考,但是我都測試過了,并沒有找到合適的生成完全和標準匯編一致的那種,-M后面的參數也不能亂加,需要根據自己的交叉編譯器,因為這里用的是 arm-none-eabi-gcc,所以可以通過arm-none-eabi-objdump --help 查看能用的命令和參數:gcc工具鏈下的匯編還是不太熟悉,所以我們下面反匯編文件與 C語言的對比,使用Keil下的反匯編進行說明。
3.2 C 和 匯編 比較分析
前面介紹了那么多,最終用一個簡單的程序對比一下C語言反匯編后的匯編語言,加深一下印象,當作個實戰總結。
基于STM32L051(Cortex-M0)內核,目的是為了比較C和匯編,用了個最簡單的程序來分析,沒有用到任務外設,程序如下:
?
?
//前面省略... void?delay(u32?count) { ?while(count--); } u32?add(u16?val1,u16?val2) { ?u32?add_val; ? ?add_val?=?val1?+?val2; ? ?return?add_val; } ?int?main(void) ?{ ?u16?a,b; ?u32?c; ?a?=?12345; ?b?=?45678; ?c?=?add(a,b); ?while(1) ?{ ???c--; ???delay(200000); ??} ?}
?
?
反匯編的代碼對應部分如下(因為基于硬件平臺,其他異常中斷,堆,棧,包括其他一些也有匯編代碼,這里省略):
?
?
;省略前面 ????delay ????????0x080001ae:????bf00????????..??????NOP?????? ????????0x080001b0:????1e01????????..??????SUBS?????r1,r0,#0 ????????0x080001b2:????f1a00001????....????SUB??????r0,r0,#1 ????????0x080001b6:????d1fb????????..??????BNE??????0x80001b0?;?delay?+?2 ????????0x080001b8:????4770????????pG??????BX???????lr ????add ????????0x080001ba:????4602????????.F??????MOV??????r2,r0 ????????0x080001bc:????1850????????P.??????ADDS?????r0,r2,r1 ????????0x080001be:????4770????????pG??????BX???????lr ????main ????????0x080001c0:????f2430439????C.9.????MOV??????r4,#0x3039 ????????0x080001c4:????f24b256e????K.n%????MOV??????r5,#0xb26e ????????0x080001c8:????4629????????)F??????MOV??????r1,r5 ????????0x080001ca:????4620?????????F??????MOV??????r0,r4 ????????0x080001cc:????f7fffff5????....????BL???????add?;?0x80001ba ????????0x080001d0:????4606????????.F??????MOV??????r6,r0 ????????0x080001d2:????e003????????..??????B????????0x80001dc?;?main?+?28 ????????0x080001d4:????1e76????????v.??????SUBS?????r6,r6,#1 ????????0x080001d6:????4804????????.H??????LDR??????r0,[pc,#16]?;?[0x80001e8]?=?0x30d40 ????????0x080001d8:????f7ffffe9????....????BL???????delay?;?0x80001ae ????????0x080001dc:????e7fa????????..??????B????????0x80001d4?;?main?+?20 ????$d ????????0x080001de:????0000????????..??????DCW????0 ????????0x080001e0:????e000ed0c????....????DCD????3758157068 ????????0x080001e4:????05fa0000????....????DCD????100270080 ????????0x080001e8:????00030d40????@...????DCD????200000 ;省略后面
?
?
3.2.1 MOV后面 立即數的疑問
在對比分析這段代碼前,在 main 函數中的第一句:
?
?
0x080001c0:????f2430439????C.9.????MOV??????r4,#0x3039
?
?
就有一個大大的疑問, MOV r4,#0x3039中 0x3039 并不是立即數(按照我們第二章 立即數的說明) ,包括接下來的 0xb26e 也不是立即數,怎么可以直接用 mov,按理來說需要用 LDR偽指令的??
至于這個問題,網上簡單查找了一下,找到一篇有關說明的文章:ARM 匯編的mov操作立即數的疑問 ?其中有說到,在 keil 公司方網站里關于arm匯編的說明里有這么一段:
SyntaxMOV{cond} Rd, #imm16where: imm16 is any value in the range 0-65535.
所以是不是在 Keil 中的arm匯編 立即數可以使16位的?
為了驗證一下,我稍微修改了一下程序,就是把a的值賦值超過16位(當然定義函數之類的也要跟著改,測試代碼中a為u16的無符號整形),測試了一下。
a賦值為 65535,結果如下(65535不是立即數,也可以直接mov):
?
?
0x080001c0:????f64f75ff????O..u????MOV??????r5,#0xffff?
?
?
a賦值為 65536,結果如下(65536是立即數,可以直接mov):
?
?
0x080001c0:????f44f3580????O..5????MOV??????r5,#0x10000
?
?
a賦值為一個大于16位的,不是立即數的數,比如:0x1FFFF :
?
?
0x080001c0:????4d08????????.M??????LDR??????r5,[pc,#32]?;?[0x80001e4]?=?0x1ffff
?
?
果然,最后當 a 大于16位,不是立即數時候,會使用偽指令 LDR,所以我們可以得出結論:
在 Keil 中的arm匯編中,16位內(包括16位)的數都直接使用 MOV 賦值,大于16位,如果是立即數,直接使用MOV,不是立即數用LDR (立即數的判斷方式還是前面講的那樣)
3.2.2 反匯編文件解析
對于上面的示例程序的匯編碼,簡單解析如下:添加一個有意思的測試對于delay函數中的語句,上圖是while(count--);改成while(--count);后匯編代碼如下:
對于上面的測試程序,匯編中并沒有使用到 PUSH 和 POP 指令,因為程序太簡單了,不需要使用到棧,為了能夠熟悉下單片機中必須且經常需要用到的 棧,我們稍微修改一下add函數,在add函數中調用了delay函數:
?
?
u32?add(u16?val1,u16?val2) { ?u32?add_val; ? ?add_val?=?val1?+?val2; ? ?delay(10); ? ?return?add_val; }
?
?
對于的add函數匯編代碼如下:
?
?
?add ????????0x080001ba:????b530????????0.??????PUSH?????{r4,r5,lr}???;把r4?r5?lr的值入棧 ????????0x080001bc:????4603????????.F??????MOV??????r3,r0 ????????0x080001be:????460c????????.F??????MOV??????r4,r1 ????????0x080001c0:????191d????????..??????ADDS?????r5,r3,r4 ????????0x080001c2:????200a????????.???????MOVS?????r0,#0xa ????????0x080001c4:????f7fffff3????....????BL???????delay?;?0x80001ae ????????0x080001c8:????4628????????(F??????MOV??????r0,r5 ????????0x080001ca:????bd30????????0.??????POP??????{r4,r5,pc}??;把r4?r5?lr的值出棧,
?
?
(匯編中可以看到指令后面后面加了個S ,MOVS 、ADDS,這就是我們前面說到的,帶了S 會影響 xPSR 寄存器中的值)
可以看到,因為存在函數的多次調用,main函數中調用add函數,add函數中調用delay函數,所以在add函數運行之前,通過 push 把 r4,r5,lr 寄存器的值先存入棧中,等待程序執行完(函數調用結束)再吧 ?r4,r5,lr 寄存器的值恢復。
上面的程序雖然簡單,但是通過我們C程序 與 匯編程序的對比分析,能夠讓我們更加深入的理解匯編語言.
審核編輯:黃飛
?
評論
查看更多