GCC 内联汇编

16 篇文章 0 订阅

Intel , AT&T

Intel CodeAT&T Code
mov eax,1movl $1,%eax
mov ebx,0ffhmovl $0xff,%ebx
int 80hint $0x80
mov ebx, eaxmovl %eax, %ebx
mov eax,[ecx]movl (%ecx),%eax
mov eax,[ebx+3]movl 3(%ebx),%eax
mov eax,[ebx+20h]movl 0x20(%ebx),%eax
add eax,[ebx+ecx*2h]addl (%ebx,%ecx,0x2),%eax
lea eax,[ebx+ecx]leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h]subl -0x20(%ebx,%ecx,0x4),%eax

How to Use Inline Assembly Language in C Code1

The asm keyword allows you to embed assembler instructions within C code. The asm keyword is a GNU extension. When writing code that can be compiled with -ansi and the various -std options, use __asm__ instead of asm

While the uses of asm are many and varied, it may help to think of an asm statement as a series of low-level instructions that convert input parameters to output parameters.

要使用 C 语言及汇编语言混合编程的话,需要用到 asm 或者 __asm__关键字。GCC 支持两种类型的语法形式,见下文。

Basic Asm

A basic asm statement is one with no operands.

asm asm-qualifiers ( AssemblerInstructions )
  • volatile
    The optional volatile qualifier has no effect. All basic asm blocks are implicitly volatile.
  • inline
    If you use the inline qualifier, then for inlining purposes the size of the asm statement is taken as the smallest size possible

举例如下:

asm volatile ("smc #1234");
// macro
#define DebugBreak() asm("int $3")

多条语句习惯上用换行符区分即可。\n

asm volatile (
	"smc #1234 \n"
	"smc #0"
);

如果在内联代码中操作了一些寄存器,比如修改了寄存器内容(而之后也没有进行还原操作),此时编译器并不知道寄存器内容被修改了。这点尤其是在编译器对代码进行了一些优化的情况下而导致问题,程序将当作它没有被修改过而继续执行,此时会触发异常。如何将这些信息告诉编译器?我们可以通过下面的扩展内联汇编进行。

Extended Asm

An extended asm statement includes one or more operands.The extended form is preferred for mixing C and assembly language within a function, but to include assembly language at top level you must use basic asm.

基本的 asm 语法在有的场景不支持一些功能,比如在汇编代码里调用一个 C 函数。 Extended asm 能够完成上述功能,但是其作用域仅限于函数内部。

推荐使用扩展形式内联汇编2

理由如下:
主要是扩展的形式支持告知编译器哪些寄存器被修改,允许优化器进行更多的工作,得到更好的性能。

  • Extended asm allows the programmer to specify inputs and outputs for the asm as well as which registers it modifies (clobbers) while basic asm does not.
  • Different C compilers use different semantics regarding which registers the asm code can overwrite. gcc assumes no registers get modified. If your asm modifies registers without informing the compiler (which requires using extended asm), undefined behavior will result.
  • Clobbering registers (which basic asm does not support) can give better performance than push/pop.
  • Some compilers automatically flush registers to memory before invoking any asm (what gcc calls a “memory” clobber). However gcc’s basic asm does not do this. If your asm requires this, you need to use the “memory” clobber in extended asm.
  • gcc is considering changing the long-time semantics of basic asm. Instead of not clobbering anything, it may soon begin clobbering everything. This may fix subtle errors in existing code. However there is a (small) chance this will cause problems with existing, correctly function code. Further, this could also introduce performance issues as the contents of some/all registers (even ones your asm doesn’t use) may need to be saved/reloaded around your asm statement. To “future-proof” your code against these types of changes, use extended asm and specify exactly what needs to be clobbered.
  • Because of the fact that gcc’s basic asm has no inputs, outputs or clobbers, it can be difficult for optimizers to consistently position basic asm in the generated code.
asm asm-qualifiers (
	AssemblerTemplate 
    : OutputOperands 
    : InputOperands
    : Clobbers
)

// 下面的形式支持 goto 的语法
asm asm-qualifiers (
	AssemblerTemplate 
	: OutputOperands
	: InputOperands
	: Clobbers
	: GotoLabels
)

如果汇编代码只是做一些运算而没有什么附加影响的时候最好不要使用 volatile 修饰。不用 volatile 能给GCC留下优化代码的空间。

  • volatile
    The typical use of extended asm statements is to manipulate input values to produce output values. However, your asm statements may also produce side effects. If so, you may need to use the volatile qualifier to disable certain optimizations.
  • inline
    If you use the inline qualifier, then for inlining purposes the size of the asm statement is taken as the smallest size possible.
  • goto
    This qualifier informs the compiler that the asm statement may perform a jump to one of the labels listed in the GotoLabels.

