原文鏈接:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html ,此文爲我的中文翻譯,轉載請標明出處!
GCC-Inline-Assembly-HOWTO
Sandeep.S
本HOWTO解釋了GCC提供的內聯彙編功能的使用方法。閱讀本文只有兩個先決條件,是x86彙編語言和C的基本知識。
1. 簡介
2. 概述
3. GCC彙編語法
4. 基本內聯
5. 擴展的Asm
6. 更多關於約束
7. 一些有用的方法
8. 結束語
9. 參考文獻
1. 簡介
1.1版權和許可
版權所有(C)2003 Sandeep S.
這份文件是免費的; 您可以根據自由軟件基金會發布的GNU通用公共許可證條款重新分發和/或修改此內容; 許可證的第2版,或(根據您的選擇)任何更高版本。
本文件的分發是希望它有用,但沒有任何擔保; 甚至沒有適銷性或特定用途適用性的暗示保證。有關更多詳細信息,請參閱GNU通用公共許可證。
1.2反饋和更正
請向Sandeep.S提出反饋和批評 。我將感謝任何指出本文件中的錯誤和不準確之處的人; 我得到通知後,我會盡快糾正他們。
1.3致謝
我衷心感謝GNU人員提供了這樣一個偉大的功能。感謝Mr.Pramode CE所做的一切幫助。感謝Govt Engineering College的朋友,Trichur的道義支持與合作,特別是對Nisha Kurur和Sakeeb S.感謝我在Govt Engineering College的親愛的老師,Trichur的合作。
另外,感謝Phillip,Brennan Underwood和[email protected]; 這裏的許多東西都是從他們的作品中無恥地偷走的。
2. 概述
我們在這裏瞭解GCC內聯彙編。這個內聯代表什麼?
我們可以指示編譯器將函數的代碼插入其調用者的代碼中,直到實際調用的位置。這些函數是內聯函數。聽起來像宏?確實有相似之處。
內聯函數有什麼好處?
這種內聯方法減少了函數調用開銷。如果任何實際參數值是常量,則它們的已知值可能允許在編譯時進行簡化,因此不需要包含所有內聯函數的代碼。對代碼大小的影響不太可預測,這取決於具體情況。要聲明內聯函數,我們必須inline
在其聲明中使用關鍵字 。
現在我們可以猜猜什麼是內聯彙編。它只是一些編寫爲內聯函數的彙編程序。它們在系統編程中非常方便,快速且非常有用。我們的主要重點是研究(GCC)內聯彙編函數的基本格式和用法。要聲明內聯彙編函數,我們使用關鍵字asm
。
內聯彙編很重要,主要是因爲它能夠操作並使其輸出在C變量上可見。由於這種能力,“asm”作爲彙編指令和包含它的“C”程序之間的接口。
3. GCC彙編語法
GCC是用於Linux的GNU C編譯器,它使用AT&T / UNIX 彙編語法。這裏我們將使用AT&T語法進行彙編編碼。如果您不熟悉AT&T語法,請不要擔心,我會教您。這與Intel語法完全不同。我將給出重大分歧。
- Source-Destination順序。
AT&T語法中操作數的方向與英特爾的方向相反。在Intel語法中,第一個操作數是目標,第二個操作數是源,而在AT&T語法中,第一個操作數是源,第二個操作數是目標。即
Intel語法中的“Op-code dst src”更改爲
AT&T語法中的“Op-code src dst”。
- 註冊命名。
寄存器名稱以%爲前綴,即,如果要使用eax,則寫入%eax。
- 即時操作數。
AT&T的即時操作數前面是'$'。對於靜態“C”變量,前綴爲'$'。在Intel語法中,對於十六進制常量,'h'是後綴,這裏我們將'0x'加到常量前面。因此,對於十六進制,我們首先看到'$',然後是'0x',最後是常量。
- 操作數大小。
在AT&T語法中,存儲器操作數的大小由操作碼名稱的最後一個字符確定。“b”,“w”和“l”的操作碼後綴指定字節(8位),字(16位)和長(32位)存儲器引用。Intel語法通過在內存操作數(不是操作碼)前加上'byte ptr','word ptr'和'dword ptr'來實現這一點。
因此,英特爾“mov al,byte ptr foo”在AT&T語法中是“movb foo,%al”。
- 內存操作數。
在Intel語法中,基址寄存器包含在'['和']'中,而在AT&T中它們更改爲'('和')'。此外,在Intel語法中,間接內存引用就像
section:[base + index * scale + disp],改爲
section:AT&T中的disp(基數,指數,比例)。
需要記住的一點是,當一個常量用於disp / scale時,'$'不應該是前綴。
現在我們看到了英特爾語法和AT&T語法之間的一些主要差異。我只寫了一些。有關完整信息,請參閱GNU彙編程序文檔。現在我們將看一些例子以便更好地理解。
+------------------------------+------------------------------------+ | Intel Code | AT&T Code | +------------------------------+------------------------------------+ | mov eax,1 | movl $1,%eax | | mov ebx,0ffh | movl $0xff,%ebx | | int 80h | int $0x80 | | mov ebx, eax | movl %eax, %ebx | | mov eax,[ecx] | movl (%ecx),%eax | | mov eax,[ebx+3] | movl 3(%ebx),%eax | | mov eax,[ebx+20h] | movl 0x20(%ebx),%eax | | add eax,[ebx+ecx*2h] | addl (%ebx,%ecx,0x2),%eax | | lea eax,[ebx+ecx] | leal (%ebx,%ecx),%eax | | sub eax,[ebx+ecx*4h-20h] | subl -0x20(%ebx,%ecx,0x4),%eax | +------------------------------+------------------------------------+
4. 基本內聯
基本內聯彙編的格式非常簡單。它的基本形式是
asm("assembly code");
例如。
asm("movl %ecx %eax"); /* moves the contents of ecx to eax */ __asm__("movb %bh (%eax)"); /*moves the byte from bh to the memory pointed by eax */
您可能已經注意到我在這裏使用過asm
和__asm__
。兩者都有效。如果關鍵字asm
與我們程序中的某些內容衝突,我們可以使用__asm__
。如果我們有多個指令,我們用雙引號每行寫一個,並在指令後面加上'\ n'和'\ t'。這是因爲gcc將每個指令作爲一個字符串發送至彙編器。
例如。
__asm__ ("movl %eax, %ebx\n\t" "movl $56, %esi\n\t" "movl %ecx, $label(%edx,%ebx,$4)\n\t" "movb %ah, (%ebx)");
如果在我們的代碼中,我們觸碰(即,更改內容)一些寄存器並從asm返回而不修復這些更改,則會發生一些不好的事情。這是因爲GCC不知道寄存器內容的變化,這導致我們遇到麻煩,特別是當編譯器進行一些優化時。它會假設某些寄存器包含某些變量的值,我們可能在沒有通知GCC的情況下對其進行了更改,並且它仍然沒有發生任何事情。我們可以做的是使用那些沒有副作用的指令或在我們退出或等待某些事情崩潰時解決問題。這是我們想要一些擴展功能的地方。擴展的asm爲我們提供了該功能。
5. 擴展的Asm
在基本的內聯彙編中,我們只有指令。在擴展彙編中,我們還可以指定操作數。它允許我們指定輸入寄存器,輸出寄存器和破壞寄存器列表。指定要使用的寄存器並不是強制性的,我們可以將這個問題留給GCC,這可能更適合GCC的優化方案。無論如何,基本格式是:
asm ( assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
彙編程序模板由彙編指令組成。每個操作數由操作數約束字符串描述,後跟括號中的C表達式。冒號將彙編程序模板與第一個輸出操作數分隔開,另一個冒號將最後一個輸出操作數與第一個輸入分開,如果有的話。逗號分隔每個組中的操作數。操作數的總數限制爲10或機器描述中任何指令模式中的最大操作數數量,以較大者爲準。
如果沒有輸出操作數但是有輸入操作數,則必須在輸出操作數所在的位置周圍放置兩個連續的冒號。
例如:
asm ("cld\n\t" "rep\n\t" "stosl" : /* no output registers */ : "c" (count), "a" (fill_value), "D" (dest) : "%ecx", "%edi" );
現在,這段代碼做了什麼?上面的內聯將fill_value的count
倍填充到edi
寄存器指向的位置。它還告訴gcc的是,寄存器的內容eax
和edi
不再有效。讓我們再看一個例子,讓事情更清晰。
int a=10, b; asm ("movl %1, %%eax; movl %%eax, %0;" :"=r"(b) /* output */ :"r"(a) /* input */ :"%eax" /* clobbered register */ );
這裏我們做的是使用匯編指令使'b'的值等於'a'的值。一些興趣點是:
- “b”是輸出操作數,由%0引用,“a”是輸入操作數,由%1引用。
- “r”是對操作數的約束。我們稍後會詳細介紹約束。目前,“r”向GCC表示使用任何寄存器來存儲操作數。輸出操作數約束應該有一個約束脩飾符“=”。而這個修飾符表示它是輸出操作數並且是隻寫的。
- 寄存器名稱前面有兩個%的前綴。這有助於GCC區分操作數和寄存器。操作數具有單個%作爲前綴。
- 第三個冒號後的修改後的寄存器%eax告訴GCC%eax的值將在“asm”內修改,因此GCC不會使用該寄存器來存儲任何其他值。
當“asm”的執行完成時,“b”將反映更新的值,因爲它被指定爲輸出操作數。換句話說,“asm”中對“b”的改變應該反映在“asm”之外。
現在我們可以詳細查看每個字段。
5.1彙編模板
彙編程序模板包含插入C程序內的彙編指令集。格式如下:要麼每條指令都用雙引號括起來,要麼整個指令組都在雙引號內。每條指令也應以分隔符結束。有效分隔符是換行符(\ n)和分號(;)。'\ n'後面可能跟一個標籤(\ t)。對應於C表達式的操作數由%0,%1 ......等表示。
5.2操作數
C表達式用作“asm”中的彙編指令的操作數。每個操作數都被寫爲雙引號中的第一個操作數約束。對於輸出操作數,在引號內也會有一個約束脩飾符,然後是C表達式,它代表操作數。即
“約束”(C表達式)是一般形式。對於輸出操作數,將有一個額外的修飾符。約束主要用於決定操作數的尋址模式。它們還用於指定要使用的寄存器。
如果我們使用多個操作數,則用逗號分隔。
在彙編程序模板中,每個操作數都由數字引用。編號如下進行。如果總共有n個操作數(包括輸入和輸出),則第一個輸出操作數編號爲0,按遞增順序繼續,最後一個輸入操作數編號爲n-1。最大操作數是我們在上一節中看到的。
輸出操作數表達式必須是左值。輸入操作數不受此限制。他們可能是表達式。擴展的asm功能最常用於編譯器本身不知道存在的機器指令;-)。如果無法直接尋址輸出表達式(例如,它是位字段),則我們的約束必須允許寄存器。在這種情況下,GCC將使用寄存器作爲asm的輸出,然後將該寄存器內容存儲到輸出中。
如上所述,普通輸出操作數必須是隻寫的; GCC將假設在指令之前這些操作數中的值已經死亡且無需生成。擴展的asm還支持輸入輸出或讀寫操作數。
所以現在我們專注於一些例子。我們想要將數字乘以5。爲此,我們使用指令lea
。
asm ("leal (%1,%1,4), %0" : "=r" (five_times_x) : "r" (x) );
這裏我們的輸入是'x'。我們沒有指定要使用的寄存器。GCC將選擇一些輸入寄存器,一個用於輸出,並按我們的意願行事。如果我們希望輸入和輸出駐留在同一個寄存器中,我們可以指示GCC這樣做。這裏我們使用那些類型的讀寫操作數。通過指定適當的約束,例如。
asm ("leal (%0,%0,4), %0" : "=r" (five_times_x) : "0" (x) );
現在輸入和輸出操作數在同一個寄存器中。但是我們不知道哪個寄存器。現在,如果我們也要指定它,那麼有一種方法。
asm ("leal (%%ecx,%%ecx,4), %%ecx" : "=c" (x) : "c" (x) );
在上面的三個例子中,我們沒有將任何寄存器放入clobber列表。爲什麼?在前兩個例子中,GCC決定寄存器,它知道發生了什麼變化。在最後一個,我們沒有必要把ecx
加入clobber-list,gcc知道它進入了x。因此,因爲它可以知道ecx
的值,所以它不被認爲是破壞的。
5.3 Clobber列表
一些指令破壞了一些硬件寄存器。我們必須在clobber-list中列出這些寄存器,即asm函數中第三個' : ' 之後的字段。這是爲了告知gcc我們將自己使用和修改它們。所以gcc不會假設它加載到這些寄存器中的值是有效的。我們不應該在此列表中列出輸入和輸出寄存器。因爲,gcc知道“asm”使用它們(因爲它們被明確指定爲約束)。如果指令隱式或顯式地使用任何其他寄存器(並且輸入或輸出約束列表中不存在寄存器),則必須在破壞列表中指定這些寄存器。
如果我們的指令可以改變條件代碼寄存器,我們必須將“cc”添加到破壞寄存器列表中。
如果我們的指令以不可預測的方式修改內存,請將“memory”添加到修飾寄存器列表中。這將導致GCC不在彙編器指令的寄存器中保持緩存的內存值。如果受影響的內存未在asm的輸入或輸出中列出,我們還必須添加volatile關鍵字。
我們可以根據需要多次讀取和編寫被破壞的寄存器。考慮模板中多個指令的示例; 它假定子程序_foo接收在寄存器eax
和ecx
參數的參數。
asm ("movl %0,%%eax; movl %1,%%ecx; call _foo" : /* no outputs */ : "g" (from), "g" (to) : "eax", "ecx" );
5.4Volatile......?
如果您熟悉內核源代碼或類似的一些漂亮的代碼,您必須已經看到許多函數聲明爲volatile
或 __volatile__
。我之前提到過關鍵字asm
和__asm__
。那這 volatile
是什麼?
如果我們的彙編語句必須在我們放置的地方執行,(即不能作爲優化移出循環),請將關鍵字volatile
放在asm之後和()之前。我們將其聲明爲
asm volatile ( ... : ... : ... : ...);
使用__volatile__
的時候,我們必須非常小心。
如果我們的程序集只是用於進行一些計算並且沒有任何副作用,那麼最好不要使用關鍵字volatile
。避免gcc無法優化代碼。
在 一些有用的方法 中,我提供了許多內聯asm函數的示例。在那裏我們可以看到詳細的clobber列表。
6. 更多關於約束
到目前爲止,您可能已經理解約束與內聯彙編有很大關係。但是我們對限制很少說。約束可以說明操作數是否在寄存器中,以及哪種寄存器; 操作數是否可以作爲內存引用,以及哪種地址; 操作數是否可以是立即常量,以及它可能具有哪些可能的值(即值的範圍)....等等。
6.1常用約束。
存在許多約束,其中僅頻繁使用少數約束。我們將看看這些約束。
- 註冊操作數約束(r)
使用此約束指定操作數時,它們將存儲在通用寄存器(GPR)中。採用以下示例:
asm ("movl %%eax, %0\n" :"=r"(myval));
這裏變量myval保存在寄存器中,寄存器中的值
eax
被複制到該寄存器中,並且值myval
從該寄存器更新到存儲器中。當指定“r”約束時,gcc可以將變量保存在任何可用的GPR中。要指定寄存器,必須使用特定的寄存器約束直接指定寄存器名稱。他們是:+---+--------------------+ | r | Register(s) | +---+--------------------+ | a | %eax, %ax, %al | | b | %ebx, %bx, %bl | | c | %ecx, %cx, %cl | | d | %edx, %dx, %dl | | S | %esi, %si | | D | %edi, %di | +---+--------------------+
- 內存操作數約束(m)
當操作數在存儲器中時,對它們執行的任何操作將直接發生在存儲器位置,而不是寄存器約束,寄存器約束首先將值存儲在要修改的寄存器中,然後將其寫回存儲器位置。但是寄存器約束通常僅在它們對於指令絕對必要時才使用,或者它們顯著加速了該過程。在需要在“asm”內更新C變量並且您真的不想使用寄存器來保存其值時,可以最有效地使用內存約束。例如,idtr的值存儲在內存位置loc中:
asm("sidt %0\n" : :"m"(loc));
- 匹配(數字)約束
在某些情況下,單個變量可以作爲輸入和輸出操作數。可以通過使用匹配約束在“asm”中指定這種情況。
asm ("incl %0" :"=a"(var):"0"(var));
我們在操作數小節中也看到了類似的例子。在此示例中,匹配約束,寄存器%eax用作輸入和輸出變量。var輸入讀取到%eax,更新後%eax在增量後再次存儲在var中。這裏的“0”指定與第0個輸出變量相同的約束。也就是說,它指定var的輸出實例應僅存儲在%eax中。可以使用此約束:
- 在從變量讀取輸入或修改變量並將修改寫回同一變量的情況下。
- 如果不需要輸入和輸出操作數的單獨實例。
使用匹配約束的最重要的影響是它們導致有效使用可用寄存器。
使用的一些其他約束是:
- “m”:允許使用內存操作數,以及機器通常支持的任何類型的地址。
- “o”:允許使用內存操作數,但前提是該地址是可偏移的。即,向地址添加一個小偏移量會給出一個有效的地址。
- “V”:不可偏移的內存操作數。換句話說,任何符合“m”約束但不符合“o”約束的東西。
- “i”:允許立即整數操作數(具有常量值的操作數)。這包括符號常量,其值僅在彙編時才知道。
- “n”:允許具有已知數值的立即整數操作數。許多系統不能支持小於字寬的操作數的彙編時常量。這些操作數的約束應該使用'n'而不是'i'。
- “g”:允許任何寄存器,存儲器或立即整數操作數,但非通用寄存器的寄存器除外。
以下約束是x86特定的。
- “r”:註冊操作數約束,查看上面給出的表。
- “q”:註冊a,b,c或d。
- “I”:範圍爲0到31的常量(對於32位移位)。
- “J”:範圍爲0到63(對於64位移位)的常量。
- “K”:0xff。
- “L”:0xffff。
- “M”:0,1,2或3(lea指令的移位)。
- “N”:範圍爲0到255的常量(用於輸出指令)。
- “f”:浮點寄存器
- “t”:第一個(堆棧頂部)浮點寄存器
- “u”:第二個浮點寄存器
- “A”:指定“a”或“d”寄存器。這對於64位整數值非常有用,這些值旨在通過保存最高有效位的“d”寄存器和保存最低有效位的“a”寄存器返回。
6.2約束脩飾符
在使用約束時,爲了更精確地控制約束的影響,GCC爲我們提供了約束脩飾符。最常用的約束脩飾符是
- “=”:表示該操作數對該指令是隻寫的; 先前的值被丟棄並由輸出數據替換。
- “&”:表示此操作數是一個earlyclobber操作數,在使用輸入操作數完成指令之前修改該操作數。因此,該操作數可能不在於用作輸入操作數的寄存器或任何存儲器地址的一部分。如果輸入操作數僅用作輸入,則在寫入早期結果之前,它可以綁定到earlyclobber操作數。
約束的列表和解釋絕不是完整的。示例可以更好地理解內聯asm的使用和使用。在下一節中,我們將看到一些示例,我們將在其中找到有關clobber-lists和約束的更多信息。
7. 一些有用的方法。
現在我們已經介紹了關於GCC內聯彙編的基本理論,現在我們將集中討論一些簡單的例子。將內聯asm函數編寫爲MACRO總是很方便。我們可以在內核代碼中看到許多asm函數。(/usr/src/linux/include/asm/*.h)。
-
首先,我們從一個簡單的例子開始。我們將編寫一個程序來相加兩個數字。
int main(void) { int foo = 10, bar = 15; __asm__ __volatile__("addl %%ebx,%%eax" :"=a"(foo) :"a"(foo), "b"(bar) ); printf("foo+bar=%d\n", foo); return 0; }
在這裏,我們堅持要求GCC在%eax中存儲foo,bar存儲在%ebx中,我們也希望結果保存在%eax中。'='符號表示它是輸出寄存器。現在我們可以用其他方式實現變量和整數相加。
__asm__ __volatile__( " lock ;\n" " addl %1,%0 ;\n" : "=m" (my_var) : "ir" (my_int), "m" (my_var) : /* no clobber-list */ );
這是一個原子加法。我們可以刪除指令'lock'來刪除原子性。在輸出字段中,“= m”表示my_var是輸出,它在內存中。類似地,“ir”表示,my_int是一個整數,應該駐留在某個寄存器中(回想一下我們上面看到的表)。clobber列表中沒有寄存器。
-
現在我們將對一些寄存器/變量執行一些操作並比較該值。
__asm__ __volatile__( "decl %0; sete %1" : "=m" (my_var), "=q" (cond) : "m" (my_var) : "memory" );
這裏,my_var的值減1,如果結果值是
0
,則設置變量cond。我們可以通過添加指令“lock; \ n \ t”作爲彙編程序模板中的第一條指令來添加原子性。以類似的方式,我們可以使用“incl%0”而不是“decl%0”,以便增加my_var。
這裏要注意的是(i)my_var是駐留在內存中的變量。(ii)約束“= q”保證了cond在eax,ebx,ecx和edx寄存器其中之一中。(iii)我們可以看到內存在clobber列表中。即,代碼正在改變內存的內容。
-
如何設置/清除寄存器中的位?作爲下一個方法,我們將會看到它。
__asm__ __volatile__( "btsl %1,%0" : "=m" (ADDR) : "Ir" (pos) : "cc" );
這裏,ADDR變量位置'pos'處的位(存儲器變量)設置爲
1,
我們可以使用'btrl'代替'btsl'來清除該位。pos的約束“Ir”表示pos位於寄存器中,其值的範圍爲0-31(x86依賴約束)。也就是說,我們可以在ADDR設置/清除變量的第0到第31位。由於條件代碼將被更改,我們將“cc”添加到clobberlist。 -
現在我們來看一些更復雜但有用的功能。字符串副本。
static inline char * strcpy(char * dest,const char *src) { int d0, d1, d2; __asm__ __volatile__( "1:\tlodsb\n\t" "stosb\n\t" "testb %%al,%%al\n\t" "jne 1b" : "=&S" (d0), "=&D" (d1), "=&a" (d2) : "0" (src),"1" (dest) : "memory"); return dest; }
源地址存儲在esi中,目標位於edi中,然後啓動複製,當我們達到0時,複製完成。約束“&S”,“&D”,“&a”表示寄存器esi,edi和eax是early clobber寄存器,即它們的內容將在函數完成之前改變。這裏也很清楚爲什麼memory在clobberlist中。
我們可以看到一個類似的函數移動一個double words。請注意,該函數聲明爲宏。
#define mov_blk(src, dest, numwords) \ __asm__ __volatile__ ( \ "cld\n\t" \ "rep\n\t" \ "movsl" \ : \ : "S" (src), "D" (dest), "c" (numwords) \ : "%ecx", "%esi", "%edi" \ )
這裏我們沒有輸出,因此寄存器ecx,esi和edi的內容發生的變化是塊移動的副作用。所以我們必須將它們添加到clobber列表中。
-
在Linux中,使用GCC內聯彙編實現系統調用。讓我們看看如何實現系統調用。所有系統調用都寫成宏(linux / unistd.h)。例如,具有三個參數的系統調用被定義爲宏,如下所示。
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1,type2 arg2,type3 arg3) \ { \ long __res; \ __asm__ volatile ( "int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3))); \ __syscall_return(type,__res); \ }
每當進行具有三個參數的系統調用時,上面顯示的宏用於進行調用。系統調用號放在eax中,然後是ebx,ecx,edx中的每個參數。最後,“int 0x80”是使系統調用工作的指令。可以從eax收集返回值。
每個系統調用都以類似的方式實現。退出是一個單個參數系統調用,讓我們看看它的代碼是什麼樣的。它如下所示。
{ asm("movl $1,%%eax; /* SYS_exit is 1 */ xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */ int $0x80" /* Enter kernel mode */ ); }
退出的數量是“1”,這裏,它的參數是0。所以我們安排eax包含1和ebx包含0和by
int $0x80
,exit(0)
執行。這就是退出的方式。
8. 結束語
本文檔介紹了GCC內聯彙編的基礎知識。一旦理解了基本概念,就不難採取自己的步驟。我們看到了一些有助於理解GCC內聯彙編常用功能的示例。
GCC內聯是一個廣泛的主題,本文絕不是完整的。關於我們討論的語法的更多細節可以在GNU Assembler的官方文檔中找到。同樣,有關約束的完整列表,請參閱GCC的官方文檔。
當然,Linux內核大規模使用GCC Inline。所以我們可以在內核源代碼中找到各種各樣的例子。他們可以幫助我們很多。
如果您在本文檔中發現任何明顯錯別字或過時信息,請告知我們。
9. 參考文獻