在閱讀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)"
);
還有時候,根據程式上下文,嵌入的彙編代碼中可能會出現一些類似于魔數()的操作數,比如下面的代碼:
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;
$0x80"
: "=a" (__res)
: "0" (__nr_fork));
__syscall_return(pid_t,
__res);
根據前面對inline assembly文法及使用方法的說明,我們不難了解這段代碼的含義。将這段内聯彙編翻譯更可讀的僞碼形式為:
%eax = __nr_fork /*
__nr_fork為核心配置設定給系統調用fork的調用号 */
int $0x80
/* 觸發中斷,核心根據%eax的值可知,引起中斷的是fork system call
*/
__res = %eax /*
中斷傳回值保持在%eax中 */
【參考資料】
1.
2.
3. 《程式員的自我修養—連結、裝載與庫》,第12章
4.