高中第一次看到在 C 语言中嵌入汇编语言时,我就觉得它非常好玩,到大学刚开学的时候,我甚至有使用它的需求,可惜直到现在我才抽出时间开始学习它。

分类

在 GNU 的 GCC 中,内联汇编被分为两类:Basic asmExtended asm

GCC 的官方文档中写到,在函数中混合使用 C 语言和汇编语言时,最好使用扩展形式;但是要在顶层(top level)包含汇编语言,则必须使用基本形式。

这里的 top level 一词非常诡异,让我摸不着头脑,之后我在官方文档的后文发现了蛛丝马迹:

两种必须使用 Basic asm 的情形:

  • Extended asm 声明必须在 C 函数内,所以如果在文件作用域(top-level),在函数之外使用汇编,必须使用 Basic asm 。你可以使用此技术发出汇编程序指令,定义可在文件的其他位置调用的汇编语言宏,或用汇编语言编写整个函数。
  • 函数的属性(attribute)被声明为 naked 时必须使用 Basic asm 。

naked attribute 在此不做展开。

谜底揭开了,原来 top level 指的就是文件这一层。

Basic asm

可以使用关键字 asm 来声明一段内联汇编,不过这是 GNU 的拓展,如果在编译条件中使用了 -std 或者 -ansi 时,必须使用 __asm__

修饰

可以使用 volatileinline 来修饰,不过 volatile 没有作用,所有的内联汇编代码段都是默认 volatile 的。如果使用 inline ,那么为了内联的目的,内联汇编语句的大小被当作是可能的最小大小。

这里涉及到了内联汇编的大小估计,这是因为内联汇编生成的最终代码只由汇编器(Assembler)知晓,所以编译器需要自己估算,通过查找 '\n\t', ';' 等符号来估计指令数(行数),再乘以可能的最长指令长度来获得估算大小。所以如果在 inline 的 Basic asm 中使用了伪指令等能生成汇编语句的成分,汇编器可能会报错 unreachable 。

是否需要估计内联汇编的大小,这个与平台有关。

参数

又被称为 AssemblerTemplate ,由字符串字面值组成,该字符串由汇编语句组成,可以包括伪指令,GCC 的编译器并不检查它们的有效性,指令会被直接传递到汇编器中。

在一段内联汇编中使用多条汇编语句时要注意自己使用的是什么汇编器(以及它们使用的是何种汇编方言),一般都可以用 '\n\t' 来分隔,有些汇编器可以使用 ';' ,但要注意在这种汇编方言中分号是不是注释开始的符号。

注意

  • 由于调用函数以及访问 C 数据可能很复杂,在这种情况下使用 Extended asm 是更好的选择。

  • 在编译器优化后连续的 asm 声明可能不再连续,所以如要需要连续语句,请写在同一个 asm 语句中。尤其是两个 asm 声明中包含了跳转时,如果被编译器调整顺序则会产生意想不到的后果。如果要跳转到 C 标签,还是使用 Extended asm 更好。

  • 如果在汇编语句中定义了符号或者标签,可能会导致重复符号的错误。

Extended asm

拓展内联汇编有两种模板:

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate 
                      : OutputOperands
                      : InputOperands
                      : Clobbers
                      : GotoLabels)

同样地,在不使用 GNU 拓展时要用 __asm__ 代替 asm

修饰

除了 volatileinline 外,还有 goto ,表示这段内联汇编代码可能会跳转到 GotoLabels 中的某一个标签上。

参数

input, output, goto 的操作数加起来不能超过 30 个。

AssemblerTemplate

同样是由字符串字面值组成,但是里面包含了一些标识符,它们指向了 output, input, goto 中的操作数。

OutputOperands

不能将其单纯理解为输出,而是理解为会被修改的变量。

AssemblerTemplate 中的指令修改的 C 变量列表,由逗号分隔,可以为空。

结构一般为:

[ [asmSymbolicName]  ] constraint (cvariablename)
asmSymbolicName

可以理解为在这段拓展内联汇编中的符号别名,它可以和上下文中的 C 语言符号重复,也可以和其他内联汇编代码中的符号名重复。在 AssemblerTemplate 中的使用方法为 %[Value]

如果不使用符号别名,则必须使用位置编号来指示符号,例如 OutputOperands 中有三项,则用 %0, %1%2 表示。

constraint

Output 的约束修饰符必须是以 '=' 开始(表示将会覆写这个变量),如果这个变量既被读也被写就需要 '+' 。当使用 '=' 时不要假定可以读取到将被覆写的变量的当前值,除非这个变量也在 Input 列表中。

