Thumb指令集之: ARM和Thumb的混合編程
Thumb以其較高的代碼密度和在窄存儲器上的性能,使得它在很多系統(tǒng)中得到廣泛應用。但在很多情況下,還是不得不使用ARM指令,這是因為:
①ARM代碼比Thumb代碼有更快的執(zhí)行速度;
②ARM處理器的一些特定功能必須由ARM指令實現(xiàn),其中包括PSR指令、協(xié)處理器指令;
③異常發(fā)生時,處理器自動進入ARM狀態(tài),如果異常處理程序需要使用Thumb指令也必須通用一個ARM程序頭(ARMassemblerheader)。
基于以上原因,即使程序需要由Thumb代碼實現(xiàn),也必須通過ARM-Thumb互交(ARM-Thumbinterworking)進入Thumb狀態(tài)。
ARM-Thumb互交是指對匯編語言和C/C++語言的ARM和Thumb代碼進行連接的方法,它進行兩種狀態(tài)(ARM和Thumb狀態(tài))間的切換。在進行這種切換時,有時需使用額外的代碼,這些代碼被稱為Veneer。AAPCS定義了ARM和Thumb過程調(diào)用的標準。
從一個ARM例程調(diào)用一個Thumb例程,內(nèi)核必須進行狀態(tài)切換。狀態(tài)的變化由CPSR的T位來顯示。在跳轉到一個例程時BX指令可用于ARM和Thumb狀態(tài)切換,具體用法如下。
在Thumb狀態(tài)調(diào)用ARM例程時,采用:
BXRn;
在ARM狀態(tài)調(diào)用Thumb例程時,采用:
BX{cond}Rn;
其中,Rn可以是r0~r15中的任意寄存器。
這種帶狀態(tài)切換的跳轉指令BX,將寄存器Rn的內(nèi)容拷貝到程序計數(shù)器寄存器PC,因此可以實現(xiàn)4G空間的跳轉。指令根據(jù)寄存器Rn的bit[0]來決定處理器是否進行狀態(tài)切換,詳細內(nèi)容參見ARM指令一節(jié)。
下面是一段ARM程序,該程序調(diào)用虛擬的SWI_writeC子程序從存儲器的固定地址取出字符串“helloworld”并輸出。
AREAHello,CODE,READONLY
SWI_WriteCEQU&0 ;軟中斷調(diào)用參數(shù)
SWI_ExitEQU&11 ;程序退出軟中斷調(diào)用參數(shù)
ENTRY
STARTADRr1,TEXT ;取字符串地址
LOOPLDRBr0,[r1],#1 ;取下一字節(jié)內(nèi)容
CMPr0,#0 ;判斷是否為字符串尾
SWINESWI_WriteC ;軟中斷調(diào)用打印字符
BENLOOP ;循環(huán)
SWISWI_Exit ;軟中斷調(diào)用退出程序執(zhí)行
TEXT=“HelloWorld”,&0a,&0d,0
END
下面的代碼將上面的ARM代碼轉換成等價的Thumb代碼。
AREAHelloW_Thumb,CODE,READONLY
SWI_WriteCEQU&0 ;軟中斷調(diào)用參數(shù)
SWI_ExitEQU&11 ;程序退出軟中斷調(diào)用參數(shù)
ENTRY ;程序入口點
CODE32進入ARM狀態(tài)
ADRr0,START+1 ;取得Thumb代碼入口地址
BXr0 ;進入Thumb代碼
CODE16 ;Thumb代碼入口點
STARTADRr1,TEXT ;r1->"HelloWorld"
LOOPLDRBr0,[r1] ;取下一字節(jié)內(nèi)容
ADDr1,r1,#1 ;地址指針加1**T
CMPr0,#0 ;判斷是否為字符串尾
BEQDONE ;完成?**T
SWISWI_WriteC ;如果不是字符串尾
BLOOP ;繼續(xù)循環(huán)
DONESWISWI_Exit ;程序退出
ALIGN ;字對齊
TEXTDATA
"HelloWorld",&0a,&0d,&00
END
上例中,ARM代碼到Thumb代碼轉換過程中新增加的指令用“**T”標注。
在實現(xiàn)ARM代碼和Thumb代碼轉換時,大部分的ARM指令有等價的Thumb指令,只有少數(shù)指令沒有。如加載字節(jié)指令(LDR)不支持自動變址,軟中斷指令不能條件執(zhí)行。
在編寫Thumb代碼時要注意以下幾點。
①匯編器需要知道什么時候產(chǎn)生ARM代碼、什么時候產(chǎn)生Thumb代碼,程序中使用CODE32和CODE16偽操作提供給編譯器這些信息。
②由于處理器上電執(zhí)行是在ARM狀態(tài)下完成的,所以要使用Thumb指令必須由ARM指令調(diào)用Thumb指令,這一過程是通過“BXLR”指令來實現(xiàn)的。需要注意的是,在使用“BXLR”指令前,要對寄存器LR做正確的初始化。
③在ARM和Thumb混合編程時,常使用ALIGN偽操作保證內(nèi)存地址對齊。
11.10.2互交子程序編寫ARM/Thumb互交代碼時,下面兩點需要注意。
①對于C/C++子程序而言,只要在編譯時指定--apcs/interwork選項,匯編器會生成合適的返回代碼,使得程序返回到和調(diào)用程序相同的狀態(tài)。
②在匯編語言子程序中,用戶必須自己編寫相應的返回代碼,使得程序返回到和調(diào)用程序相同的狀態(tài)。
如果目標代碼包含以下內(nèi)容,應該在編譯或匯編時使用--apcs/interwork選項使處理器能夠在ARM和Thumb代碼間進行正確的切換,這種情況包含以下4種。
①需要返回到ARM狀態(tài)的Thumb子程序。
②需要返回到Thumb狀態(tài)的ARM子程序。
③間接調(diào)用ARM子程序的Thumb子程序。
④間接調(diào)用Thumb子程序的ARM子程序。
如果在程序連接階段,連接器發(fā)現(xiàn)ARM子程序和Thumb子程序間存在相互調(diào)用,而源文件在編譯時沒有使用--apcs/interwork選項,則連接器將報告以下錯誤。
Error:L6239E:CannotcallARMsymbol'arm_function'innon-interworkingobject
armsub.ofromTHUMBcodeinthumbmain.o(.text)
其中,“arm_function”為需要進行狀態(tài)切換的子程序名。
在這種情況下,用戶必須使用--apcs/interwork選項重新對源文件進行編譯。
但在下面兩種情況下,不必指定--apcs/interwork選項。
①在Thumb狀態(tài)下,發(fā)生異常中斷時,處理器自動切換到ARM狀態(tài),這時不需要添加狀態(tài)切換代碼。
②當異常發(fā)生在Thumb狀態(tài)時,從異常返回不需要添加狀態(tài)切換的Veneer代碼。
1.使用匯編語言實現(xiàn)互交對于匯編程序來說,可以有兩種方法來實現(xiàn)程序狀態(tài)的切換。第一種方法是利用連接器提供的交互子程序Veneer來實現(xiàn)程序狀態(tài)的切換,這時用戶可以使用指令BL來調(diào)用子程序;另一種方法是用戶自己編寫狀態(tài)切換的程序。
在ARMv4版本及其以前的版本中,可以使用BX指令實現(xiàn)程序狀態(tài)的切換。
從ARMv5版本開始,下面的指令也可以用來實現(xiàn)程序的狀態(tài)切換。
·BX(BranchandeXchange)
·BLX、LDR、LDM和POP
下面的兩個偽操作用來區(qū)分源程序中的ARM代碼和Thumb代碼。
·CODE16
·CODE32
下面簡單介紹用于狀態(tài)切換的指令和偽操作,更詳細的信息請分別參見相關章節(jié)。
(1)BX指令
ARM狀態(tài)下的BX指令,使程序跳轉到指令中指定的參數(shù)Rm指定的地址執(zhí)行程序,Rm的第0位拷貝到CPSR中T位,位[31∶1]移入PC。若Rm的bit[0]為1,則跳轉時自動將CPSR中的標志位T置位,即把目標地址的代碼解釋為Thumb代碼;若Rm的位bit[0]為0,則跳轉時自動將CPSR中的標志位T復位,即把目標地址代碼解釋為ARM代碼。指令的語法格式如下。
BX{<cond>}<Rm>
①<cond>
為指令編碼中的條件域。它指示指令在什么條件下執(zhí)行。當<cond>忽略時,指令為無條件執(zhí)行(cond=AL(Alway))。
②<Rm>
包含跳轉指令的目標地址。如果Rm的bit[0]=0,目標地址處指令為ARM指令;如果Rm的bit[0]=1,目標地址處指令為Thumb指令。
指令操作的偽代碼。
指令操作的偽代碼如下面程序段所示。
IfconditionPassed{cond}then
TFlag=Rm[0]
PC=RmAND0xfffffffe
Thumb狀態(tài)下的BX指令,用于ARM和Thumb代碼間的相互調(diào)用。
指令的語法格式。
BX<Rm>
其中<Rm>為目標地址寄存器,包含程序的跳轉地址。BX指令的目標地址寄存器可以是r0~r15中的任意寄存器。
注意
如果Rm[1:0]=0b10,不滿足ARM指令的內(nèi)存對齊方式。指令的執(zhí)行結果不可預知。如果該指令使用r15作為目標寄存器,其操作方式和使用其他寄存器相同。
指令操作的偽代碼如下所示。
TFlag=Rm[0]
PC=Rm[31:1]<<1
ARM指令集中的BX指令和Thumb指令集中的BX指令相差較大,它們分別為不同方向的跳轉。當r15作為目的寄存器使用時,要特別注意該指令在兩個指令集中的區(qū)別。
(2)BLX指令
ARM狀態(tài)下的BLX指令使用一個寄存器中的絕對地址或標號,用于使程序跳轉到Thumb狀態(tài)或從Thumb狀態(tài)返回,該指令用分支寄存器的最低位來更新CPSR中的T位,并將返回地址寫入到連接寄存器LR中。
指令的語法格式如下所示:
①BLX<target_addr>
②BLX{<cond>}<Rm>
第一種格式中,轉移目標按下述方法計算。將指令中指定的24位偏移量進行符號擴展,左移兩位形成字偏移量,然后將其累加進程序計數(shù)器PC中。這時,程序計數(shù)器的內(nèi)容為BX指令地址加8字節(jié)。位H(bit[24])也加到結果地址的第一位(bit[1]),使目標地址為半字地址,以執(zhí)行接下來的Thumb指令。計算偏移量的工作一般由ARM匯編器來完成。這種形式的跳轉指令只能實現(xiàn)±32MB空間的跳轉。
第二種格式中,寄存器Rm指定轉移目標,Rm的第0位拷貝到CPSR中的T位,bit[31:0]移入PC。
·如果Rm的bit[0]=1,則跳轉時自動將CPSR中的標志位T置位,即把目標地址的代碼解釋為Thumb代碼。
·如果Rm的bit[0]=0,則跳轉時自動將CPSR中的標志位T復位,即把目標地址的代碼解釋為ARM代碼。
指令操作的偽代碼如下面程序段所示。
第一種格式BLX指令。
LR=addressoftheinstructionaftertheBLXinstruction
TFlag=1
PC=PC+PC=PC+(SignExtend(signed_immed_24)<<2)+(H<<1)
第二種格式BLX指令。
IfConditionPass{cond}then
LR=addressoftheinstructionafterthebranchinstruftion
TFlag=Rm[0]
PC=RmAND0xfffffffe
Thumb狀態(tài)下帶返回鏈接的跳轉指令BLX(1)提供了一種在Thumb狀態(tài)下無條件調(diào)用ARM子程序的方法,當從子程序返回時,通常使用下面的方式之一:
·BXLR
·加載PC的LDR或LDM指令。
BLX指令不可條件執(zhí)行,可以實現(xiàn)在大約±4MB的地址空間范圍內(nèi)跳轉,實現(xiàn)方法是將一條BLX指令編譯成兩條16位的Thumb指令,從而實現(xiàn)上述跳轉。對編譯后的兩條指令說明如下。
①H=10的跳轉指令。該跳轉包含跳轉偏移量的高位部分。
②H=01的跳轉指令。該跳轉包含跳轉偏移量的低位部分。
指令的語法格式
BL<target_address>
<target_address>
指定程序跳轉的目標地址。指令通過下面的方法計算目標地址。
·將H=10的BL指令的offset_11域左移12位。
·將結果符號擴展為32位。
·將得到的值加到PC寄存器中。
·與H=11的BL指令的offset_11域相加。
因此BL指令可以實現(xiàn)在大約±4MB的地址空間范圍內(nèi)跳轉。
指令操作的偽代碼為:
ifH==10then
LR=PC+(SignExtend(offset_11)<<12)
ElseifH==11then
PC=LR+(offset_11<<11)
LR=(addressofnextinstruction)|1
ElseifH==01then
PC=(LR+(offset_11<<1))AND0xFFFFFFFC
LR=(addressofnextinstruction)|1
ElseifH==01then
PC=(LR+(offset_11<<1))AND0Xfffffffc
LR=(addressofnextinstruction)|1
TFlag=0
另外Thumb狀態(tài)下包含另一種格式的BLX指令,該BLX(2)指令用于ARM和Thumb子程序間的相互調(diào)用。程序狀態(tài)字的T標志位根據(jù)目的寄存器的bit[0]位而改變。
指令的語法格式為:
BLX<Rm>
其中<Rm>為目標地址寄存器,r0~r14寄存器均可以作為目標地址寄存器。
注意
如果在此指令中使用r15作為目的寄存器,指令的執(zhí)行結果不可預知。
此指令只在ARMv5版本以上指令集中被支持。
指令操作的偽代碼為:
LR=(addressoftheinstructionafterthisBLX)|1
TFlag=Rm[0]
PC=Rm[31:1]<<1
(3)匯編偽指令
匯編編譯器可以產(chǎn)生ARM代碼也可以產(chǎn)生Thumb代碼。使用--thumb或--16選項指示編譯器產(chǎn)生Thumb代碼。由于所有支持Thumb代碼的ARM處理器都從ARM狀態(tài)開始執(zhí)行,所以必須人為地使用BX或BLX指令,使處理器狀態(tài)切換到Thumb狀態(tài)并使用下面的偽操作使編譯器產(chǎn)生Thumb代碼。
·CODE16
·CODE32
CODE32偽操作指示匯編器后面的指令為32位的ARM指令。
ARM和CODE32偽操作的意義相同。
當匯編器對源程序進行編譯時,如果需要,將會在程序中插入空指令,以保證內(nèi)存單元字對齊。
語法格式如下。
ARM
CODE32
使用在同時包含ARM指令和Thumb指令的源文件中。當需要從ARM指令序列切換到Thumb指令序列時,使用偽操作ARM(或CODE32);當需要從Thumb指令序列切換到ARM指令序列時使用Thumb偽操作。ARM(或CODE32)偽操作只是指示匯編器后面的指令類型是ARM指令。該偽操作本身并不進行程序狀態(tài)的切換,要進行狀態(tài)切換,可以使用BX指令操作。
CODE16偽指令通知編譯器,其后的指令序列為16位的Thumb指令。
語法格式如下。
CODE16
若在匯編源程序中同時包含ARM指令和Thumb指令時,可用CODE16偽指令通知編譯器其后的指令序列為16位的Thumb指令。
(4)編程實例
PRESERVE8
AREAAddReg,CODE,READONLY ;段名為AddReg,屬性為READONLY
ENTRY ;程序入口
;SECTION1
main
ADRr0,ThumbProg+1 ;確定跳轉地址
;并將bit[0]置1
;使程序切換到thumb狀態(tài)
BXr0 ;程序跳轉并執(zhí)行狀態(tài)切換
;SECTION2
CODE16 ;Thumb代碼指示偽操作
ThumbProg
MOVr2,#2 ;r2=2
MOVr3,#3 ;r2=3
ADDr2,r2,r3 ;r2=r2+r3
ADRr0,ARMProg
BXr0 ;程序跳轉并執(zhí)行狀態(tài)切換
;SECTION3
CODE32 ;ARM代碼指示偽操作
ARMProg
MOVr4,#4
MOVr5,#5
ADDr4,r4,r5
;SECTION4
stopMOVr0,#0x18 ;設置semihosting軟中斷號
LDRr1,=0x20026 ;ADP_Stopped_ApplicationExit
SWI0x123456 ;ARMsemihostingSWI軟中斷調(diào)用
END ;文件結束
上面的例子分為4部分,通過下面的步驟編譯和運行。
①使用文本編輯器,如notepad,輸入上面的代碼,并保存成文件addreg.s;
②在命令行中鍵入?yún)R編命令armasm–gaddreg.s;
③在命令行中鍵入鏈接命令armlinkaddreg.o-oaddreg;
④使用調(diào)試器(Debugger)(如,RealViewDebuggerorAXD)運行映像文件??梢允褂脝尾綀?zhí)行,觀察代碼在Thumb狀態(tài)下的執(zhí)行。
注意
Thumb代碼的地址標號如果用偽操作export聲明為“外部的”,則連接器會自動調(diào)整該地址標號使其bit[0]等于1;如果該地址標號沒有被聲明為“外部的”,則使用者必須手動地對標號進行調(diào)整,如上例中的ThumProg+1。
(5)ARMv5架構下的狀態(tài)切換
在ARMv5體系結構的指令集中,增加了下面兩條指令用于ARM和Thumb代碼互交。
·BLXaddress
該指令跳轉到指令中指定的地址處執(zhí)行程序并進行程序狀態(tài)切換,該地址是“PC相關的”,地址范圍為±32MB(ARM狀態(tài))或±4MB(Thumb狀態(tài))。
·BLXregister
在該中格式的跳轉指令中寄存器Rm指定轉移目標,Rm的第0位拷貝到CPSR中的T位,bit[31:0]移入PC。
使用上面兩條指令,在執(zhí)行程序跳轉之前,處理器自動將返回連接寄存器LR的bit[0]位更新為CPSR寄存器的T位,所以無論處理器狀態(tài)是否發(fā)生變化,程序都能正確返回。
當使用LDR、LDM及POP指令向PC寄存器中賦值時,寄存器CPSR中的Thumb位將被設置成PC寄存器的bit[0],這時就實現(xiàn)了程序狀態(tài)的切換。這種方法在子程序的返回時非常有效,同樣的指令可以根據(jù)需要返回到ARM狀態(tài)或Thumb狀態(tài)。
連接器在對目標代碼進行連續(xù)時,將代碼中的地址標號分為3類。
·ARM指令地址標號。
·Thumb指令地址標號。
·數(shù)據(jù)(Data)地址標號。
當連接器重定位Thumb代碼中的地址標號時,地址標號的bit[0]位將被自動設置為1。這就意味著跳轉指令(這些指令包括BX、BLX和LDR)可以根據(jù)目標地址正確的進行狀態(tài)切換。
注意
上面提到的連接器自動設置目的地址的行為,只有在ARMv5及其以上版本中支持。
2.使用C和C++語言實現(xiàn)互交對于不同的C和C++源程序,可以有些程序中包含ARM指令,有些程序中包含Thumb指令,這些程序可以相互調(diào)用,只是在編譯這些程序時指定--apcs/interwork選項。當使用了--apcs/interwork選項,編譯器會自動進行一些相應處理;連接器在檢測到程序中存在互交工作時,會生成一些用于程序狀態(tài)切換的代碼。
(1)代碼編譯
可以使用下面的指令,將C或C++程序編譯為可以執(zhí)行互交的目標代碼。
armcc--c90--thumb--apcs/interwork
armcc--c90--arm--apcs/interwork
armcc--cpp--thumb--apcs/interwork
armcc--cpp--arm--apcs/interwork
注意
--cpp是C++文件(文件后綴為.cpp)默認的編譯選項。
使用--apcs/interwork選項對文件進行編譯時,編譯器會進行如下處理。
·對于葉子程序(leaffunction,即程序中沒有其他子程序調(diào)用的程序),編譯器將程序中的“MOVPC,LR”指令替換成“BXLR”指令,因為“MOVPC”指令不能進行狀態(tài)切換。
·對于非葉子程序,要進行一系列的指令替換,如:
POP{r4,r5,pc}
替換為:
POP{r4,r5}
POP{r3}
BXr3
下面的例子顯示了一段帶子程序調(diào)用的C語言程序,使用--apcs/interwork選項進行編譯時,對代碼產(chǎn)生的影響。
C語言源程序。
Voidfunc(void)
{
….
Sub()
..
}
使用armcc--apcs/interwork選項進行編譯產(chǎn)生結果如下。
Func
STMFDsp!,{r4-r7,lr}
….
BLsub
….
LDMFDsp!,{r4-r7,lr}
BXlr
使用tcc--apcs/interwork選項進行編譯產(chǎn)生結果如下。
PUSH{r4-r7,lr}
….
BLsub
….
POP{r4-r7}
POP{r3}
BX
(2)C語言的互交實例
下面的例子顯示了一個Thumb狀態(tài)下的代碼通過互交調(diào)用ARM子程序;而后又在ARM子程序中調(diào)用Thumb指令集的庫函數(shù)printf()。
/*********************
*thumbmain.c*
**********************/
#include<stdio.h>
externvoidarm_function(void);
intmain(void)
{
printf("HellofromThumbWorld\n");
arm_function();
printf("AndgoodbyefromThumbWorld\n");
return(0);
}
/*********************
*armsub.c*
**********************/
#include<stdio.h>
voidarm_function(void)
{
printf("HelloandGoodbyefromARMworld\n");
}
使用下面的命令對程序進行編譯連接。
①編譯生成帶互交的Thumb代碼。
armcc--thumb-c-g--apcs/interwork-othumbmain.othumbmain.c
②編譯生成帶互交的ARM代碼。
armcc-c-g--apcs/interwork-oarmsub.oarmsub.c
③連接目標文件。
armlinkthumbmain.oarmsub.o-othumbtoarm.axf
另外,可以使用--info選項使連接器輸出由于互交所增加的代碼大小。
armlinkarmsub.othumbmain.o-othumbtoarm.axf--infoveneers
輸出信息如下所示。
AddingVeneerstotheimage
AddingTAveneer(4bytes,Inline)forcallto'arm_function'fromthumbmain.o(.text).
AddingATveneer(8bytes,Inline)forcallto'__0printf'fromarmsub.o(.text).
AddingATveneer(8bytes,Inline)forcallto'__rt_lib_init'fromkernel.o(.text).
AddingATveneer(12bytes,Long)forcallto'__rt_lib_shutdown'fromkernel.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'__rt_memclr_w'fromstdio.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'__rt_raise'fromstdio.o(.text).
AddingTAveneer(8bytes,Short)forcallto'__rt_exit'fromexit.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'__user_libspace'fromfree.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'_fp_init'fromlib_init.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'__heap_extend'frommalloc.o(.text).
AddingATveneer(8bytes,Inline)forcallto'__raise'fromrt_raise.o(.text).
AddingTAveneer(4bytes,Inline)forcallto'__rt_errno_addr'fromftell.o(.text).
12Veneer(s)(total72bytes)addedtotheimage.
(3)Thumb狀態(tài)下的功能指針
任何指向Thumb函數(shù)(由Thumb指令完成的功能函數(shù)并且其返回狀態(tài)也為Thumb狀態(tài))的指針,其最低有效位(LSB)必為1。
當重定位Thumb代碼中的地址標號時,連接器將自動設置地址的最低有效位。如果在程序中使用絕對地址,連接器將無法完成該設置。因此,如果在Thumb代碼中使用絕對地址時,必須手工設置為其地址加1。
下面的例子顯示了Thumb代碼的功能指針的使用。
typedefint(*FN)();
myfunc(){
FNfnptrs[]={
(FN)(0x8084+1), //有效的Thumb地址
(FN)(0x8074) //無效的Thumb地址
};
FN*myfunctions=fnptrs;
myfunctions[0](); //調(diào)用成功
myfunctions[1](); //調(diào)用失敗
}