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个操作数变量ab,这段汇编代码的作用是将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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容