文章來自這里:gcc內聯匯編......
在閱讀Linux內核源碼或對代碼做性能優化時,經常會有在C語言中嵌入一段匯編代碼的需求,這種嵌入匯編在CS術語上叫做inline assembly
。本文的筆記試圖說明Inline Assembly
的基本語法規則和用法(建議英文閱讀能力較強的同學直接閱讀本文參考資料中推薦的技術文章^_^
)。
注意:由于gcc
采用AT&T風格的匯編語法(與Intel Syntax相對應,二者的區別參見這里),因此,本文涉及到的匯編代碼均以AT&T Syntax為準。
1. 基本語法規則
內聯匯編(或稱嵌入匯編)的基本語法模板比較簡單,如下所示(為使結構更清晰,這里特意做了換行,其實完全可以全部寫到單行中):
asm [ volatile ] (
assembler template
[ : output operands ] /* optional */
[ : input operands ] /* optional */
[ : list of clobbered registers ] /* optional */
);
備注:本文遵從linux系統的統一風格,以[ ]
來表示其對應的內容為可選項。
由代碼模板可以看到,基本語法規則由5部分組成,下面分別進行說明。
1)關鍵字asm
和volatile
asm
為gcc
關鍵字,表示接下來要嵌入匯編代碼。為避免keyword asm
與程序中其它部分產生命名沖突,gcc
還支持__asm__
關鍵字,與asm
的作用等價。
volatile
為可選關鍵字,表示不需要gcc
對下面的匯編代碼做任何優化。同樣出于避免命名沖突的原因,__volatile__
也是gcc
支持的與volatile
等效的關鍵字。
BTW
: C語言中也經常用到volatile關鍵字來修飾變量(不熟悉的同學,請參考這里)
2)assembler template
這部分即我們要嵌入的匯編命令,由于我們是在C
語言中內聯匯編代碼,故需用雙引號""
將命令括起來,以便gcc
以字符串形式將這些命令傳給匯編器AS
。例如可以寫成這樣:movl %eax, %ebx
。
有時候,匯編命令可能有多個,則通常分多行寫,每行的命令都用雙引號括起來,命令后緊跟\n\t
之類的分隔符(當然,也可以只用1對雙引號將多行命令括起來,從語法來說,兩種寫法均有效,我們可自行決定用哪種格式來寫)。示例代碼如下所示:
__asm__ __volatile__ ( "movl %eax, %ebx\n\t"
"movl %ecx, 2(%edx, %ebx, $8)\n\t"
"movb %ah, (%ebx)"
);
還有時候,根據程序上下文,嵌入的匯編代碼中可能會出現一些類似于魔數(Magic Number )的操作數,比如下面的代碼:
int a=10, b;
asm ("movl %1, %%eax; /* NOTICE: 下面會說明此處用%%eax引用寄存器eax的原因
movl %%eax, %0;"
:"=r"(b) /* output 該字段的語法后面會詳細說明,此處可無視,下同 */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
我們看到,movl
指令的操作數(operand)中,出現了%1
、%0
,這往往讓新手摸不著頭腦。其實只要知道下面的規則就不會產生疑惑了:
在內聯匯編中,操作數通常用數字來引用,具體的編號規則為:若命令共涉及n個操作數,則第1個輸出操作數(the first output operand)被編號為0,第2個output operand編號為1,依次類推,最后1個輸入操作數(the last input operand)則被編號為n-1。
具體到上面的示例代碼中,根據上下文,涉及到2
個操作數變量a
、b
,這段匯編代碼的作用是將a
的值賦給b
,可見,a
是input operand
,而b
是output operand
,那么根據操作數的引用規則,不難推出,a
應該用%1
來引用,b
應該用%0
來引用。
還需要說明的是:當命令中同時出現寄存器和以%num
來引用的操作數時,會以%%reg
來引用寄存器(如上例中的%%eax
),以便幫助gcc
來區分寄存器和由C
語言提供的操作數。
3)output operands
該字段為可選項,用以指明輸出操作數,典型的格式為:
`: "=a" (out_var)`
其中,"=a"
指定output operand
的應遵守的約束(constraint),out_var
為存放指令結果的變量,通常是個C
語言變量。本例中,“=”
是output operand
字段特有的約束,表示該操作數是只寫的(write-only);“a”
表示先將命令執行結果輸出至%eax
,然后再由寄存器%eax
更新位于內存中的out_var
。關于常用的約束規則,本文后面會給出說明。
若輸出有多個,則典型格式示例如下:
asm ( "cpuid"
: "=a" (out_var1), "=b" (out_var2), "=c" (out_var3)
: "a" (op)
);
可見,我們可以為每個output operand指定其約束。
4)input operands
該字段為可選項,用以指明輸入操作數,其典型格式為:
: "constraints" (in_var)
其中,constraints
可以是gcc
支持的各種約束方式,in_var
通常為C語言提供的輸入變量。
與output operands
類似,當有多個input
時,典型格式為:
: "constraints1" (in_var1), "constraints2" (in_var2), "constraints3" (in_var3), ...
當然,input operands + output operands
的總數通常是有限制的,考慮到每種指令集體系結構對其涉及到的指令支持的最多操作數通常也有限制,此處的操作數限制也不難理解。此處具體的上限為max(10, max_in_instruction)
,其中max_in_instruction
為ISA
中擁有最多操作數的那條指令包含的操作數數目。
需要明確的是,在指明input operands
的情況下,即使指令不會產生output operands
,其:也需要給出。例如asm ("sidt %0\n" : :"m"(loc))
; 該指令即使沒有具體的output operands
也要將:
寫全,因為有后面跟著: input operands
字段。
5)list of clobbered registers
該字段為可選項,用于列出指令中涉及到的且沒出現在output operands
字段及input operands
字段的那些寄存器。若寄存器被列入clobber-list
,則等于是告訴gcc
,這些寄存器可能會被內聯匯編命令改寫。因此,執行內聯匯編的過程中,這些寄存器就不會被gcc
分配給其它進程或命令使用。
2. 常用約束(commonly used constraints
)
前面介紹output operands
和input operands
字段過程中,我們已經知道這些operands通常需要指明各自的constraints
,以便更明確地完成我們期望的功能(試想,如果不明確指定約束而由gcc自行決定的話,一旦代碼執行結果不符合預期,調試將變得很困難)。
下面開始介紹一些常用的約束項。
1)寄存器操作數約束(register operand
constraint,
r) 當操作數被指定為這類約束時,表明匯編指令執行時,操作數被將存儲在指定的通用寄存器(
General Purpose Registers,
GPR`)中。例如:
asm ("movl %%eax, %0\n" : "=r"(out_val));
該指令的作用是將%eax
的值返回給%0
所引用的C語言變量out_val
,根據"=r
"約束可知具體的操作流程為:先將%eax
值復制給任一GPR
,最終由該寄存器將值寫入%0
所代表的變量中。"r"
約束指明gcc
可以先將%eax
值存入任一可用的寄存器,然后由該寄存器負責更新內存變量。
通常還可以明確指定作為“中轉”的寄存器,約束參數與寄存器的對應關系為:
a : %eax, %ax, %al
b : %ebx, %bx, %bl
c : %ecx, %cx, %cl
d : %edx, %dx, %dl
S : %esi, %si
D : %edi, %di
例如,如果想指定用%ebx作為中轉寄存器,則命令為:
asm ("movl %%eax, %0\n" : "=b"(out_val));
2)內存操作數約束(Memory operand constraint
,m
)
當我們不想通過寄存器中轉,而是直接操作內存時,可以用"m"來約束。例如:
asm volatile ( "lock; decl %0" : "=m" (counter) : "m" (counter));
該指令實現原子減一操作,輸入、輸出操作數均直接來自內存(也正因如此,才能保證操作的原子性)。
3)關聯約束(matching constraint
)
在有些情況下,如果命令的輸入、輸出均為同一個變量,則可以在內聯匯編中指定以matching constraint
方式分配寄存器,此時,input operand
和output operand
共用同一個“中轉”寄存器。例如:
asm ("incl %0" :"=a"(var):"0"(var));
該指令對變量var
執行incl
操作,由于輸入、輸出均為同一變量,因此可用"0"
來指定都用%eax
作為中轉寄存器。注意"0"
約束修飾的是input operands
。
4)其它約束
除上面介紹的3中常用約束外,還有一些其它的約束參數(如"o"
、"V"
、"i"
、"g"
等),感興趣的同學可以參考這里。
3. 實例剖析
前面介紹了很多理論性的規則,這里通過分析一個實例來加深對inline assembly
的理解。
下面的代碼是Linux
內核i386
版本中的syscall0
定義:
#define _syscall0(type, name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
__syscall_return(type, __res); \
}
對于系統調用fork
來說,上述宏展開為:
pid_t fork(void)
{
long __res;
__asm__ volatile ( "int $0x80"
: "=a" (__res)
: "0" (__NR_fork));
__syscall_return(pid_t, __res);
}
根據前面對inline assembly
語法及使用方法的說明,我們不難理解這段代碼的含義。將這段內聯匯編翻譯更可讀的偽碼形式為:
pid_t fork(void)
{
long __res;
%eax = __NR_fork /* __NR_fork為內核分配給系統調用fork的調用號 */
int $0x80 /* 觸發中斷,內核根據%eax的值可知,引起中斷的是fork system call */
__res = %eax /* 中斷返回值保持在%eax中 */
__syscall_return(pid_t, __res);
}
【參考資料】
- GCC-Inline-Assembly-HOWTO
- Inline assembly for x86 in Linux
- 《程序員的自我修養—鏈接、裝載與庫》,第12章
- Using Assembly Language in Linux