参数

  • AssemblerTemplate
    这个填写相应的汇编指令,其中可能包含了对输入、输出的操作,goto 参数等。
  • OutputOperands
    输出操作数,该操作数被上面的汇编指令所修改。用逗号分隔,可以为空。
  • InputOperands
    输入操作数,该操作数被上面的汇编指令所读取,用逗号分隔,可以为空。
  • Clobbers
    寄存器或者其他变量,这些寄存器或变量值被上面的汇编指令所修改,但是不作为输出,用逗号分隔,可以为空。
  • GotoLabels
    当使用 goto 形式的语法时,这里放着 C 标号,作为跳转目标。

The total number of input + output + goto operands is limited to 30.

// exp1: This code copies src to dst and add 1 to dst.
void foo {
	int src = 1;
	int dst;   

	asm (
		"mov %0, %1\n"
    	"add $1, %0"
    	: "=r" (dst)
    	: "r" (src)
    );
	printf("%d\n", dst);
}

// exp2: 输出列表为空,但是后面的列表非空,: 也是不能省略的
asm("msr cpsr, %[ps]" : : [ps] "r" (status));

// 操作数语法为: [符号名] "限制字符" (C变量名)
// 符号名用于汇编指令中引用 C 变量,语法是 %[符号名]
// 符号操作符的名字使用了独立的命名空间。这就意味着它使用的是其他的符号表。不必关心使用的符号名在C代码中已经使用了。
asm (
	"msr cpsr, %[ps]" /* read status to cpsr */ 
	: /* No output, empty list */
	: [ps] "r" (status) /* input list */
);

// exp3: 不使用符号名,在汇编代码中操作数的引用使用的是%后面跟一个数字
// 顺序是从输出寄存器序列从左到右从上到下以 %0 开始,分别记为 %0、%1 ···
// 这种方式不方便维护代码,不推荐使用
asm (
	"msr cpsr, %0" /* read status to cpsr */ 
	: /* No output, empty list */
	: "r" (status) /* input list */
);

volatile

如果编译器发现输出操作数在asm语句后没被使用,asm语句可以被优化掉。
另外循环语句中发现结果不变的情况下,编译器也会把asm语句从循环中移到外面。
如果希望阻止上面这些优化,可以带上volatile限定符。否则,不要带这个限定符可以或者更好的性能。

asm语句没有输出操作数或者asm goto语句默认是volatile的。

constraint

constraint 限定字符,用于控制最终的代码生成过程。这里只介绍一些常用的限定符。更多的还是要实际接触到再去查资料。
操作数语法为: [符号名] “限制字符” (C变量名)

寄存器操作数 constraint: r

如果操作数指定了这个constraints,操作数将被存储在通用寄存器中。变量会被被保存在一个由GCC自己选择的寄存器中,这个寄存器作为中转,并且在内存中的操作数的值也会按这个寄存器值被更新。

rRegister(s)
a%eax, %ax, %al
b%ebx, %bx, %bl
c%ecx, %cx, %cl
d%edx, %dx, %adl
S%esi, %si
D%edi, %di

内存操作数 constraint: m

顾名思义,对操作数的操作完全作用于内存中。寄存器 constraint 通常只用于必要的汇编指令,或者用于能明显加快操作速度的情况。

匹配 constraint

在某些情况下,一个变量可能被用来传递输入也用来保存输出。这种情况下我们需要用到匹配constraint。

Using digit n tells the compiler to use the same register as for the n-th operand, starting with zero.

asm ("incl %0" :"=a" (var) : "0" (var)); 从var读取变量值自增后写回。var作为两个操作数的值并不能保证两个操作数对应同样的寄存器或者内存,必须显式指定。"0" 就是指定使用和第一个输出相同的寄存器。如果输出操作数使用了符号名的语法,那么输入操作数也使用同名的符号名来指定,asm ("incl %0" : [result] "=a" (var) : "[result]" (var));
较新的编译器上支持新的语法+,即asm ("incl %0" : "+r" (val))

Constraint Modifiers

  • “=” Write-only operand, usually used for all output operands 指明这个操作数是只写的;之前保存在其中的值将被废弃而被输出值所代替。
  • “+” Read-write operand, must be listed as an output operand
  • “&” Means that this operand is an earlyclobber operand, which is modified before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is used as an input operand or as part of any memory address. An input operand can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written.
  • “&” A register that should be used for output only

Clobbers and Scratch Registers

编译器可以识别到输出操作数对寄存器的更改,但是指令中仍然可能操作到其他寄存器或者产生副作用,比如一些标记位的变化,为了告知编译器这些变化,需要把它们列在 clobber list 中。这个list中的寄存器和输入输出选定的寄存器是互斥的,如果输入输出显式指定了某个寄存器,那么它不能列在这个list中。

