gcc內聯匯編

文章來自這里: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)關鍵字asmvolatile
asmgcc關鍵字,表示接下來要嵌入匯編代碼。為避免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,可見,ainput operand,而boutput 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_instructionISA中擁有最多操作數的那條指令包含的操作數數目。

需要明確的是,在指明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 operandsinput 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 operandoutput 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);      
}  

【參考資料】

  1. GCC-Inline-Assembly-HOWTO
  2. Inline assembly for x86 in Linux
  3. 《程序員的自我修養—鏈接、裝載與庫》,第12章
  4. Using Assembly Language in Linux
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • 原文作者 Sandeep.S英文原文 [https://www.ibiblio.org/gferg/ldp/GCC...
    JeffreyLi閱讀 40,233評論 8 41
  • 原文: GCC-Inline-Assembly-HOWTO 1. 簡介(Introduction.) 1.1 Co...
    桂糊涂閱讀 4,554評論 1 5
  • 關于gcc內聯代碼約束的測試! 所謂一切都是虛妄,我們來看一看生成的代碼吧! 對應的匯編如下: 我稍微來說一下吧,...
    Yihulee閱讀 1,144評論 0 0
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,829評論 18 139
  • 上周,我?!袄罅趾谜n堂”英語組課堂教學研討活動如期開展。研討活動開展前一周,英語科組長組織全體英語老師進...
    陳小辣同學閱讀 905評論 0 1