?
12.1??內(nèi)聯(lián)匯編和嵌入型匯編的使用
內(nèi)聯(lián)匯編和嵌入型匯編是包含在C/C++編譯器中的匯編器。使用它可以在C/C++程序中實現(xiàn)C/C++語言不能完成的一些工作。例如,在下面幾種情況中必須使用內(nèi)聯(lián)匯編或嵌入型匯編。
·??程序中使用飽和算術運算(Saturating?arithmetic),如SSAT16?和?USAT16指令。
·??程序中需要對協(xié)處理器進行操作。
·??在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++)關鍵字聲明,語法格式如下所示。
·??__asm("instruction[;instruction]"); //?必須為單條指令
???__asm{instruction[;instruction]}
·??__asm{
???...
???instruction
???...
}
·??asm("instruction[;instruction]"); //?必須為單條指令
???asm{instruction[;instruction]}
·??asm{
????...
????instruction
????...
}
內(nèi)聯(lián)匯編支持大部分的ARM指令,但不支持帶狀態(tài)轉移的跳轉指令,如BX和BLX指令,詳見ARM相關文檔。
由于內(nèi)聯(lián)匯編嵌入在C或C++程序中,所有在用法上有其自身的一些特點。
①?如果同一行中包含多條指令,則用分號隔開。
②?如果一條指令不能在一行中完成,使用反斜杠“/”將其連接。
③?內(nèi)聯(lián)匯編中的注釋語句可以使用C或C++風格的。
④?匯編語言中使用逗號“,”作為指令操作數(shù)的分隔符,所以如果在C語言中使用逗號必須用圓括號括起來。如,__asm?{ADD?x,?y,?(f(),?z)}。
⑤?內(nèi)聯(lián)匯編語言中的寄存器名被編譯器視為C或C++語言中的變量,所以內(nèi)聯(lián)匯編中出現(xiàn)的寄存器名不一定和同名的物理寄存器相對應。這些寄存器名在使用前必須聲明,否則編譯器將提示警告信息。
⑥?內(nèi)聯(lián)匯編中的寄存器(除程序狀態(tài)寄存器CPSR和SPSR外)在讀取前必須先賦值,否則編譯器將產(chǎn)生錯誤信息。下面的例子顯示了內(nèi)聯(lián)匯編和真正匯編的區(qū)別。
錯誤的內(nèi)聯(lián)匯編函數(shù)如下所示。
int?f(int?x)
{
???????__asm
???????{
????????????STMFD?sp!,?{r0} //?保存r0不合法,因為在讀之前沒有對寄存器寫操作
????????????ADD?r0,?x,?1
????????????EOR?x,?r0,?x
????????????LDMFD?sp!,?{r0} //?不需要恢復寄存器
???????}
???????return?x;
}
將其進行改寫,使它符合內(nèi)聯(lián)匯編的語法規(guī)則。
int?f(int?x)
{
???????int?r0;
???????__asm
???????{
????????????ADD?r0,?x,?1
????????????EOR?x,?r0,?x
???????}
???????return?x;
}
下面通過幾個例子進一步了解內(nèi)聯(lián)匯編的語法。
①?字符串拷貝
下面的例子使用一個循環(huán)完成了字符串的拷貝工作。
#include?<stdio.h>
void?my_strcpy(const?char?*src,?char?*dst)
{
??????int?ch;
??????__asm
??????{
??????loop:
???????????LDRB????ch,?[src],?#1
???????????STRB????ch,?[dst],?#1
???????????CMP?????ch,?#0
???????????BNE?????loop
??????}
}
int?main(void)
{
??????const?char?*a?=?"Hello?world!";
??????char?b[20];
??????my_strcpy?(a,?b);
??????printf("Original?string:?'%s'n",?a);
??????printf("Copied?string:?'%s'n",?b);
??????return?0;
}
?
②?中斷使能
下面的例子通過讀取程序狀態(tài)寄存器CPSR并設置它的中斷使能位bit[7]來禁止/打開中斷。需要注意的是,該例只能運行在系統(tǒng)模式下,因為用戶模式是無權修改程序狀態(tài)寄存器的。
__inline?void?enable_IRQ(void)
{
??????int?tmp;
??????__asm
??????{
???????????MRS?tmp,?CPSR
???????????BIC?tmp,?tmp,?#0x80
???????????MSR?CPSR_c,?tmp
??????}
}
__inline?void?disable_IRQ(void)
{
??????int?tmp;
??????__asm
??????{
???????????MRS?tmp,?CPSR
???????????ORR?tmp,?tmp,?#0x80
???????????MSR?CPSR_c,?tmp
??????}
}
int?main(void)
{
??????disable_IRQ();
??????enable_IRQ();
}
③?分隔符的計算
下面的例子計算兩個整數(shù)數(shù)組中分隔符“,”的個數(shù)。該例子顯示了如何在內(nèi)聯(lián)匯編中訪問C或C++語言中的數(shù)據(jù)類型。該例中的內(nèi)聯(lián)匯編函數(shù)mlal()被編譯器優(yōu)化為一條SMLAL指令,可以使用-S?–interleave編譯選項使編譯器輸出匯編結果。
#include?<stdio.h>
/*?change?word?order?if?big-endian?*/
#define?lo64(a)?(((unsigned*)?&a)[0]) /*?long?long型的低32位?*/
#define?hi64(a)?(((int*)?&a)[1]) /*?long?long型的高32位?*/
__inline?__int64?mlal(__int64?sum,?int?a,?int?b)
{
#if?!defined(__thumb)?&&?defined(__TARGET_FEATURE_MULTIPLY)
??????__asm
??????{
????????SMLAL?lo64(sum),?hi64(sum),?a,?b
??????}
#else
??????sum?+=?(__int64)?a?*?(__int64)?b;
#endif
??????return?sum;
}
__int64?dotprod(int?*a,?int?*b,?unsigned?n)
{
??????__int64?sum?=?0;
??????do
??????????sum?=?mlal(sum,?*a++,?*b++);
??????while?(--n?!=?0);
??????return?sum;
}
int?a[10]?=?{?1,?2,?3,?4,?5,?6,?7,?8,?9,?10?};
int?b[10]?=?{?10,?9,?8,?7,?6,?5,?4,?3,?2,?1?};
int?main(void)
{
??????printf("Dotproduct?%lld?(should?be?%d)n",?dotprod(a,?b,?10),?220);
??????return?0;
}
2.內(nèi)聯(lián)匯編中的限制
可以在內(nèi)聯(lián)匯編代碼中執(zhí)行的操作有許多限制。這些限制提供安全的方法,并確保在匯編代碼中不違反?C?和?C++?代碼編譯中的假設。
①?不能直接向程序計數(shù)器PC賦值。
②?內(nèi)聯(lián)匯編不支持標號變量。
③?不能在程序中使用“.”或{PC}得到當前指令地址值。
④?在16進制常量前加“0x”代替“&”。
⑤?建議不要對堆棧進行操作。
⑥?編譯器可能會使用r12和r13寄存器存放編譯的中間結果,在計算表達式值時可能會將寄存器r0~r3、r12及r14用于子程序調用。另外在內(nèi)聯(lián)匯編中設置程序狀態(tài)寄存器CPSR中的標志位NZCV時,要特別小心,內(nèi)聯(lián)匯編中的設置很可能會和編譯器計算的表達式的結果沖突。
⑦?用內(nèi)聯(lián)匯編代碼更改處理器模式是可能的。然而,更改處理器模式會禁止使用?C或?C++操作數(shù)或禁止對已編譯C或C++代碼的調用,直到將處理器模式更改回原設置之后之前的函數(shù)庫才可正常使用。
⑧?為Thumb狀態(tài)編譯C或C++時,內(nèi)聯(lián)匯編程序不可用且不匯編Thumb指令。
⑨?盡管可以使用通用協(xié)處理器指令指定?VFP?或?FPA?指令,但內(nèi)聯(lián)匯編程序不為它們提供直接支持。
不能用內(nèi)聯(lián)匯編代碼更改?VFP?向量模式。內(nèi)聯(lián)匯編可包含浮點表達式操作數(shù),該操作數(shù)可使用編譯程序生成的?VFP?代碼求出操作數(shù)值。因此,僅由編譯程序修改?VFP?狀態(tài)很重要。
⑩?內(nèi)嵌匯編不支持的指令有BX、BLX、BXJ和BKPT指令。而LDM、STM、LDRD和STRD指令可能被等效為ARM?LDR或STR指令。
3.內(nèi)聯(lián)匯編中的虛擬寄存器
內(nèi)聯(lián)匯編程序提供對?ARM?處理器物理寄存器的非直接訪問。如果在內(nèi)聯(lián)匯編程序指令中將某個ARM寄存器用作操作數(shù),它就成為相同名稱的虛擬寄存器的引用,而不是對實際物理ARM寄存器的引用。例如內(nèi)聯(lián)匯編指令中使用了寄存器r0,但對于C編譯器,指令中出現(xiàn)的r0只是一個變量,并非實際的物理寄存器r0,當程序運行時,可能是由物理寄存器r1來存放r0所代表的值。
下面的例子顯示了編譯器如何對內(nèi)聯(lián)匯編指令的寄存器進行分配。
程序的源代碼如下。
#include?<stdio.h>
void?test_inline_register(void)
{
??????int?i;
??????int?r5,r6,r7;
??????__asm
??????{
???????????MOV??i,#0
??????loop:
???????????MOV??r5,#0
???????????MOV??r6,#0
???????????MOV??r7,#0
???????????ADD??i,i,#1
???????????CMP??i,#3
???????????BNE??loop
??????}
}
int?main(void)
{
????
??????test_inline_register?();
??????printf("test?inline?registern");
????
??????return?0;
}
由C編譯器編譯出的匯編碼如下所示。
????test_inline_register:
????0000807C?E3A00000??MOV??????r0,#0
>>>?TEST_INLINE_REGISTER#12?????loop:
????00008080?E1A00000??NOP??????
>>>?TEST_INLINE_REGISTER#13?????????MOV??r5,#0
????00008084?E3A01000??MOV??????r1,#0
>>>?TEST_INLINE_REGISTER#14?????????MOV??r6,#0
????00008088?E3A02000??MOV??????r2,#0
>>>?TEST_INLINE_REGISTER#15?????????MOV??r7,#0
????0000808C?E3A03000??MOV??????r3,#0
>>>?TEST_INLINE_REGISTER#16?????????ADD??i,i,#1
????00008090?E2800001??ADD??????r0,r0,#1
>>>?TEST_INLINE_REGISTER#17?????????CMP??i,#3
????00008094?E3500003??CMP??????r0,#3
????00008098?0A000000??BEQ??????0x80a0??????????????<TEST_INLINE_REGISTER#21>
>>>?TEST_INLINE_REGISTER#18?????????BNE??loop
????0000809C?EAFFFFF8??B????????0x8084?????????????<TEST_INLINE_REGISTER#13>
>>>?TEST_INLINE_REGISTER#21?}
????000080A0?E12FFF1E??BX???????r14
>>>?TEST_INLINE_REGISTER#25?{
? |
注意 |
下面的代碼是由Realview2.2編譯出的代碼,使用其他編譯器結果可能有差異。同一段內(nèi)嵌匯編經(jīng)過不同版本的編譯器編譯后,在指令里可能使用不一樣的實際寄存器,但是只要遵循文檔里的編碼指導,執(zhí)行的功能肯定相同。 |
?
例子中以“>>>”的開頭的行是程序的源碼部分,緊接其后的是由編譯器編譯出的匯編代碼。從上例可以很清楚地看出,源程序中使用了r5、r6和r7,但由編譯器編譯后的代碼使用了寄存器r1、r2和r3。
另外,需要特別指出的是在內(nèi)聯(lián)匯編中使用寄存器必須先聲明其變量類型,如上例中的“int?r5,r6,r7”。如果不在使用前進行聲明,編譯器將給出以下錯誤信息。
#1267-D:?Implicit?physical?register?R3?should?be?defined?as?a?variable
編譯程序定義的虛擬寄存器有函數(shù)局部作用范圍,即在同一個C函數(shù)中,涉及相同虛擬寄存器名稱的多個asm語句或聲明,訪問相同的虛擬寄存器。
內(nèi)聯(lián)匯編沒有為pc(r15)、lr(r14)和sp(r13)寄存器創(chuàng)建虛擬寄存器,而且不能在內(nèi)聯(lián)匯編代碼中讀取或直接修改它們的值。如果內(nèi)聯(lián)匯編程序中出現(xiàn)了對這些寄存器的訪問,編譯器將給出以下錯誤消息。例如,如果指定r14:
#20:?identifier?"r14"?is?undefined
內(nèi)聯(lián)匯編可以直接使用CPSR和SPSR對程序狀態(tài)字進行操作,因為內(nèi)聯(lián)匯編中不存在虛擬處理器狀態(tài)寄存器(PSR)。任何對?PSR?的引用總是指向物理?PSR。
4.內(nèi)聯(lián)匯編中的指令展開
內(nèi)聯(lián)匯編代碼中的ARM指令可能會在編譯過程中擴展為幾條指令。擴展取決于指令、指令中指定的操作數(shù)個數(shù)以及每個操作數(shù)的類型和值。通常,被擴展的指令有以下兩種情況:
·??含有常數(shù)操作的指令;
·??LDM、STM、LDRD?和?STRD指令;
·??乘法指令MUL被擴展為一系列的加法和移位指令。
下面的例子說明了編譯器如何對含有常數(shù)操作的指令進行擴展。
包含有常數(shù)操作的加法指令:
ADD?r0,r0,#1023
被編譯器編譯為如下兩條指令:
ADD?r0,r0,#1024
SUB?r0,r0,#1
注意 |
擴展指令對程序狀態(tài)寄存器CPSR的影響:算術指令影響相應的NZCV標準位;其他指令設置NZ標志位不影響V標志位。 |
所有的LDM和STM指令被擴展為等效的LDR和STR指令序列。然而,在優(yōu)化過程中,編譯程序可能因此將單獨的指令重組為一條LDM或STM指令。
5.內(nèi)聯(lián)匯編中的常數(shù)
指令中的標志符“#”是可選的(前面的例子中,指令中常數(shù)前均加了標志符“?!保?。如果在指令中使用了“?!?,則其后的表達式必為常數(shù)。
6.內(nèi)聯(lián)匯編指令對標志位的影響
內(nèi)聯(lián)匯編指令可能顯式或隱式地更新處理器程序狀態(tài)寄存器的條件標志位。在僅包含虛擬寄存器操作數(shù)或簡單表達式操作數(shù)的內(nèi)聯(lián)匯編中,其執(zhí)行結果是可以預見。如果指令中指定了隱式或顯式更新條件標志位,則條件標志位根據(jù)指令的執(zhí)行進行設置。如果未指定更新,則條件標志不會更改。如果內(nèi)嵌匯編指令的操作數(shù)都不是簡單操作數(shù)時或指令不顯式更新條件標志位,則條件標志位可能會被破壞。一般情況下,編譯程序不易診斷出對條件標志的潛在破壞。然而,在構造析構C++臨時函數(shù)的操作數(shù)時,如果指令試圖更新條件標志,編譯程序將給予警告,因為析構函數(shù)可能會破壞條件標志位。
?
7.內(nèi)聯(lián)匯編指令中的操作數(shù)
內(nèi)聯(lián)匯編指令中的操作數(shù)分為以下4種。
·??虛擬寄存器
·??表達式操作數(shù)
·??寄存器列表
·??中間操作數(shù)
(1)虛擬寄存器
在內(nèi)聯(lián)匯編指令中指定的寄存器表示虛擬寄存器而不是實際的物理寄存器。由編譯器編譯的匯編代碼中使用的物理寄存器可能與在指令中指定的不同。每個虛擬寄存器的初值是不可預測的,必須在讀取之前將初值寫入虛擬寄存器。如果在寫入之前試圖讀虛擬寄存器,編譯程序會給予警告。
(2)表達式操作數(shù)
在內(nèi)聯(lián)匯編指令中,可將函數(shù)自變量、C或C++變量和其他C或C++表達式指定為寄存器操作數(shù)。用作操作數(shù)的表達式必須為整數(shù)類型,如char、short、int或long,(長整型long?long除外)或指針類型。當表達式作為內(nèi)聯(lián)匯編指令的操作數(shù)時,編譯器在編譯時自動增加一段代碼計算表示式的值并將其加載到指定的寄存器中。
注意 |
數(shù)據(jù)類型中除char和short(默認為無符號類型)外,其他均為有符號類型。 |
下面的例子顯示了編譯器如何處理內(nèi)聯(lián)匯編中的表達式操作數(shù)。
程序源代碼如下所示。
/*?Example?Operands?*/
void?my_operand(void)
{
??????int?i,j,total;
????
??????__asm
??????{
??????mov?i,#0
??????mov?j,#1
??????add?total,j,i+j
??????}
}
int?main(void)
{
???
??????my_operand?();
?}
由編譯器編譯出的匯編代碼如下所示(其中只列出了內(nèi)聯(lián)匯編的一段代碼)。
????my_operand:
????0000807C?E3A01000??MOV??????r1,#0
>>>?OPERANDS#12?????mov?j,#1
????00008080?E3A00001??MOV??????r0,#1
????00008084?E0812000??ADD??????r2,r1,r0
>>>?OPERANDS#13?????add?total,j,i+j
????00008088?E0803002??ADD??????r3,r0,r2
>>>?OPERANDS#15?}
????0000808C?E12FFF1E??BX???????r14
>>>?OPERANDS#19?{
?
從編譯的代碼可以看出,編譯器將“add?total,j,i+j”分為兩步來完成,用戶在編寫自己的內(nèi)聯(lián)匯編應用程序時要特別注意這一點。
包含多個表達式操作數(shù)的指令,沒有指定表達式操作數(shù)求值的順序。
將C或C++表達式用作內(nèi)聯(lián)匯編程序操作數(shù),如果表達式的值不能滿足?ARM指令中所要求的指令操作數(shù)約束條件,一條指令將被擴展為多條指令。
如果用作操作數(shù)的表達式創(chuàng)建需要析構的臨時函數(shù),析構將發(fā)生在執(zhí)行內(nèi)聯(lián)匯編指令之后,這與C++析構臨時函數(shù)的規(guī)則相類似。
簡單表達式操作數(shù)包含以下幾種類型。
·??變量值
·??變量地址
·??指針變量的反引用(the?dereferencing?of?a?point?varable)
·??偽操作指定的程序常量
非簡單表達式操作數(shù)包含以下幾種類型。
·??隱式函數(shù)調用,如除法,或顯式函數(shù)調用
·??C++臨時函數(shù)的構造
·??算術或邏輯操作
(3)寄存器列表
寄存器列表最多可包含?16?個操作數(shù)。這些操作數(shù)可以是虛擬寄存器或表達式操作數(shù)。在寄存器列表中指定虛擬寄存器和表達式操作數(shù)的順序非常重要。寄存器列表中操作數(shù)的讀寫順序是從左到右。第一個操作數(shù)使用最低地址,隨后的操作數(shù)的地址依次在前一地址基礎上增加?4。這一點與LDM?或?STM?指令的普通操作(編號最低的物理寄存器總是存入最低的存儲器地址)是不同的。之所以存在這種區(qū)別是因為在內(nèi)聯(lián)匯編中使用的寄存器被編譯器虛擬化了。
同一個表達式操作數(shù)或虛擬寄存器可以在寄存器列表中出現(xiàn)多次,重復使用。
如果表達式操作數(shù)或虛擬寄存器被指定為指令中的基址寄存器,表達式或虛擬寄存器的值按照ARM指令尋址方式進行更新。更新將覆蓋原表達式或虛擬寄存器的值。
(4)中間操作數(shù)(Intermediate?operands)
在內(nèi)聯(lián)匯編指令中,可能將C或C++整型常量表達式用作立即數(shù)處理。用于指定直接移位的常量表達式的值必須在ARM指令規(guī)定的移位操作數(shù)的范圍內(nèi);用于為存儲器或協(xié)處理器數(shù)據(jù)傳送指令指定直接偏移量的常量表達式,必須符合ARM體系結構中的內(nèi)存對齊標準。
8.函數(shù)調用和分支跳轉
利用內(nèi)聯(lián)匯編程序的BL和SWI指令可在常規(guī)指令字段后指定3個可選列表。這些指令格式有以下幾種。
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調用的中斷號;function為被調用函數(shù)名;{input_param_list}為輸入?yún)?shù)列表;{output_value_list}為輸出參數(shù)列表;{corrupt_reg_list}為被破壞寄存器列表。
注意 |
內(nèi)聯(lián)匯編程序不支持BX、BLX和BXJ指令。不能在任何輸入、輸出或“被破壞的寄存器列表(corrupted?register?list)”中指定lr、sp或pc寄存器;任何SWI指令或函數(shù)調用不能更改sp寄存器。 |
下面分別詳細介紹語法格式中各參數(shù)的使用。
(1)未指定任何列表
如果在SWI和BL指令后沒指定任何列表,則有下面規(guī)則。
·??r0~r3用作輸入?yún)?shù);
·??r0?用于輸出值;
·??r12和r14的值將會被修改。
(2)輸入?yún)?shù)列表
指令中的輸入?yún)?shù)列表{?input_param_list?}列出了傳遞給被調用函數(shù)function和SWI的參數(shù)。被傳遞的參數(shù)可以是表達式、變量或包含表達式或變量的物理寄存器。
內(nèi)聯(lián)匯編編譯器在編譯時增加一小段編譯程序負責在函數(shù)和SWI調用前
將傳遞的參數(shù)載入特定的物理寄存器中。為確保與現(xiàn)有內(nèi)聯(lián)匯編代碼的向后兼容性,程序中指定物理寄存器名稱而并不對其賦值,使相同名稱虛擬寄存器中的值出現(xiàn)在物理寄存器中。
?
例如,指令BL?foo?{r0=expression1,?r1=expression2,?r2}生成以下偽代碼:
MOV?(physical)?r0,?expression1
MOV?(physical)?r1,?expression2
MOV?(physical)?r2,?(virtual)?r2
BL?foo
(3)輸出參數(shù)列表
輸出參數(shù)列表{?output_value_list?}列出了用來存放功能函數(shù)和SWI調用返回值的寄存器或表達式。列表中的值可以是物理寄存器、可修改長值表達式或單個物理寄存器名稱。
內(nèi)聯(lián)匯編程序從特定的物理寄存器中取值并賦值到特定的表達式中。指定物理寄存器名稱而并不賦值,導致相同名稱虛擬寄存器被物理寄存器中的值更新。
例如,BL?foo?{?},?{result1=r0,?r1}生成以下偽碼:
BL?foo
MOV?result1,?(physical)?r0
MOV?(virtual)?r1,?(physical)?r1
(4)被破壞的寄存器列表(Corrupted?register?list)
此列表指定被函數(shù)調用破壞的物理寄存器。如果條件標志被調用的函數(shù)修改,必須在被破壞的寄存器列表中指定PSR。
BL和SWI指令總是破壞lr。
如果指令中缺少此列表項,則r0~r3、ip、lr和PSR被破壞。
注意 |
指令BL和B的區(qū)別在于,跳轉指令B只能使程序跳轉到C或C++程序的一個地址標號,不能用于子程序調用。 |
?
9.內(nèi)嵌匯編中的標號
內(nèi)聯(lián)匯編代碼中定義的標號可被用作跳轉或C和C++“goto”語句的目標。在內(nèi)聯(lián)匯編代碼中,C和C++中定義的標號可被用作跳轉指令的目標。
10.內(nèi)嵌匯編器版本間的差異
不同版本的ARM編譯器對內(nèi)聯(lián)匯編程序的語法要求有顯著差異。在具體使用時請參見相關文檔。
·??如果使用的是?ADS?v1.2,請參閱?ADS?開發(fā)者指南;
·??如果使用的是?RVCT?v1.2,請參閱?RealView?編譯工具?1.2?版開發(fā)者指南。
12.1.2??嵌入式匯編
利用?ARM?編譯器可將匯編代碼包括到一個或多個C或C++函數(shù)定義中去。嵌入式匯編器提供對目標處理器不受限制的低級別訪問,利用它可以使用C和C++預處理程序偽操作(preprocessor?directive)并可以方便的使用偏移量訪問結構成員。
本小節(jié)將介紹以下內(nèi)容:
·??嵌入式匯編程序語法;
·??嵌入式匯編語句的限制;
·??嵌入式匯編程序表達式和C或C++表達式之間的差異;
·??嵌入式匯編函數(shù)的生成;
·??__cpp?關鍵字;
·??手動重復解決方案;
·??相關基類的關鍵字;
·??成員函數(shù)類的關鍵字;
·??調用非靜態(tài)成員函數(shù)。
有關為?ARM?處理器編寫匯編語言的詳細信息,請參閱ADS或RealView編譯工具的匯編程序指南。
1.嵌入式匯編語言語法
嵌入式匯編函數(shù)定義由?--asm(C和C++)或asm(C++)?函數(shù)限定符標記,可用于:
·??成員函數(shù);
·??非成員函數(shù);
·??模板函數(shù);
·??模板類成員函數(shù)。
用__asm或asm聲明的函數(shù)可以有調用參數(shù)和返回類型。它們從C和C++中調用的方式與普通C和C++函數(shù)調用方式相同。嵌入式匯編函數(shù)語法是:
__asm?return-type?function-name(parameter-list)
{
?????//?ARM/Thumb/Thumb-2?assembler?code
?????instruction[;instruction]
?????...
?????[instruction]
}
嵌入式匯編的初始執(zhí)行狀態(tài)是在編譯程序時由編譯選項決定的。這些編譯選項如下所示:
·??如果初始狀態(tài)為ARM狀態(tài),則內(nèi)嵌匯編器使用--arm選項;
·??如果初始狀態(tài)為Thumb狀態(tài),則內(nèi)嵌匯編器使用--thumb選項。
注意 |
嵌入式匯編的初始狀態(tài)由編譯器的編譯選項確定,與程序中的#pragma?arm?和?#pragma?thumb偽操作無關。 |
?
可以顯示地使用ARM、THUMB和CODE16偽操作改變嵌入式匯編的執(zhí)行狀態(tài)。關于ARM偽操作的詳細信息請參加指令偽操作一節(jié)。如果使用的處理器支持Thumb-2指令,則可以在Thumb狀態(tài)下,在嵌入式匯編中使用Thumb-2指令。
參數(shù)名允許用在參數(shù)列表中,但不能用在嵌入式匯編函數(shù)體內(nèi)。例如,以下函數(shù)在函數(shù)體內(nèi)使用整數(shù)i,但在匯編中無效:
__asm?int?f(int?i)?{
?????ADD?i,?i,?#1?//?編譯器報錯
}
可以使用?r0?代替?i。
下面通過嵌入式匯編的例子,來進一步熟悉嵌入式匯編的使用。
下面的例子實現(xiàn)了字符串的拷貝,注意和上一節(jié)中內(nèi)聯(lián)匯編中字符串拷貝的例子相比較,分析其中的區(qū)別。
#include?<stdio.h>
__asm?void?my_strcpy(const?char?*src,?const?char?*dst)?{
loop
???????LDRB??r3,?[r0],?#1
???????STRB??r3,?[r1],?#1
???????CMP???r3,?#0
???????BNE???loop
???????MOV???pc,?lr
}
void?main()
{
??????const?char?*a?=?"Hello?world!";
??????char?b[20];
??????my_strcpy?(a,?b);
??????printf("Original?string:?'%s'n",?a);
??????printf("Copied???string:?'%s'n",?b);?
}
2.嵌入式匯編語言的使用限制
嵌入式匯編的使用有下面一些限制。
①?在預處理之后,__asm?函數(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ù)的順序是在編譯時定義好的,所有從一個內(nèi)嵌匯編跳轉到一個內(nèi)嵌匯編程序是運行的,但在內(nèi)聯(lián)匯編中卻不能實現(xiàn)。
③?__asm?函數(shù)調用遵循AAPCS規(guī)則。所以,即使在__asm?函數(shù)體內(nèi)可用的匯編代碼(例如,更改狀態(tài)),在__asm函數(shù)和普通C或C++函數(shù)相互調用時,未必可用,因為此調用也必須遵循?AAPCS規(guī)則。
?
3.嵌入式匯編程序表達式和C或C++表達式之間的差異
嵌入式匯編表達式和C或C++表達式之間存在以下差異。
①?匯編程序表達式總是無符號的。相同的表達式在匯編程序和?C?或?C++?中有不同值。例如:
MOV?r0,?#(-33554432?/?2)??????????//?結果為?0x7f000000
MOV?r0,?#__cpp(-33554432?/?2)????//?結果為?0xff000000
②?以0開頭的匯編程序編碼仍是十進制的。例如:
MOV?r0,?#0700????????????????//?十進制?700
MOV?r0,?#__cpp(0700)????????//?八進制?0700?等于?十進制?448
③?匯編程序運算符優(yōu)先順序與?C?和?C++?不同。例如:
MOV?r0,?#(0x23?:AND:?0xf?+?1)?????//?((0x23?&?0xf)?+?1)?=>?4
MOV?r0,?#__cpp(0x23?&?0xf?+?1)????//?(0x23?&?(0xf?+?1))?=>?0
④?匯編程序字符串不是以空字符為終止標志的:
DCB?"no?trailing?null"?????????????????????//?16?bytes
DCB?__cpp("I?have?a?trailing?null!!")???//?25?bytes
注意 |
在_cpp標識符作用范圍之內(nèi)使用C或C++語法規(guī)則。 |
?
4.嵌入式匯編函數(shù)的生成
由關鍵字__asm聲明的嵌入式匯編程序,在編譯時將作為整個文件體傳遞給ARM匯編器。傳遞過程中,__asm函數(shù)的順序保持不變(用模板實例生成的函數(shù)除外)。正是由于嵌入式匯編的這個特性,使得由一個__asm標識的嵌入式匯編程序調用在同一文件中的另一個嵌入式匯編程序是可以實現(xiàn)的。
當使用編譯器?armcc?時,局部鏈接器(Partial?Link)將匯編程序產(chǎn)生的目標文件與編譯C程序的目標文件相結合,產(chǎn)生單個目標文件。
編譯程序為每個?__asm?函數(shù)生成AREA命令。例如,以下__asm函數(shù):
#include?<cstddef>
struct?X?{?int?x,y;?void?addto_y(int);?};
__asm?void?X::addto_y(int)?{
?????LDR??????r2,[r0,?#__cpp(offsetof(X,?y))]
?????ADD??????r1,r2,r1
?????STR??????r1,[r0,?#__cpp(offsetof(X,?y))]
?????BX???????lr
}
對于此函數(shù),編譯程序生成:
AREA?||.emb_text||,?CODE,?READONLY
EXPORT?|_ZN1X7addto_yEi|
#line?num?"file"
|_ZN1X7addto_yEi|?PROC
?LDR?r2,[r0,?#4]
?ADD?r1,r2,r1
?STR?r1,[r0,?#4]
?BX?lr
ENDP
END
由上面的例子可以看出,對于變量offsetof的使用必須加__cpp()標識符才能引用,因為該變量是在cstddef頭文件中定義的。
由__asm聲明的常規(guī)函數(shù)被放在名為.emb_text的段(Section)中。這一點也是嵌入式匯編和內(nèi)聯(lián)匯編最大的不同。相反,隱式實例模板函數(shù)(Implicitly?Instantiated?Template?Function)和內(nèi)聯(lián)匯編函數(shù)放在與函數(shù)名同名的區(qū)域(Area)內(nèi),并為該區(qū)域增加公共屬性。這就確保了這類函數(shù)的特殊語義得以保持。
由于內(nèi)聯(lián)和模板函數(shù)的區(qū)域的特殊命名,所以這些函數(shù)不按照文件中定義的順序排列,而是任意排序。因此,不能以__asm函數(shù)在原文件中的排列順序,來判斷它們的執(zhí)行順序,也就是說,即使兩個連續(xù)排列的__asm函數(shù),也不一定能順序執(zhí)行。
5.關鍵字__cpp
可用__cpp關鍵字從匯編代碼中訪問C或C++的編譯時常量表達式,其中包括含有外部鏈接的數(shù)據(jù)或函數(shù)地址。標識符__cpp內(nèi)的表達式必須是適合用作C++靜態(tài)初始化的常量表達式(請參閱ISO/IEC?14882:1998中的3.6.2非本地對象初始化一節(jié)和本書的常量表達式一節(jié))。
編譯時,編譯器將使用__cpp(expr)?的地方用匯編程序可以使用的常量所取代。例如:
LDR?r0,?=__cpp(&some_variable)
LDR?r1,?=__cpp(some_function)
BL??__cpp(some_function)
MOV?r0,?#__cpp(some_constant_expr)
__cpp表達式中的名稱可在__asm函數(shù)的C++上下文中查閱。__cpp表達式結果中的任何名稱按照要求被損毀并自動為其生成IMPORT語句。
6.手動重復解決方案
可以在嵌入式匯編中使用C++轉換為非虛擬函數(shù)調用解決重復。例如:
void?g(int);
void?g(long);
struct?T?{
??????int?mf(int);
??????int?mf(int,int);
};
__asm?void??f(T*,?int,?int)?{
?????BL?__cpp(static_cast<int?(T::*)(int,?int)>(&T::mf))?//?calls?T::mf(int,?int)
?????BL?__cpp(static_cast<void?(*)(int)>(g))?//?calls?g(int)
?????MOV?pc,?lr
}
?
7.相關基類的關鍵字
利用以下關鍵字可以確定從對象起始處到其相關基類的偏移量:
__offsetof_base(D,?B)?
其中,B必須是D的非虛擬基類。
該函數(shù)返回從D對象的起始處到其中B基子對象的起始處的偏移量。結果可能是零。必須將偏移量(以字節(jié)為單位)添加到D*?p來執(zhí)行。
static_cast<B*>(p)?的等效功能,如下程序段所示:
__asm?B*?my_static_base_cast(D*?/*p*/)?{
?????if?__offsetof_base(D,?B)?<>?0 //排除偏移量為0的情況
???????????ADD?r0,?r0,?#__offsetof_base(D,?B)
?????endif
?????MOV?pc,?lr
}
在匯編程序源代碼中,這些關鍵字被轉換為整數(shù)或邏輯常量。只能將它們用于__asm函數(shù),而不能用于__cpp表達式。
8.成員函數(shù)類的關鍵字
以下關鍵字方便了從__asm函數(shù)中調用虛擬或非虛擬成員函數(shù)。以__mcall開頭的關鍵字可用于虛擬和非虛擬函數(shù)。以__vcall開頭的關鍵字僅能用于虛擬函數(shù)。在調用靜態(tài)成員函數(shù)的過程中,這些關鍵字沒有特別的作用。
下面詳細介紹這些關鍵字的使用。
①?__mcall_is_virtual(D,?f)?
如果f是D中的虛擬成員函數(shù)或是D的基類,結果是{TRUE},否則結果是{FALSE}。如果返回{TRUE},可用虛擬調度進行調用,否則必須直接進行調用。
②?__mcall_is_in_vbase(D,?f)?
如果f是D虛擬基類中的非靜態(tài)成員函數(shù),結果是{TRUE},否則結果是{FALSE}。如果返回{TRUE},必須用__mcall_offsetof_vbaseptr(D,?f)進行this調整,否則必須用__mcall_this_
offset(D,?f)進行調整。
③?__mcall_this_offset(D,?f)
其中D是類,f是D中定義的非靜態(tài)成員函數(shù)或是D的非虛擬基類。該函數(shù)返回從D對象的起始處到定義f的基的起始處的偏移量。在用指向D的指針調用f的過程中,這是必要的this調整。返回值在D中可找到f時為零,或者與__offsetof_base(D,?B)相同,其中B為包含f的D非虛擬基類。在D的虛擬基類中找到f時,如果使用__mcall_this_offset(D,?f),則返回任意值,在程序中使用該返回值,匯編器將報告__mcall_this_offset無效使用的錯誤。
④?__vcall_offsetof_vfunc(D,?f)
其中D是類,f是D中定義的虛擬函數(shù)或是D的基類。將偏移量返回到虛擬函數(shù)表,在該表中可以找到從虛擬函數(shù)表到虛擬函數(shù)的偏移量。在f不是虛擬成員函數(shù)時,如果使用__vcall_offsetof_vfunc(D,?f),則返回任意值,而在設計上使用該值時會導致匯編錯誤。
9.調用非靜態(tài)成員函數(shù)
本小節(jié)列出了可以從?__asm?函數(shù)中調用虛擬或非虛擬函數(shù)的關鍵字。靜態(tài)成員函數(shù)的參數(shù)不相同(沒有?this),使得檢測靜態(tài)成員函數(shù)的關鍵字__mcall_is_static不可用,因此調用位置很可能已經(jīng)專用于調用靜態(tài)成員函數(shù)。
(1)調用非虛擬成員函數(shù)
例如,在虛擬基(virtual?base)或非虛擬基(non-virtual?base)中,以下代碼可用于調用虛擬函數(shù):
//?rp包含指向D的指針,該程序的功能是實現(xiàn)在使用rp時調用D的非虛成員函數(shù)f
//?所有參數(shù)準備好
//?假設并不返回一個結構類型
?if?__mcall_is_in_vbase(D,?f)
???ASSERT?{FALSE}???//?can't?access?virtual?base
?else
???MOV?r0,?rp //使用指向D的指針rp*
???ADD?r0,?r0,?#__mcall_this_offset(D,?f) //地址調整
?endif
???BL?__cpp(&D::f)
(2)調用虛擬成員函數(shù)
例如,在虛擬或非虛擬基中,以下代碼可用于調用虛擬函數(shù):
//?rp包含指向D的指針,該程序的功能是在使用rp時調用D的虛擬函數(shù)f
//?所有參數(shù)準備好
//?假如函數(shù)并不返回一個結構類型
?if?__mcall_is_in_vbase(D,?f)
??ASSERT?{FALSE} //?不能調用虛擬基
?else
??MOV?r0,?rp //?使用指向D的指針rp
??LDR?r12,?[rp] //?加載vtable表結構指針
??ADD?r0,?r0,?#__mcall_this_offset(D,?f) //?地址調整
?endif
??MOV?lr,?pc //?保存返回地址到lr
??LDR?pc,?[r12,?#__vcall_offsetof_vfunc(D,?f)] //?調用函數(shù)rp→f()
?
10.嵌入式匯編版本間的差異
不同版本的ARM編譯器對嵌入式匯編程序的語法要求會有所差異。在具體使用時請參見相關文檔。
值得注意的是,目前的嵌入式匯編器已經(jīng)完全支持ARMv6指令集,也就是說可以在嵌入式匯編中使用ARMv6指令集中的指令。
12.1.3??內(nèi)聯(lián)匯編中使用SP、LR和PC寄存器的遺留問題
雖然目前的編譯器不支持在內(nèi)聯(lián)匯編中使用SP、LR和PC寄存器,但在RVCT?v1.2及其以前的編譯器版本中是允許的。下面的例子顯示了使用早期編譯器版本,在內(nèi)聯(lián)匯編中使用LR寄存器的例子。
void?func()
{
??????int?var;
??????__asm
??????{
???????????mov??var,?lr??/*?得到func()函數(shù)的返回地址?*/
??????}
}
如果使用RVCT?v2.0編譯器編譯上面的代碼,編譯器將報告以下錯誤。
Error:??#20:?identifier?"lr"?is?undefined
使用RVCT?v2.0版本及其以后的編譯器,要在C或C++代碼中使用匯編訪問SP、LR和PC寄存器可以使用下面幾種方法。
①?使用嵌入式匯編代碼。嵌入式匯編支持所有的ARM指令,同時允許在代碼中訪問SP、LR和PC寄存器。
②?在內(nèi)聯(lián)匯編中使用以下一些指令。
·??__current_pc():訪問PC寄存器。
·??__current_sp():訪問SP寄存器。
·??__return_address():訪問LR,返回地址寄存器。
下面給出了兩個訪問SP、LR和PC寄存器的典型實例程序。
①?使用編譯器給定的指令。
void?printReg()
{
??????unsigned?int?spReg,?lrReg,?pcReg;
??????__asm?{
???????????MOV?spReg,?__current_sp()
???????????MOV?pcReg,?__current_pc()
???????????MOV?lrReg,?__return_address()
??????}
??????printf("SP?=?0x%Xn",spReg);
??????printf("PC?=?0x%Xn",pcReg);
??????printf("LR?=?0x%Xn",lrReg);
}
②?使用嵌入式匯編。
__asm?void?func()
{
?????MOV?r0,?lr
?????...
?????BX?lr
}
使用嵌入式匯編可以使用調試器捕獲程序的返回地址。
12.1.4??內(nèi)聯(lián)匯編代碼與嵌入式匯編代碼之間的差異
本節(jié)總結了內(nèi)聯(lián)匯編和嵌入式匯編在編譯方法上存在的差異:
·??內(nèi)聯(lián)匯編代碼使用高級處理器抽象,并在代碼生成過程中與C和C++代碼集成。因此,編譯程序將C和C++代碼與匯編代碼一起進行優(yōu)化。
·??與內(nèi)聯(lián)匯編代碼不同,嵌入式匯編代碼從C和C++代碼中分離出來單獨進行匯編,產(chǎn)生與C和C++源代碼編譯對象相結合的編譯對象。
·??可通過編譯程序來內(nèi)聯(lián)內(nèi)聯(lián)匯編代碼,但無論是顯式還是隱式,都無法內(nèi)聯(lián)嵌入式匯編代碼。
表12.1總結了內(nèi)聯(lián)匯編程序與嵌入式匯編程序之間的主要差異。
表12.1 內(nèi)聯(lián)匯編程序與嵌入式匯編程序之間的主要差異
功????能 |
嵌入式匯編程序 |
內(nèi)聯(lián)匯編程序 |
指令集 |
ARM和Thumb |
僅支持ARM |
ARM匯編指令偽操作 |
支持 |
不支持 |
ARMv6指令集 |
支持 |
僅支持媒體指令 |
C/C++表達式 |
只支持常數(shù)表達式 |
完全支持 |
匯編代碼是否優(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寄存器 |
是否自動產(chǎn)生返回指令 |
手工添加返回指令 |
指定產(chǎn)生(但不支持BX、BXJ和BLX指令) |
是否支持BKPT指令 |
不直接支持 |
不支持 |