When the compiler selects which registers to use to represent input and output operands, it does not use any of the clobbered registers. As a result, clobbered registers are available for any use in the assembler code.

除了指明寄存器外,还有两个特殊的参数:

  • cc 表明指令更改了标记位
  • memory 表明指令中不仅访问了输入输出操作数,还做了额外的访存操作,例如输入操作数是一个指针,asm指令对指针指向的内存做了访问(读/写)。为了功能的正确性,生成的代码会把相关寄存器值先刷到内存(因为asm指令可能访存),再执行asm指令。另外,编译器也不假设asm指令前从内存中读取的值在asm指令后还保持不变(因为asm指令可能访存),编译器会生成代码,按需从内存重新读取。也就是说,这个参数实际上是一个编译器的读写内存屏障(read/write memory barrier),需要注意的是它并不阻止处理器的投机访问,如果这个也需要控制的话,需要另外的处理器相关的屏障指令。

If the assembler code does modify anything, use the “memory” clobber to force the optimizers to flush all register values to memory and reload them if necessary after the asm statement.

goto labels

扩展形式的内联汇编也支持goto语句,需要在goto list里列出所有可能跳转的C标签。如果要在asm指令中要引用某个goto list里的标签,需要使用前缀%l,加上这个标签在goto list里的下标(从0开始),再加上所有的输入输出操作数的个数,输出操作数如果带了+限定符,则被当做两个操作数。如%l3。为了避免这种复杂的计算,最好的方式是使用%l[符号名]的方式引用。

这个示例通过符号名的方式引用了lab,并且使用+表明factor即是输入也是输出,如果这里只设置为=,功能将不正确。

The following artificial example shows an asm goto that sets up an output only on one path inside the asm goto. Usage of constraint modifier = instead of + would be wrong as factor is used on all paths from the asm goto.

int foo(int inp)
{
  int factor = 0;
  asm goto ("cmp %1, 10; jb %l[lab]; mov 2, %0"
            : "+r" (factor)
            : "r" (inp)
            :
            : lab);
lab:
  return inp * factor; /* return 2 * inp or 0 if inp < 10 */
}
// AT & T
// call _foo, and use eax ecx as parameters
asm( 
	"movl %0,%%eax;
	movl %1,%%ecx;
    call _foo"
    : /*no outputs*/
    : "g" (from), "g" (to)
    : "eax", "ecx"
   );

Linux example

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

// 上面代码的源地址存在esi寄存器中,目的地址存在EDI中。接着开始复制操作,直到遇到0结束。
// 约束符constraint 为”&S”,”&D”,”&a”,指定了使用的寄存器为esi,edi和eax。
// 很明显这些寄存器是clobber寄存器,因为它们的内容会在函数执行后被改变。
// 此外我们也能看出为什么memory被放在clobber list中,因为d0, d1, d2被更新了。
 
// 我们再来看一个类似的函数。该函数用来移动一块双字(double word)。注意这个函数是用宏来定义的。
#define mov_blk(src, dest, numwords) \
    __asm__ __volatile__ (                                          \
                           "cld\n\t"                                \
                           "rep\n\t"                                \
                           "movsl"                                  \
                           :                                        \
                           : "S" (src), "D" (dest), "c" (numwords)  \
                           : "%ecx", "%esi", "%edi"                 \
                           )
// 该函数没有输出,但是块移动过程导致ecx, esi, edi内容被改变,所以我们必须把它们放在clobber list中。
 
// 在Linux中,系统调用是用GCC内联汇编的形式实现的。
// 让我们来看看一个系统调用是如何实现的。所有的系统调用都是用宏来写的 (在linux/unistd.h)。
// 例如,一个带三个参数的系统调用的定义如下:
#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); \
    }
// 所有带三个参数的系统调用都会用上面这个宏来执行。
// 这段代码中,系统调用号放在eax中,参数分别放在ebx,ecx,edx中,最后用”int 0x80”执行系统调用。返回值放在eax中。
     
// Linux中所有的系统调用都是用上面类似的方式实现的。比如Exit系统调用,它是带单个参数的系统调用。实现的代码如下:
   {
          asm("movl $1,%%eax;         /* SYS_exit is 1 */
                   xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
                   int  $0x80"            /* Enter kernel mode */
              );
    }
// Exit的系统调用号是1,参数为0,所以我们把1放到eax中并且把0放到ebx中,最后通过调用int $0x80,exit(0)就被执行了。
// 这就是exit函数的全部。

More Reference

ARM GCC Inline Assembler Cookbook


  1. https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C ↩︎

  2. https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值