混合使用C、C++和匯編語之:內(nèi)聯(lián)匯編和嵌入型匯編的使用
內(nèi)聯(lián)匯編和嵌入型匯編是包含在C' target='_blank' style='cursor:pointer;color:#D05C38;text-decoration:underline;'>C/C++編譯器中的匯編器。使用它可以在C/C++程序中實(shí)現(xiàn)C/C++語言不能完成的一些工作。例如,在下面幾種情況中必須使用內(nèi)聯(lián)匯編或嵌入型匯編。
·程序中使用飽和算術(shù)運(yùn)算(Saturatingarithmetic),如SSAT16和USAT16指令。
·程序中需要對協(xié)處理器進(jìn)行操作。
·在C或C++程序中完成對程序狀態(tài)寄存器的操作。
使用內(nèi)聯(lián)匯編編寫的程序代碼效率也比較高。
12.1.1內(nèi)聯(lián)匯編1.內(nèi)聯(lián)匯編語法內(nèi)聯(lián)匯編使用“_asm”(C++)和“asm”(C和C++)關(guān)鍵字聲明,語法格式如下所示。
·__asm("instruction[;instruction]"); //必須為單條指令
__asm{instruction[;instruction]}
·__asm{
...
instruction
...
}
·asm("instruction[;instruction]"); //必須為單條指令
asm{instruction[;instruction]}
·asm{
...
instruction
...
}
內(nèi)聯(lián)匯編支持大部分的ARM指令,但不支持帶狀態(tài)轉(zhuǎn)移的跳轉(zhuǎn)指令,如BX和BLX指令,詳見ARM相關(guān)文檔。
由于內(nèi)聯(lián)匯編嵌入在C或C++程序中,所有在用法上有其自身的一些特點(diǎn)。
①如果同一行中包含多條指令,則用分號(hào)隔開。
②如果一條指令不能在一行中完成,使用反斜杠“/”將其連接。
③內(nèi)聯(lián)匯編中的注釋語句可以使用C或C++風(fēng)格的。
④匯編語言中使用逗號(hào)“,”作為指令操作數(shù)的分隔符,所以如果在C語言中使用逗號(hào)必須用圓括號(hào)括起來。如,__asm{ADDx,y,(f(),z)}。
⑤內(nèi)聯(lián)匯編語言中的寄存器名被編譯器視為C或C++語言中的變量,所以內(nèi)聯(lián)匯編中出現(xiàn)的寄存器名不一定和同名的物理寄存器相對應(yīng)。這些寄存器名在使用前必須聲明,否則編譯器將提示警告信息。
⑥內(nèi)聯(lián)匯編中的寄存器(除程序狀態(tài)寄存器CPSR和SPSR外)在讀取前必須先賦值,否則編譯器將產(chǎn)生錯(cuò)誤信息。下面的例子顯示了內(nèi)聯(lián)匯編和真正匯編的區(qū)別。
錯(cuò)誤的內(nèi)聯(lián)匯編函數(shù)如下所示。
intf(intx)
{
__asm
{
STMFDsp!,{r0} //保存r0不合法,因?yàn)樵谧x之前沒有對寄存器寫操作
ADDr0,x,1
EORx,r0,x
LDMFDsp!,{r0} //不需要恢復(fù)寄存器
}
returnx;
}
將其進(jìn)行改寫,使它符合內(nèi)聯(lián)匯編的語法規(guī)則。
intf(intx)
{
intr0;
__asm
{
ADDr0,x,1
EORx,r0,x
}
returnx;
}
下面通過幾個(gè)例子進(jìn)一步了解內(nèi)聯(lián)匯編的語法。
①字符串拷貝
下面的例子使用一個(gè)循環(huán)完成了字符串的拷貝工作。
#include<stdio.h>
voidmy_strcpy(constchar*src,char*dst)
{
intch;
__asm
{
loop:
LDRBch,[src],#1
STRBch,[dst],#1
CMPch,#0
BNEloop
}
}
intmain(void)
{
constchar*a="Helloworld!";
charb[20];
my_strcpy(a,b);
printf("Originalstring:'%s'\n",a);
printf("Copiedstring:'%s'\n",b);
return0;
}
②中斷使能
下面的例子通過讀取程序狀態(tài)寄存器CPSR并設(shè)置它的中斷使能位bit[7]來禁止/打開中斷。需要注意的是,該例只能運(yùn)行在系統(tǒng)模式下,因?yàn)橛脩裟J绞菬o權(quán)修改程序狀態(tài)寄存器的。
__inlinevoidenable_IRQ(void)
{
inttmp;
__asm
{
MRStmp,CPSR
BICtmp,tmp,#0x80
MSRCPSR_c,tmp
}
}
__inlinevoiddisable_IRQ(void)
{
inttmp;
__asm
{
MRStmp,CPSR
ORRtmp,tmp,#0x80
MSRCPSR_c,tmp
}
}
intmain(void)
{
disable_IRQ();
enable_IRQ();
}
③分隔符的計(jì)算
下面的例子計(jì)算兩個(gè)整數(shù)數(shù)組中分隔符“,”的個(gè)數(shù)。該例子顯示了如何在內(nèi)聯(lián)匯編中訪問C或C++語言中的數(shù)據(jù)類型。該例中的內(nèi)聯(lián)匯編函數(shù)mlal()被編譯器優(yōu)化為一條SMLAL指令,可以使用-S–interleave編譯選項(xiàng)使編譯器輸出匯編結(jié)果。
#include<stdio.h>
/*changewordorderifbig-endian*/
#definelo64(a)(((unsigned*)&a)[0]) /*longlong型的低32位*/
#definehi64(a)(((int*)&a)[1]) /*longlong型的高32位*/
__inline__int64mlal(__int64sum,inta,intb)
{
#if!defined(__thumb)&&defined(__TARGET_FEATURE_MULTIPLY)
__asm
{
SMLALlo64(sum),hi64(sum),a,b
}
#else
sum+=(__int64)a*(__int64)b;
#endif
returnsum;
}
__int64dotprod(int*a,int*b,unsignedn)
{
__int64sum=0;
do
sum=mlal(sum,*a++,*b++);
while(--n!=0);
returnsum;
}
inta[10]={1,2,3,4,5,6,7,8,9,10};
intb[10]={10,9,8,7,6,5,4,3,2,1};
intmain(void)
{
printf("Dotproduct%lld(shouldbe%d)\n",dotprod(a,b,10),220);
return0;
}
2.內(nèi)聯(lián)匯編中的限制可以在內(nèi)聯(lián)匯編代碼中執(zhí)行的操作有許多限制。這些限制提供安全的方法,并確保在匯編代碼中不違反C和C++代碼編譯中的假設(shè)。
①不能直接向程序計(jì)數(shù)器PC賦值。
②內(nèi)聯(lián)匯編不支持標(biāo)號(hào)變量。
③不能在程序中使用“.”或{PC}得到當(dāng)前指令地址值。
④在16進(jìn)制常量前加“0x”代替“&”。
⑤建議不要對堆棧進(jìn)行操作。
⑥編譯器可能會(huì)使用r12和r13寄存器存放編譯的中間結(jié)果,在計(jì)算表達(dá)式值時(shí)可能會(huì)將寄存器r0~r3、r12及r14用于子程序調(diào)用。另外在內(nèi)聯(lián)匯編中設(shè)置程序狀態(tài)寄存器CPSR中的標(biāo)志位NZCV時(shí),要特別小心,內(nèi)聯(lián)匯編中的設(shè)置很可能會(huì)和編譯器計(jì)算的表達(dá)式的結(jié)果沖突。
⑦用內(nèi)聯(lián)匯編代碼更改處理器模式是可能的。然而,更改處理器模式會(huì)禁止使用C或C++操作數(shù)或禁止對已編譯C或C++代碼的調(diào)用,直到將處理器模式更改回原設(shè)置之后之前的函數(shù)庫才可正常使用。
⑧為Thumb狀態(tài)編譯C或C++時(shí),內(nèi)聯(lián)匯編程序不可用且不匯編Thumb指令。
⑨盡管可以使用通用協(xié)處理器指令指定VFP或FPA指令,但內(nèi)聯(lián)匯編程序不為它們提供直接支持。
不能用內(nèi)聯(lián)匯編代碼更改VFP向量模式。內(nèi)聯(lián)匯編可包含浮點(diǎn)表達(dá)式操作數(shù),該操作數(shù)可使用編譯程序生成的VFP代碼求出操作數(shù)值。因此,僅由編譯程序修改VFP狀態(tài)很重要。
⑩內(nèi)嵌匯編不支持的指令有BX、BLX、BXJ和BKPT指令。而LDM、STM、LDRD和STRD指令可能被等效為ARMLDR或STR指令。
3.內(nèi)聯(lián)匯編中的虛擬寄存器內(nèi)聯(lián)匯編程序提供對ARM處理器物理寄存器的非直接訪問。如果在內(nèi)聯(lián)匯編程序指令中將某個(gè)ARM寄存器用作操作數(shù),它就成為相同名稱的虛擬寄存器的引用,而不是對實(shí)際物理ARM寄存器的引用。例如內(nèi)聯(lián)匯編指令中使用了寄存器r0,但對于C編譯器,指令中出現(xiàn)的r0只是一個(gè)變量,并非實(shí)際的物理寄存器r0,當(dāng)程序運(yùn)行時(shí),可能是由物理寄存器r1來存放r0所代表的值。
下面的例子顯示了編譯器如何對內(nèi)聯(lián)匯編指令的寄存器進(jìn)行分配。
程序的源代碼如下。
#include<stdio.h>
voidtest_inline_register(void)
{
inti;
intr5,r6,r7;
__asm
{
MOVi,#0
loop:
MOVr5,#0
MOVr6,#0
MOVr7,#0
ADDi,i,#1
CMPi,#3
BNEloop
}
}
intmain(void)
{
test_inline_register();
printf("testinlineregister\n");
return0;
}
由C編譯器編譯出的匯編碼如下所示。
test_inline_register:
0000807CE3A00000MOVr0,#0
>>>TEST_INLINE_REGISTER\#12loop:
00008080E1A00000NOP
>>>TEST_INLINE_REGISTER\#13MOVr5,#0
00008084E3A01000MOVr1,#0
>>>TEST_INLINE_REGISTER\#14MOVr6,#0
00008088E3A02000MOVr2,#0
>>>TEST_INLINE_REGISTER\#15MOVr7,#0
0000808CE3A03000MOVr3,#0
>>>TEST_INLINE_REGISTER\#16ADDi,i,#1
00008090E2800001ADDr0,r0,#1
>>>TEST_INLINE_REGISTER\#17CMPi,#3
00008094E3500003CMPr0,#3
000080980A000000BEQ0x80a0<TEST_INLINE_REGISTER\#21>
>>>TEST_INLINE_REGISTER\#18BNEloop
0000809CEAFFFFF8B0x8084<TEST_INLINE_REGISTER\#13>
>>>TEST_INLINE_REGISTER\#21}
000080A0E12FFF1EBXr14
>>>TEST_INLINE_REGISTER\#25{
注意
下面的代碼是由Realview2.2編譯出的代碼,使用其他編譯器結(jié)果可能有差異。同一段內(nèi)嵌匯編經(jīng)過不同版本的編譯器編譯后,在指令里可能使用不一樣的實(shí)際寄存器,但是只要遵循文檔里的編碼指導(dǎo),執(zhí)行的功能肯定相同。
例子中以“>>>”的開頭的行是程序的源碼部分,緊接其后的是由編譯器編譯出的匯編代碼。從上例可以很清楚地看出,源程序中使用了r5、r6和r7,但由編譯器編譯后的代碼使用了寄存器r1、r2和r3。
另外,需要特別指出的是在內(nèi)聯(lián)匯編中使用寄存器必須先聲明其變量類型,如上例中的“intr5,r6,r7”。如果不在使用前進(jìn)行聲明,編譯器將給出以下錯(cuò)誤信息。
#1267-D:ImplicitphysicalregisterR3shouldbedefinedasavariable
編譯程序定義的虛擬寄存器有函數(shù)局部作用范圍,即在同一個(gè)C函數(shù)中,涉及相同虛擬寄存器名稱的多個(gè)asm語句或聲明,訪問相同的虛擬寄存器。
內(nèi)聯(lián)匯編沒有為pc(r15)、lr(r14)和sp(r13)寄存器創(chuàng)建虛擬寄存器,而且不能在內(nèi)聯(lián)匯編代碼中讀取或直接修改它們的值。如果內(nèi)聯(lián)匯編程序中出現(xiàn)了對這些寄存器的訪問,編譯器將給出以下錯(cuò)誤消息。例如,如果指定r14:
#20:identifier"r14"isundefined
內(nèi)聯(lián)匯編可以直接使用CPSR和SPSR對程序狀態(tài)字進(jìn)行操作,因?yàn)閮?nèi)聯(lián)匯編中不存在虛擬處理器狀態(tài)寄存器(PSR)。任何對PSR的引用總是指向物理PSR。
4.內(nèi)聯(lián)匯編中的指令展開內(nèi)聯(lián)匯編代碼中的ARM指令可能會(huì)在編譯過程中擴(kuò)展為幾條指令。擴(kuò)展取決于指令、指令中指定的操作數(shù)個(gè)數(shù)以及每個(gè)操作數(shù)的類型和值。通常,被擴(kuò)展的指令有以下兩種情況:
·含有常數(shù)操作的指令;
·LDM、STM、LDRD和STRD指令;
·乘法指令MUL被擴(kuò)展為一系列的加法和移位指令。
下面的例子說明了編譯器如何對含有常數(shù)操作的指令進(jìn)行擴(kuò)展。
包含有常數(shù)操作的加法指令:
ADDr0,r0,#1023
被編譯器編譯為如下兩條指令:
ADDr0,r0,#1024
SUBr0,r0,#1
注意
擴(kuò)展指令對程序狀態(tài)寄存器CPSR的影響:算術(shù)指令影響相應(yīng)的NZCV標(biāo)準(zhǔn)位;其他指令設(shè)置NZ標(biāo)志位不影響V標(biāo)志位。
所有的LDM和STM指令被擴(kuò)展為等效的LDR和STR指令序列。然而,在優(yōu)化過程中,編譯程序可能因此將單獨(dú)的指令重組為一條LDM或STM指令。
5.內(nèi)聯(lián)匯編中的常數(shù)指令中的標(biāo)志符“#”是可選的(前面的例子中,指令中常數(shù)前均加了標(biāo)志符“#”)。如果在指令中使用了“#”,則其后的表達(dá)式必為常數(shù)。
6.內(nèi)聯(lián)匯編指令對標(biāo)志位的影響內(nèi)聯(lián)匯編指令可能顯式或隱式地更新處理器程序狀態(tài)寄存器的條件標(biāo)志位。在僅包含虛擬寄存器操作數(shù)或簡單表達(dá)式操作數(shù)的內(nèi)聯(lián)匯編中,其執(zhí)行結(jié)果是可以預(yù)見。如果指令中指定了隱式或顯式更新條件標(biāo)志位,則條件標(biāo)志位根據(jù)指令的執(zhí)行進(jìn)行設(shè)置。如果未指定更新,則條件標(biāo)志不會(huì)更改。如果內(nèi)嵌匯編指令的操作數(shù)都不是簡單操作數(shù)時(shí)或指令不顯式更新條件標(biāo)志位,則條件標(biāo)志位可能會(huì)被破壞。一般情況下,編譯程序不易診斷出對條件標(biāo)志的潛在破壞。然而,在構(gòu)造析構(gòu)C++臨時(shí)函數(shù)的操作數(shù)時(shí),如果指令試圖更新條件標(biāo)志,編譯程序?qū)⒔o予警告,因?yàn)槲鰳?gòu)函數(shù)可能會(huì)破壞條件標(biāo)志位。
7.內(nèi)聯(lián)匯編指令中的操作數(shù)內(nèi)聯(lián)匯編指令中的操作數(shù)分為以下4種。
·虛擬寄存器
·表達(dá)式操作數(shù)
·寄存器列表
·中間操作數(shù)
(1)虛擬寄存器
在內(nèi)聯(lián)匯編指令中指定的寄存器表示虛擬寄存器而不是實(shí)際的物理寄存器。由編譯器編譯的匯編代碼中使用的物理寄存器可能與在指令中指定的不同。每個(gè)虛擬寄存器的初值是不可預(yù)測的,必須在讀取之前將初值寫入虛擬寄存器。如果在寫入之前試圖讀虛擬寄存器,編譯程序會(huì)給予警告。
(2)表達(dá)式操作數(shù)
在內(nèi)聯(lián)匯編指令中,可將函數(shù)自變量、C或C++變量和其他C或C++表達(dá)式指定為寄存器操作數(shù)。用作操作數(shù)的表達(dá)式必須為整數(shù)類型,如char、short、int或long,(長整型longlong除外)或指針類型。當(dāng)表達(dá)式作為內(nèi)聯(lián)匯編指令的操作數(shù)時(shí),編譯器在編譯時(shí)自動(dòng)增加一段代碼計(jì)算表示式的值并將其加載到指定的寄存器中。
注意
數(shù)據(jù)類型中除char和short(默認(rèn)為無符號(hào)類型)外,其他均為有符號(hào)類型。
下面的例子顯示了編譯器如何處理內(nèi)聯(lián)匯編中的表達(dá)式操作數(shù)。
程序源代碼如下所示。
/*ExampleOperands*/
voidmy_operand(void)
{
inti,j,total;
__asm
{
movi,#0
movj,#1
addtotal,j,i+j
}
}
intmain(void)
{
my_operand();
}
由編譯器編譯出的匯編代碼如下所示(其中只列出了內(nèi)聯(lián)匯編的一段代碼)。
my_operand:
0000807CE3A01000MOVr1,#0
>>>OPERANDS\#12movj,#1
00008080E3A00001MOVr0,#1
00008084E0812000ADDr2,r1,r0
>>>OPERANDS\#13addtotal,j,i+j
00008088E0803002ADDr3,r0,r2
>>>OPERANDS\#15}
0000808CE12FFF1EBXr14
>>>OPERANDS\#19{
從編譯的代碼可以看出,編譯器將“addtotal,j,i+j”分為兩步來完成,用戶在編寫自己的內(nèi)聯(lián)匯編應(yīng)用程序時(shí)要特別注意這一點(diǎn)。
包含多個(gè)表達(dá)式操作數(shù)的指令,沒有指定表達(dá)式操作數(shù)求值的順序。
將C或C++表達(dá)式用作內(nèi)聯(lián)匯編程序操作數(shù),如果表達(dá)式的值不能滿足ARM指令中所要求的指令操作數(shù)約束條件,一條指令將被擴(kuò)展為多條指令。
如果用作操作數(shù)的表達(dá)式創(chuàng)建需要析構(gòu)的臨時(shí)函數(shù),析構(gòu)將發(fā)生在執(zhí)行內(nèi)聯(lián)匯編指令之后,這與C++析構(gòu)臨時(shí)函數(shù)的規(guī)則相類似。
簡單表達(dá)式操作數(shù)包含以下幾種類型。
·變量值
·變量地址
·指針變量的反引用(thedereferencingofapointvarable)
·偽操作指定的程序常量
非簡單表達(dá)式操作數(shù)包含以下幾種類型。
·隱式函數(shù)調(diào)用,如除法,或顯式函數(shù)調(diào)用
·C++臨時(shí)函數(shù)的構(gòu)造
·算術(shù)或邏輯操作
(3)寄存器列表
寄存器列表最多可包含16個(gè)操作數(shù)。這些操作數(shù)可以是虛擬寄存器或表達(dá)式操作數(shù)。在寄存器列表中指定虛擬寄存器和表達(dá)式操作數(shù)的順序非常重要。寄存器列表中操作數(shù)的讀寫順序是從左到右。第一個(gè)操作數(shù)使用最低地址,隨后的操作數(shù)的地址依次在前一地址基礎(chǔ)上增加4。這一點(diǎn)與LDM或STM指令的普通操作(編號(hào)最低的物理寄存器總是存入最低的存儲(chǔ)器地址)是不同的。之所以存在這種區(qū)別是因?yàn)樵趦?nèi)聯(lián)匯編中使用的寄存器被編譯器虛擬化了。
同一個(gè)表達(dá)式操作數(shù)或虛擬寄存器可以在寄存器列表中出現(xiàn)多次,重復(fù)使用。
如果表達(dá)式操作數(shù)或虛擬寄存器被指定為指令中的基址寄存器,表達(dá)式或虛擬寄存器的值按照ARM指令尋址方式進(jìn)行更新。更新將覆蓋原表達(dá)式或虛擬寄存器的值。
(4)中間操作數(shù)(Intermediateoperands)
在內(nèi)聯(lián)匯編指令中,可能將C或C++整型常量表達(dá)式用作立即數(shù)處理。用于指定直接移位的常量表達(dá)式的值必須在ARM指令規(guī)定的移位操作數(shù)的范圍內(nèi);用于為存儲(chǔ)器或協(xié)處理器數(shù)據(jù)傳送指令指定直接偏移量的常量表達(dá)式,必須符合ARM體系結(jié)構(gòu)中的內(nèi)存對齊標(biāo)準(zhǔn)。
8.函數(shù)調(diào)用和分支跳轉(zhuǎn)利用內(nèi)聯(lián)匯編程序的BL和SWI指令可在常規(guī)指令字段后指定3個(gè)可選列表。這些指令格式有以下幾種。
SWI{cond}swi_num,{input_param_list},{output_value_list},{corrupt_reg_list}
BL{cond}function,{input_param_list},{output_value_list},{corrupt_reg_list}
其中,swi_num為SWI調(diào)用的中斷號(hào);function為被調(diào)用函數(shù)名;{input_param_list}為輸入?yún)?shù)列表;{output_value_list}為輸出參數(shù)列表;{corrupt_reg_list}為被破壞寄存器列表。
注意
內(nèi)聯(lián)匯編程序不支持BX、BLX和BXJ指令。不能在任何輸入、輸出或“被破壞的寄存器列表(corruptedregisterlist)”中指定lr、sp或pc寄存器;任何SWI指令或函數(shù)調(diào)用不能更改sp寄存器。
下面分別詳細(xì)介紹語法格式中各參數(shù)的使用。
(1)未指定任何列表
如果在SWI和BL指令后沒指定任何列表,則有下面規(guī)則。
·r0~r3用作輸入?yún)?shù);
·r0用于輸出值;
·r12和r14的值將會(huì)被修改。
(2)輸入?yún)?shù)列表
指令中的輸入?yún)?shù)列表{input_param_list}列出了傳遞給被調(diào)用函數(shù)function和SWI的參數(shù)。被傳遞的參數(shù)可以是表達(dá)式、變量或包含表達(dá)式或變量的物理寄存器。
內(nèi)聯(lián)匯編編譯器在編譯時(shí)增加一小段編譯程序負(fù)責(zé)在函數(shù)和SWI調(diào)用前
將傳遞的參數(shù)載入特定的物理寄存器中。為確保與現(xiàn)有內(nèi)聯(lián)匯編代碼的向后兼容性,程序中指定物理寄存器名稱而并不對其賦值,使相同名稱虛擬寄存器中的值出現(xiàn)在物理寄存器中。
例如,指令BLfoo{r0=expression1,r1=expression2,r2}生成以下偽代碼:
MOV(physical)r0,expression1
MOV(physical)r1,expression2
MOV(physical)r2,(virtual)r2
BLfoo
(3)輸出參數(shù)列表
輸出參數(shù)列表{output_value_list}列出了用來存放功能函數(shù)和SWI調(diào)用返回值的寄存器或表達(dá)式。列表中的值可以是物理寄存器、可修改長值表達(dá)式或單個(gè)物理寄存器名稱。
內(nèi)聯(lián)匯編程序從特定的物理寄存器中取值并賦值到特定的表達(dá)式中。指定物理寄存器名稱而并不賦值,導(dǎo)致相同名稱虛擬寄存器被物理寄存器中的值更新。
例如,BLfoo{},{result1=r0,r1}生成以下偽碼:
BLfoo
MOVresult1,(physical)r0
MOV(virtual)r1,(physical)r1
(4)被破壞的寄存器列表(Corruptedregisterlist)
此列表指定被函數(shù)調(diào)用破壞的物理寄存器。如果條件標(biāo)志被調(diào)用的函數(shù)修改,必須在被破壞的寄存器列表中指定PSR。
BL和SWI指令總是破壞lr。
如果指令中缺少此列表項(xiàng),則r0~r3、ip、lr和PSR被破壞。
注意
指令BL和B的區(qū)別在于,跳轉(zhuǎn)指令B只能使程序跳轉(zhuǎn)到C或C++程序的一個(gè)地址標(biāo)號(hào),不能用于子程序調(diào)用。
9.內(nèi)嵌匯編中的標(biāo)號(hào)內(nèi)聯(lián)匯編代碼中定義的標(biāo)號(hào)可被用作跳轉(zhuǎn)或C和C++“goto”語句的目標(biāo)。在內(nèi)聯(lián)匯編代碼中,C和C++中定義的標(biāo)號(hào)可被用作跳轉(zhuǎn)指令的目標(biāo)。
10.內(nèi)嵌匯編器版本間的差異不同版本的ARM編譯器對內(nèi)聯(lián)匯編程序的語法要求有顯著差異。在具體使用時(shí)請參見相關(guān)文檔。
·如果使用的是ADSv1.2,請參閱ADS開發(fā)者指南;
·如果使用的是RVCTv1.2,請參閱RealView編譯工具1.2版開發(fā)者指南。
12.1.2嵌入式匯編利用ARM編譯器可將匯編代碼包括到一個(gè)或多個(gè)C或C++函數(shù)定義中去。嵌入式匯編器提供對目標(biāo)處理器不受限制的低級別訪問,利用它可以使用C和C++預(yù)處理程序偽操作(preprocessordirective)并可以方便的使用偏移量訪問結(jié)構(gòu)成員。
本小節(jié)將介紹以下內(nèi)容:
·嵌入式匯編程序語法;
·嵌入式匯編語句的限制;
·嵌入式匯編程序表達(dá)式和C或C++表達(dá)式之間的差異;
·嵌入式匯編函數(shù)的生成;
·__cpp關(guān)鍵字;
·手動(dòng)重復(fù)解決方案;
·相關(guān)基類的關(guān)鍵字;
·成員函數(shù)類的關(guān)鍵字;
·調(diào)用非靜態(tài)成員函數(shù)。
有關(guān)為ARM處理器編寫匯編語言的詳細(xì)信息,請參閱ADS或RealView編譯工具的匯編程序指南。
1.嵌入式匯編語言語法嵌入式匯編函數(shù)定義由--asm(C和C++)或asm(C++)函數(shù)限定符標(biāo)記,可用于:
·成員函數(shù);
·非成員函數(shù);
·模板函數(shù);
·模板類成員函數(shù)。
用__asm或asm聲明的函數(shù)可以有調(diào)用參數(shù)和返回類型。它們從C和C++中調(diào)用的方式與普通C和C++函數(shù)調(diào)用方式相同。嵌入式匯編函數(shù)語法是:
__asmreturn-typefunction-name(parameter-list)
{
//ARM/Thumb/Thumb-2assemblercode
instruction[;instruction]
...
[instruction]
}
嵌入式匯編的初始執(zhí)行狀態(tài)是在編譯程序時(shí)由編譯選項(xiàng)決定的。這些編譯選項(xiàng)如下所示:
·如果初始狀態(tài)為ARM狀態(tài),則內(nèi)嵌匯編器使用--arm選項(xiàng);
·如果初始狀態(tài)為Thumb狀態(tài),則內(nèi)嵌匯編器使用--thumb選項(xiàng)。
注意
嵌入式匯編的初始狀態(tài)由編譯器的編譯選項(xiàng)確定,與程序中的#pragmaarm和#pragmathumb偽操作無關(guān)。
可以顯示地使用ARM、THUMB和CODE16偽操作改變嵌入式匯編的執(zhí)行狀態(tài)。關(guān)于ARM偽操作的詳細(xì)信息請參加指令偽操作一節(jié)。如果使用的處理器支持Thumb-2指令,則可以在Thumb狀態(tài)下,在嵌入式匯編中使用Thumb-2指令。
參數(shù)名允許用在參數(shù)列表中,但不能用在嵌入式匯編函數(shù)體內(nèi)。例如,以下函數(shù)在函數(shù)體內(nèi)使用整數(shù)i,但在匯編中無效:
__asmintf(inti){
ADDi,i,#1//編譯器報(bào)錯(cuò)
}
可以使用r0代替i。
下面通過嵌入式匯編的例子,來進(jìn)一步熟悉嵌入式匯編的使用。
下面的例子實(shí)現(xiàn)了字符串的拷貝,注意和上一節(jié)中內(nèi)聯(lián)匯編中字符串拷貝的例子相比較,分析其中的區(qū)別。
#include<stdio.h>
__asmvoidmy_strcpy(constchar*src,constchar*dst){
loop
LDRBr3,[r0],#1
STRBr3,[r1],#1
CMPr3,#0
BNEloop
MOVpc,lr
}
voidmain()
{
constchar*a="Helloworld!";
charb[20];
my_strcpy(a,b);
printf("Originalstring:'%s'\n",a);
printf("Copiedstring:'%s'\n",b);
}
2.嵌入式匯編語言的使用限制嵌入式匯編的使用有下面一些限制。
①在預(yù)處理之后,__asm函數(shù)只能包含匯編代碼,但以下標(biāo)識(shí)符除外:
·__cpp(expr);
·__offsetof_base(D,B);
·__mcall_is_virtual(D,f);
·__mcall_is_in_vbase(D,f);
·__mcall_this_offset(D,f);
·__vcall_offsetof_vfunc(D,f);
②編譯程序不為__asm函數(shù)生成返回指令。如果要從__asm函數(shù)返回,必須將用匯編代碼編寫的返回指令包含到函數(shù)體內(nèi)。由于嵌入式匯編執(zhí)行__asm函數(shù)的順序是在編譯時(shí)定義好的,所有從一個(gè)內(nèi)嵌匯編跳轉(zhuǎn)到一個(gè)內(nèi)嵌匯編程序是運(yùn)行的,但在內(nèi)聯(lián)匯編中卻不能實(shí)現(xiàn)。
③__asm函數(shù)調(diào)用遵循AAPCS規(guī)則。所以,即使在__asm函數(shù)體內(nèi)可用的匯編代碼(例如,更改狀態(tài)),在__asm函數(shù)和普通C或C++函數(shù)相互調(diào)用時(shí),未必可用,因?yàn)榇苏{(diào)用也必須遵循AAPCS規(guī)則。
3.嵌入式匯編程序表達(dá)式和C或C++表達(dá)式之間的差異嵌入式匯編表達(dá)式和C或C++表達(dá)式之間存在以下差異。
①匯編程序表達(dá)式總是無符號(hào)的。相同的表達(dá)式在匯編程序和C或C++中有不同值。例如:
MOVr0,#(-33554432/2)//結(jié)果為0x7f000000
MOVr0,#__cpp(-33554432/2)//結(jié)果為0xff000000
②以0開頭的匯編程序編碼仍是十進(jìn)制的。例如:
MOVr0,#0700//十進(jìn)制700
MOVr0,#__cpp(0700)//八進(jìn)制0700等于十進(jìn)制448
③匯編程序運(yùn)算符優(yōu)先順序與C和C++不同。例如:
MOVr0,#(0x23:AND:0xf+1)//((0x23&0xf)+1)=>4
MOVr0,#__cpp(0x23&0xf+1)//(0x23&(0xf+1))=>0
④匯編程序字符串不是以空字符為終止標(biāo)志的:
DCB"notrailingnull"//16bytes
DCB__cpp("Ihaveatrailingnull!!")//25bytes
注意
在_cpp標(biāo)識(shí)符作用范圍之內(nèi)使用C或C++語法規(guī)則。
4.嵌入式匯編函數(shù)的生成由關(guān)鍵字__asm聲明的嵌入式匯編程序,在編譯時(shí)將作為整個(gè)文件體傳遞給ARM匯編器。傳遞過程中,__asm函數(shù)的順序保持不變(用模板實(shí)例生成的函數(shù)除外)。正是由于嵌入式匯編的這個(gè)特性,使得由一個(gè)__asm標(biāo)識(shí)的嵌入式匯編程序調(diào)用在同一文件中的另一個(gè)嵌入式匯編程序是可以實(shí)現(xiàn)的。
當(dāng)使用編譯器armcc時(shí),局部鏈接器(PartialLink)將匯編程序產(chǎn)生的目標(biāo)文件與編譯C程序的目標(biāo)文件相結(jié)合,產(chǎn)生單個(gè)目標(biāo)文件。
編譯程序?yàn)槊總€(gè)__asm函數(shù)生成AREA命令。例如,以下__asm函數(shù):
#include<cstddef>
structX{intx,y;voidaddto_y(int);};
__asmvoidX::addto_y(int){
LDRr2,[r0,#__cpp(offsetof(X,y))]
ADDr1,r2,r1
STRr1,[r0,#__cpp(offsetof(X,y))]
BXlr
}
對于此函數(shù),編譯程序生成:
AREA||.emb_text||,CODE,READONLY
EXPORT|_ZN1X7addto_yEi|
#linenum"file"
|_ZN1X7addto_yEi|PROC
LDRr2,[r0,#4]
ADDr1,r2,r1
STRr1,[r0,#4]
BXlr
ENDP
END
由上面的例子可以看出,對于變量offsetof的使用必須加__cpp()標(biāo)識(shí)符才能引用,因?yàn)樵撟兞渴窃赾stddef頭文件中定義的。
由__asm聲明的常規(guī)函數(shù)被放在名為.emb_text的段(Section)中。這一點(diǎn)也是嵌入式匯編和內(nèi)聯(lián)匯編最大的不同。相反,隱式實(shí)例模板函數(shù)(ImplicitlyInstantiatedTemplateFunction)和內(nèi)聯(lián)匯編函數(shù)放在與函數(shù)名同名的區(qū)域(Area)內(nèi),并為該區(qū)域增加公共屬性。這就確保了這類函數(shù)的特殊語義得以保持。
由于內(nèi)聯(lián)和模板函數(shù)的區(qū)域的特殊命名,所以這些函數(shù)不按照文件中定義的順序排列,而是任意排序。因此,不能以__asm函數(shù)在原文件中的排列順序,來判斷它們的執(zhí)行順序,也就是說,即使兩個(gè)連續(xù)排列的__asm函數(shù),也不一定能順序執(zhí)行。
5.關(guān)鍵字__cpp可用__cpp關(guān)鍵字從匯編代碼中訪問C或C++的編譯時(shí)常量表達(dá)式,其中包括含有外部鏈接的數(shù)據(jù)或函數(shù)地址。標(biāo)識(shí)符__cpp內(nèi)的表達(dá)式必須是適合用作C++靜態(tài)初始化的常量表達(dá)式(請參閱ISO/IEC14882:1998中的3.6.2非本地對象初始化一節(jié)和本書的常量表達(dá)式一節(jié))。
編譯時(shí),編譯器將使用__cpp(expr)的地方用匯編程序可以使用的常量所取代。例如:
LDRr0,=__cpp(&some_variable)
LDRr1,=__cpp(some_function)
BL__cpp(some_function)
MOVr0,#__cpp(some_constant_expr)
__cpp表達(dá)式中的名稱可在__asm函數(shù)的C++上下文中查閱。__cpp表達(dá)式結(jié)果中的任何名稱按照要求被損毀并自動(dòng)為其生成IMPORT語句。
6.手動(dòng)重復(fù)解決方案可以在嵌入式匯編中使用C++轉(zhuǎn)換為非虛擬函數(shù)調(diào)用解決重復(fù)。例如:
voidg(int);
voidg(long);
structT{
intmf(int);
intmf(int,int);
};
__asmvoidf(T*,int,int){
BL__cpp(static_cast<int(T::*)(int,int)>(&T::mf))//callsT::mf(int,int)
BL__cpp(static_cast<void(*)(int)>(g))//callsg(int)
MOVpc,lr
}
7.相關(guān)基類的關(guān)鍵字利用以下關(guān)鍵字可以確定從對象起始處到其相關(guān)基類的偏移量:
__offsetof_base(D,B)
其中,B必須是D的非虛擬基類。
該函數(shù)返回從D對象的起始處到其中B基子對象的起始處的偏移量。結(jié)果可能是零。必須將偏移量(以字節(jié)為單位)添加到D*p來執(zhí)行。
static_cast<B*>(p)的等效功能,如下程序段所示:
__asmB*my_static_base_cast(D*/*p*/){
if__offsetof_base(D,B)<>0 //排除偏移量為0的情況
ADDr0,r0,#__offsetof_base(D,B)
endif
MOVpc,lr
}
在匯編程序源代碼中,這些關(guān)鍵字被轉(zhuǎn)換為整數(shù)或邏輯常量。只能將它們用于__asm函數(shù),而不能用于__cpp表達(dá)式。
8.成員函數(shù)類的關(guān)鍵字以下關(guān)鍵字方便了從__asm函數(shù)中調(diào)用虛擬或非虛擬成員函數(shù)。以__mcall開頭的關(guān)鍵字可用于虛擬和非虛擬函數(shù)。以__vcall開頭的關(guān)鍵字僅能用于虛擬函數(shù)。在調(diào)用靜態(tài)成員函數(shù)的過程中,這些關(guān)鍵字沒有特別的作用。
下面詳細(xì)介紹這些關(guān)鍵字的使用。
①__mcall_is_virtual(D,f)
如果f是D中的虛擬成員函數(shù)或是D的基類,結(jié)果是{TRUE},否則結(jié)果是{FALSE}。如果返回{TRUE},可用虛擬調(diào)度進(jìn)行調(diào)用,否則必須直接進(jìn)行調(diào)用。
②__mcall_is_in_vbase(D,f)
如果f是D虛擬基類中的非靜態(tài)成員函數(shù),結(jié)果是{TRUE},否則結(jié)果是{FALSE}。如果返回{TRUE},必須用__mcall_offsetof_vbaseptr(D,f)進(jìn)行this調(diào)整,否則必須用__mcall_this_
offset(D,f)進(jìn)行調(diào)整。
③__mcall_this_offset(D,f)
其中D是類,f是D中定義的非靜態(tài)成員函數(shù)或是D的非虛擬基類。該函數(shù)返回從D對象的起始處到定義f的基的起始處的偏移量。在用指向D的指針調(diào)用f的過程中,這是必要的this調(diào)整。返回值在D中可找到f時(shí)為零,或者與__offsetof_base(D,B)相同,其中B為包含f的D非虛擬基類。在D的虛擬基類中找到f時(shí),如果使用__mcall_this_offset(D,f),則返回任意值,在程序中使用該返回值,匯編器將報(bào)告__mcall_this_offset無效使用的錯(cuò)誤。
④__vcall_offsetof_vfunc(D,f)
其中D是類,f是D中定義的虛擬函數(shù)或是D的基類。將偏移量返回到虛擬函數(shù)表,在該表中可以找到從虛擬函數(shù)表到虛擬函數(shù)的偏移量。在f不是虛擬成員函數(shù)時(shí),如果使用__vcall_offsetof_vfunc(D,f),則返回任意值,而在設(shè)計(jì)上使用該值時(shí)會(huì)導(dǎo)致匯編錯(cuò)誤。
9.調(diào)用非靜態(tài)成員函數(shù)本小節(jié)列出了可以從__asm函數(shù)中調(diào)用虛擬或非虛擬函數(shù)的關(guān)鍵字。靜態(tài)成員函數(shù)的參數(shù)不相同(沒有this),使得檢測靜態(tài)成員函數(shù)的關(guān)鍵字__mcall_is_static不可用,因此調(diào)用位置很可能已經(jīng)專用于調(diào)用靜態(tài)成員函數(shù)。
(1)調(diào)用非虛擬成員函數(shù)
例如,在虛擬基(virtualbase)或非虛擬基(non-virtualbase)中,以下代碼可用于調(diào)用虛擬函數(shù):
//rp包含指向D的指針,該程序的功能是實(shí)現(xiàn)在使用rp時(shí)調(diào)用D的非虛成員函數(shù)f
//所有參數(shù)準(zhǔn)備好
//假設(shè)并不返回一個(gè)結(jié)構(gòu)類型
if__mcall_is_in_vbase(D,f)
ASSERT{FALSE}//can'taccessvirtualbase
else
MOVr0,rp //使用指向D的指針rp*
ADDr0,r0,#__mcall_this_offset(D,f) //地址調(diào)整
endif
BL__cpp(&D::f)
(2)調(diào)用虛擬成員函數(shù)
例如,在虛擬或非虛擬基中,以下代碼可用于調(diào)用虛擬函數(shù):
//rp包含指向D的指針,該程序的功能是在使用rp時(shí)調(diào)用D的虛擬函數(shù)f
//所有參數(shù)準(zhǔn)備好
//假如函數(shù)并不返回一個(gè)結(jié)構(gòu)類型
if__mcall_is_in_vbase(D,f)
ASSERT{FALSE} //不能調(diào)用虛擬基
else
MOVr0,rp //使用指向D的指針rp
LDRr12,[rp] //加載vtable表結(jié)構(gòu)指針
ADDr0,r0,#__mcall_this_offset(D,f) //地址調(diào)整
endif
MOVlr,pc //保存返回地址到lr
LDRpc,[r12,#__vcall_offsetof_vfunc(D,f)] //調(diào)用函數(shù)rp→f()
10.嵌入式匯編版本間的差異不同版本的ARM編譯器對嵌入式匯編程序的語法要求會(huì)有所差異。在具體使用時(shí)請參見相關(guān)文檔。
值得注意的是,目前的嵌入式匯編器已經(jīng)完全支持ARMv6指令集,也就是說可以在嵌入式匯編中使用ARMv6指令集中的指令。
12.1.3內(nèi)聯(lián)匯編中使用SP、LR和PC寄存器的遺留問題雖然目前的編譯器不支持在內(nèi)聯(lián)匯編中使用SP、LR和PC寄存器,但在RVCTv1.2及其以前的編譯器版本中是允許的。下面的例子顯示了使用早期編譯器版本,在內(nèi)聯(lián)匯編中使用LR寄存器的例子。
voidfunc()
{
intvar;
__asm
{
movvar,lr/*得到func()函數(shù)的返回地址*/
}
}
如果使用RVCTv2.0編譯器編譯上面的代碼,編譯器將報(bào)告以下錯(cuò)誤。
Error:#20:identifier"lr"isundefined
使用RVCTv2.0版本及其以后的編譯器,要在C或C++代碼中使用匯編訪問SP、LR和PC寄存器可以使用下面幾種方法。
①使用嵌入式匯編代碼。嵌入式匯編支持所有的ARM指令,同時(shí)允許在代碼中訪問SP、LR和PC寄存器。
②在內(nèi)聯(lián)匯編中使用以下一些指令。
·__current_pc():訪問PC寄存器。
·__current_sp():訪問SP寄存器。
·__return_address():訪問LR,返回地址寄存器。
下面給出了兩個(gè)訪問SP、LR和PC寄存器的典型實(shí)例程序。
①使用編譯器給定的指令。
voidprintReg()
{
unsignedintspReg,lrReg,pcReg;
__asm{
MOVspReg,__current_sp()
MOVpcReg,__current_pc()
MOVlrReg,__return_address()
}
printf("SP=0x%X\n",spReg);
printf("PC=0x%X\n",pcReg);
printf("LR=0x%X\n",lrReg);
}
②使用嵌入式匯編。
__asmvoidfunc()
{
MOVr0,lr
...
BXlr
}
使用嵌入式匯編可以使用調(diào)試器捕獲程序的返回地址。
12.1.4內(nèi)聯(lián)匯編代碼與嵌入式匯編代碼之間的差異本節(jié)總結(jié)了內(nèi)聯(lián)匯編和嵌入式匯編在編譯方法上存在的差異:
·內(nèi)聯(lián)匯編代碼使用高級處理器抽象,并在代碼生成過程中與C和C++代碼集成。因此,編譯程序?qū)和C++代碼與匯編代碼一起進(jìn)行優(yōu)化。
·與內(nèi)聯(lián)匯編代碼不同,嵌入式匯編代碼從C和C++代碼中分離出來單獨(dú)進(jìn)行匯編,產(chǎn)生與C和C++源代碼編譯對象相結(jié)合的編譯對象。
·可通過編譯程序來內(nèi)聯(lián)內(nèi)聯(lián)匯編代碼,但無論是顯式還是隱式,都無法內(nèi)聯(lián)嵌入式匯編代碼。
表12.1總結(jié)了內(nèi)聯(lián)匯編程序與嵌入式匯編程序之間的主要差異。
表12.1 內(nèi)聯(lián)匯編程序與嵌入式匯編程序之間的主要差異
功能
嵌入式匯編程序
內(nèi)聯(lián)匯編程序
指令集
ARM和Thumb
僅支持ARM
ARM匯編指令偽操作
支持
不支持
ARMv6指令集
支持
僅支持媒體指令
C/C++表達(dá)式
只支持常數(shù)表達(dá)式
完全支持
匯編代碼是否優(yōu)化
無優(yōu)化
完全優(yōu)化
能否被內(nèi)聯(lián)(Inling)
不可能
有可能被內(nèi)聯(lián)
續(xù)表
功能
嵌入式匯編程序
內(nèi)聯(lián)匯編程序
寄存器訪問
使用指定的物理寄存器,還可以使用PC、LR和SP
使用虛擬寄存器。不能使用PC、LR和SP寄存器
是否自動(dòng)產(chǎn)生返回指令
手工添加返回指令
指定產(chǎn)生(但不支持BX、BXJ和BLX指令)
是否支持BKPT指令
不直接支持
不支持