使用了 '+' 的操作数视作两个(在计算总个数及位置时)。

还需要对值所在的位置进行约束,例如 'r' 表示寄存器,'m' 表示在内存。如果使用了一个以上的位置约束修饰符,编译器会选择它认为最好的方式进行优化。

也可以使用数字进行约束,例如:

asm ("incl %0" :"=a"(var):"0"(var));

表示输入中的 var 使用的是和输出中的 var 相同的约束,这里的 0 表示为编号为 0 的操作数。

常用的约束:

符号 含义
I a constant
a use eax
b use ebx
c use ecx
d use edx
S use esi
D use edi
r use one of eax, ebx, ecx or edx
q use one of eax, ebx, ecx, edx, esi or edi

其余见 GCC 文档。

cvariablename

一个 C 左值表达式,一般为变量。

InputOperands

不能将其单纯理解为输入,而是理解为会被读取的变量。

AssemblerTemplate 中的指令读取的 C 表达式列表,由逗号分隔,可以为空。

结构也为:

[ [asmSymbolicName]  ] constraint (cvariablename)
asmSymbolicName

如果有两个输入操作数,三个输出操作数,则第一个输入操作数为 %2 ,第二个为 %3 ,第三个为 %4

一个例子:

asm ("cmoveq %1, %2, %[result]" 
        : [result] "=r"(result) 
        : "r" (test), "r" (new), "[result]" (old));

Clobbers

In software engineering and computer science, clobbering a file, processor register or a region of computer memory is the process of overwriting its contents completely, whether intentionally or unintentionally, or to indicate that such an action will likely occur.

以逗号分隔的寄存器列表,一般为 AssemblerTemplate 更改的其他值(没有被列入 OutputOperands 的部分)。例如一些汇编指令(例如 cld )有副作用,会使用到额外的寄存器。

有两个特殊的 clobber :

  • cc - 表示将会修改 flag 寄存器。

  • memory - 这个 clobber 可以告诉编译器,这段内联汇编代码将对输入和输出操作数之外的项执行内存读取或写入操作(例如,输入操作数是一个指针,需要访问它指向的内存)。

使用 memory 时,为了确保内存中包含正确的值,GCC 可能需要在执行这段内联汇编之前,将特定的寄存器值刷新到内存中。

此外,在执行一个提前读取操作之前,编译器不会假定从内存中读取的值在执行该提前读取操作之后保持不变,而是会根据需要重新加载这些值。

memory 这个 clobber 有效地为编译器形成了读/写内存障碍。但这并不能阻止处理器在内联汇编语句之后执行推测性读操作。这种情况下还是需要 fence 指令。

GotoLabels

此部分包含 C 标签的列表,当使用 goto 形式时,AssemblerTemplate 中的代码可以跳转到这些标签。

要引用汇编程序模板中的标签,需要在它前面加上 'l' ,后面加上它在 GotoLabels 中的位置与输入和输出操作数的数目之和。带约束修饰符 '+' 的输出操作数被视为两个操作数。例如,如果有三个输入操作数,一个输出操作数带有约束修饰符 '+',一个输出操作数带有约束修饰符 '=' ,那么 GotoLabels 的第一个标签称为 %l6 ,第二个称为 %l7

也可以使用实际的 C 标签名,例如如果要引用 GotoLabels 中的 carry 标签,可以使用 %l[carry] ,这也是推荐的方法,避免了对编号的计算。

实例

两数之和:

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;
}

或者:

 __asm__ __volatile__(
                      "   lock       ;\n"
                      "   addl %1,%0 ;\n"
                      : "=m"  (my_var)
                      : "ir"  (my_int), "m" (my_var)
                      :                                 /* no clobber-list */
                      );

lock 表示这是个原子操作。

字符串复制:

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;
}
#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ (                                          \
                       "cld\n\t"                                \
                       "rep\n\t"                                \
                       "movsl"                                  \
                       :                                        \
                       : "S" (src), "D" (dest), "c" (numwords)  \
                       : "%ecx", "%esi", "%edi"                 \
                       )

Linux 系统调用:

#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); \
}

Linux 退出:

{
    asm("movl $1,%%eax;         /* SYS_exit is 1 */
         xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
         int  $0x80"            /* Enter kernel mode */
         );
}

参考资料

内联汇编深入起来非常繁杂,有些功能需要使用的时候可以直接查看 GNU 的文档。

GCC Inline Assembly HOWTO

GCC doc in web archive

How to Use Inline Assembly Language in C Code

Basic Asm

Extended Asm

How to Use Inline Assembly Language in C Code

Declaring Attributes of Functions

Local Register Variables