【彙編優化】之內聯彙編

1.內聯彙編格式

asm volatile ( 這裏寫指令 
           : 輸出操作數                   /* 可選 */
           : 輸入操作數                   /* 可選 */
           : 可能被破壞的寄存器列表         /* 可選 */
           );
或者
__asm__ __volatile__ ( 這裏寫指令 
           : 輸出操作數                   /* 可選 */
           : 輸入操作數                   /* 可選 */
           : 可能被破壞的寄存器列表         /* 可選 */
           );

_ asm _ 修飾這個是一段彙編語言,它是GCC定義的關鍵字asm的宏定義(#define _ asm _ asm),它用來聲明一個內嵌彙編表達式。所以,任何一個內嵌彙編表達式都以它開頭,它是必不可少的;如果要編寫符合ANSI C標準的代碼(即:與ANSI C兼容),那就要使用_ asm _;

_ volatile _ 修飾這段代碼不被編譯器優化,保持代碼原樣。這個volatile正是我們需要的,如果經過編譯器優化,很有可能將我們寫的程序修改,並達不到預期的執行效果了。如果要編寫符合ANSI C標準的代碼(即:與ANSI C兼容),那就要使用_ volatile _;

然後,該介紹內嵌彙編的語言了。 一般而言,C語言裏嵌入彙編代碼片段都要比純彙編語言寫的代碼複雜得多。因爲這裏有個怎樣分配寄存器、怎樣與C代碼中的變量融合的問題。爲了這個目的,必須要對所用的彙編語言做更多的擴充,增加對彙編語言的明確指示。

1.1 指令部分

第一部分就是彙編語言的語句本身,其格式與在彙編語言程序中使用的格式基本相同,但也有不同之處。這一部分被稱爲“指令部分”說明他是必須有的,而其它各部分則視具體情況而定,如果不需要的話是可以忽略的,所以在最簡單的情況下就與常規的彙編語句基本相同。

指令部分的編寫規則:

當指令列表裏面有多條指令時,可以在一對雙引號中全部寫出,也可將一條或多條指令放在一對雙引號中,所有指令放在多對雙引號中;

如果是將所有指令寫在一對雙引號中,那麼,相鄰倆條指令之間必須用分號”;“或換行符(\n)隔開,如果使用換行符(\n),通常\n後面還要跟一個\t;或者是相鄰兩條指令分別單獨寫在兩行中;
如果將指令放在多對雙引號中,除了最後一對雙引號之外,前面的所有雙引號裏的最後一條指令後面都要有一個分號(;)或(\n)或(\n\t);
在涉及到具體的寄存器時就要在寄存器名前面加上兩個”%”號,以免混淆。

1.2 輸出部分

第二部分,緊接在指令部分後面的是“輸出部分”,用來指定當前內嵌彙編語句的輸出表達式。
格式爲:“操作約束”(輸出表達式)

用括號括起來的部分,它用於保存當前內嵌彙編語句的一個輸出值。在輸出表達式內需要用(=)或(+)來進行修飾。 等號(=)加號(+)的區別:等號(=)表示當前表達式是一個純粹的輸出操作,而加號(+)則表示當前表達式不僅僅是一個輸出操作,還是一個輸入操作;但無論是等號(=)還是加號(+),所表示的都是可寫,只能用於輸出,只能出現在輸出部分,而不能出現在輸入部分;在輸出部分可以出現多個輸出操作表達式,多個輸出操作表達式之間必須用逗號(,)隔開;;

用雙引號括起來的部分,被稱作是:”輸出操作約束“,也可以稱爲”輸出約束“;關於約束部分將在後面一起進行講解。

1.3 輸入部分

第三部分用來指定當前內嵌彙編語句的輸入;稱爲輸入表達式;
格式爲:”操作約束“(輸入表達式)

輸入部分同樣也由兩部分組成:由雙引號括起來的部分和由圓括號括起來的部分;這兩個部分對於當前內嵌彙編語句的輸入來說,是必不可少的;用於約束當前內嵌彙編語句中的當前輸入;這個部分也成爲”輸入操作約束“,也可以成爲是”輸入約束“;與輸出表達式中的操作約束不同的是,輸入表達式中的操作約束不允許指定等號(=)約束或加號(+)約束,也就是說,它只能是隻讀的;約束中必須指定一個寄存器約束;

操作約束
每一個輸入和輸出表達式都必須指定自己的操作約束;約束的類型有:寄存器約束、內存約束、立即數約束、通用約束;

寄存器約束
當你的輸入或輸出需要藉助於一個寄存器時,你需要爲其指定一個寄存器約束;

可以直接指定一個寄存器名字;比如:

__asm__ __volatile__("movl %0,%%cr0"::"eax"(cr0));

也可以指定寄存器的縮寫名稱;比如:

__asm__ __volatile__("movl %0,%%cr0"::"a"(cr0));  

如果指定的是寄存器的縮寫名稱,比如:字母a;那麼,GCC將會根據當前操作表達式的寬度來決定使用%rax、%eax、%ax還是%al;

常用的寄存器約束的縮寫:

r:I/O,表示使用一個通用寄存器,由GCC在%rax/%eax/%ax/%al、%rbx/%ebx/%bx/%bl、%rcx/%ecx/%cx/%cl、%rdx/%edx/%dx/%dl中選取一個GCC認爲是合適的; 
q:I/O,表示使用一個通用寄存器,與r的意義相同; 
g:I/O,表示使用寄存器或內存地址; 
m:I/O,表示使用內存地址; 
a:I/O,表示使用%rax/%eax/%ax/%al; 
b:I/O,表示使用%rbx/%ebx/%bx/%bl; 
c:I/O,表示使用%rcx/%ecx/%cx/%cl; 
d:I/O,表示使用%rdx/%edx/%dx/%dl; 
D:I/O,表示使用%rdi/%edi/%di; 
S:I/O,表示使用%rsi/%esi/%si; 
f:I/O,表示使用浮點寄存器; 
t:I/O,表示使用第一個浮點寄存器; 
u:I/O,表示使用第二個浮點寄存器; 
A:I/O,表示把%eax與%edx組合成一個64位的整數值; 
o:I/O,表示使用一個內存位置的偏移量; 
V:I/O,表示僅僅使用一個直接內存位置; 
i:I/O,表示使用一個整數類型的立即數; 
n:I/O,表示使用一個帶有已知整數值的立即數; 
F:I/O,表示使用一個浮點類型的立即數;

內存約束
如果一個輸入/輸出操作表達式,表現爲一個內存地址(指針變量),不想藉助於任何寄存器,則可以使用內存約束;

例如:

__asm__ __volatile__ ("lgdt %0":"=m"(__gdt_addr) : : );  
__asm__ __volatile__ ("lgdt %0"::"m"(__gdt_addr));

內存約束使用約束名“m”,表示的是使用系統支持的任何一種內存方式,不需要藉助於寄存器;

立即數約束
如果一個輸入/輸出操作表達式是一個數字常數,不想藉助於任何寄存器或內存,則可以使用立即數約束;
由於立即數在表達式中只能作爲右值使用,所以,對於使用立即數約束的表達式而言,只能放在輸入部分;

比如:

__asm__ __volatile__("movl %0,%%eax"::"i"(100));  

立即數約束使用約束名“i”表示輸入表達式是一個整數類型的立即數,不需要藉助於任何寄存器,只能用於輸入部分;使用約束名“F”表示輸入表達式是一個浮點數類型的立即數,不需要藉助於任何寄存器,只能用於輸入部分;

通用約束
約束名“g”可以用於輸入和輸出,表示可以使用通用寄存器、內存、立即數等任何一種處理方式;通用約束“g”是一個非常靈活的約束,當程序員認爲一個表達式在實際操作中,無論使用寄存器方式、內存方式還是立即數方式都無所謂時,或者讓GCC可以根據不同的表達式生成不同的訪問方式時,就可以使用通用約束“g”。
約束名“0,1,2,3,4,5,6,7,8,9”只能用於輸入,表示與第n個操作表達式使用相同的寄存器/內存;

修飾符
等號(=)和加號(+)作爲修飾符,已經在輸出部分講解過了,這裏主要講解“&”符。

符號“&”也只能寫在輸出表達式的約束部分,用於約束寄存器的分配,但是隻能寫在約束部分的第二個字符的位置上。因爲,第一個字符的位置我們要寫(=)或(+)。

用符號“&”進行修飾時,等於向GCC聲明:“GCC不得爲任何輸入操作表達式分配與此輸出操作表達式相同的寄存器”;其原因是,GCC會先使用輸出值對被修飾符“&”修飾的輸出操作表達式進行賦值,然後,纔對輸入操作表達式進行賦值。這樣的話,如果不使用修飾符“&”對輸出操作表達式進行修飾,一旦後面的輸入操作表達式使用了與輸出操作表達式相同的寄存器,就會產生輸入和輸出數據混亂的情況;

值得注意的是:如果一個輸出操作表達式的寄存器約束被指定爲某個寄存器,只有當至少存在一個輸入操作表達式的寄存器約束爲可選約束(意思是GCC可以從多個寄存器中選取一個,或使用非寄存器方式)時,比如“r”或“g”時,此輸出操作表達式使用符號“&”修飾纔有意義!如果你爲所有的輸入操作表達式指定了固定的寄存器,或使用內存/立即數約束時,則此輸出操作表達式使用符號“&”修飾沒有任何意義;

如果沒有使用修飾符“&”修飾輸出操作表達式會是什麼樣子呢?那就意味着GCC會先把輸入操作表達式的值輸入到選定的寄存器中,然後經過處理,最後才用輸出值填充對應的輸出操作表達式;

佔位符
每一個佔位符對應一個輸入/輸出操作表達式,內嵌彙編中有兩種佔位符:序號佔位符和名稱佔位符;

1、序號佔位符
GCC規定:一個內嵌彙編語句中最多只能有10個輸入/輸出操作表達式,這些操作表達式按照他們被列出來的順序依次賦予編號0到9;對於佔位符中的數字而言,與這些編號是對應的;比如:佔位符%0對應編號爲0的操作表達式,佔位符%1對應編號爲1的操作表達式,依次類推;

由於佔位符前面要有一個百分號“%”,爲了區別佔位符與寄存器,GCC規定:在內嵌彙編語句的指令列表裏列出的寄存器名稱前面必須使用兩個百分號(%%),以區別於佔位符語法。GCC對佔位符進行編譯的時候,會將每一個佔位符替換爲對應的輸入/輸出操作表達式所指定的寄存器/內存/立即數;

2、名稱佔位符
由於GCC中限制這種佔位符的個數最多隻能由這10個,這也就限制了輸入/輸出操作表達式的數量做多只能有10個;如果需要的表達式的數量超過10個,那麼,這些需要佔位符就不夠用了;GCC內嵌彙編提供了名稱佔位符來解決這個問題;即:使用一個名字字符串與一個表達式對應;這個名字字符串就稱爲名稱佔位符;而這個名字通常使用與表達式中的變量完全相同的名字;

使用名字佔位符時,內嵌彙編的輸入/輸出操作表達式中的格式如下:

[name] "constraint"(變量)  

此時,指令列表中的佔位符的書寫格式如下:

%[name]  

這個格式等價於序號佔位符中的%0,%1,%2等等,使用名稱佔位符時,一個name對應一個變量;

例如:

__asm__("imull %[value1],%[value2]"
        :[value2] "=r"(data2)
        :[value1] "r"(data1),"0"(data2));  

此例中,名稱佔位符value1就對應變量data1,名稱佔位符value2對應變量data2;GCC編譯的時候,同樣會把這兩個佔位符分別替換成對應的變量所使用的寄存器/內存地址/立即數;而且也增強了代碼的可讀性;
這個例子,使用序號佔位符的寫法如下:

__asm__("imull %1,%0"
        :"=r"(data2)
        :"r"(data1),"0"(data2));
1.4 損壞部分

有的時候,當您想通知GCC當前內嵌彙編語句可能會對某些寄存器或內存進行修改,希望GCC在編譯時能夠將這一點考慮進去;那麼您就可以在損壞部分聲明這些寄存器或內存;

寄存器修改通知
這種情況一般發生在一個寄存器出現在指令列表中,但又不是輸入/輸出操作表達式所指定的,也不是在一些輸入/輸出操作表達式中使用“r”或“g”約束時由GCC選擇的。同時,此寄存器被指令列表中的指令所修改,而這個寄存器只供當前內嵌彙編語句使用;比如:

__asm__("movl %0,%%ebx"::"a"(__foo):"bx");  

這個內嵌彙編語句中,%ebx出現在指令列表中,並且被指令修改了,但是卻未被任何輸入/輸出操作表達式所指定。所以,您需要在損壞部分指定“bx”,以讓GCC知道這一點。

在損壞部分聲明這些寄存器的方法很簡單,只需要將寄存器的名字用雙引號括起來就可以了;如果要聲明多個寄存器,則相鄰兩個寄存器名字之間用逗號隔開。

注意:因爲你在輸入/輸出操作表達式中指定寄存器,或當你爲一些輸入/輸出操作表達式使用“r”/“g”約束,讓GCC爲你選擇一個寄存器時,GCC對這些寄存器的狀態是非常清楚的,它知道這些寄存器是被修改的,你根本不需要在損壞部分聲明它們;但除此之外,GCC對剩下的寄存器中哪些會被當前內嵌彙編語句所修改卻一無所知;所以,如果你真的在當前內嵌彙編指令中修改了它們,那麼就最好在損壞部分聲明它們,讓GCC針對這些寄存器做相應的處理;否則,有可能會造成寄存器不一致,從而造成程序執行錯誤;

寄存器名稱串: 
“al”/“ax”/“eax”:代表寄存器%eax 
“bl”/“bx”/“ebx”:代表寄存器%ebx 
“cl”/“cx”/“ecx”:代表寄存器%ecx 
“dl”/“dx”/“edx”:代表寄存器%edx 
“si”/“esi”:代表寄存器%esi 
“di”/“edi”:代表寄存器%edi

所以,只需要使用“ax”,“bx”,“cx”,“dx”,“si”,“di”就可以了,因爲他們都代表對應的寄存器;

如果你在一個內嵌彙編語句的損壞部分向GCC聲明瞭某個寄存器會發生改變。那麼,在GCC編譯時,如果發現這個被聲明的寄存器的內容在此內嵌彙編之後還要繼續使用,GCC會首先將此寄存器的內容保存起來,然後在此內嵌彙編語句的相關代碼生成之後,再將其內容恢復。

另外需要注意的是,如果你在損壞部分聲明瞭一個寄存器,那麼這個寄存器將不能再被用作當前內嵌彙編語句的輸入/輸出操作表達式的寄存器約束,如果輸入/輸出操作表達式的寄存器約束被指定爲“r”/“g”,GCC也不會選擇已經被聲明在損壞部分中的寄存器;

摘抄網站:https://blog.csdn.net/tyhh2001/article/details/44351797

附錄A

https://www.cnblogs.com/scu-cjx/p/6878222.html

https://blog.csdn.net/qq_29343201/article/details/52199533

http://blog.chinaunix.net/uid-21602837-id-1823631.html

https://www.cnblogs.com/rain-blog/p/gnu-gcc-insert-asm.html

http://www.ethernut.de/en/documents/arm-inline-asm.html

https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章