赞
踩
Intel Code | AT&T Code |
---|---|
mov eax,1 | movl $1,%eax |
mov ebx,0ffh | movl $0xff,%ebx |
int 80h | int $0x80 |
mov ebx, eax | movl %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 |
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 支持两种类型的语法形式,见下文。
A basic asm statement is one with no operands.
asm asm-qualifiers ( AssemblerInstructions )
implicitly volatile.
举例如下:
asm volatile ("smc #1234");
// macro
#define DebugBreak() asm("int $3")
多条语句习惯上用换行符区分即可。\n
asm volatile (
"smc #1234 \n"
"smc #0"
);
如果在内联代码中操作了一些寄存器,比如修改了寄存器内容(而之后也没有进行还原操作),此时编译器并不知道寄存器内容被修改了。这点尤其是在编译器对代码进行了一些优化的情况下而导致问题,程序将当作它没有被修改过而继续执行,此时会触发异常。如何将这些信息告诉编译器?我们可以通过下面的扩展内联汇编
进行。
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 能够完成上述功能,但是其作用域仅限于函数内部。
理由如下:
主要是扩展的形式支持告知编译器哪些寄存器被修改,允许优化器进行更多的工作,得到更好的性能。
asm asm-qualifiers (
AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
)
// 下面的形式支持 goto 的语法
asm asm-qualifiers (
AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels
)
如果汇编代码只是做一些运算而没有什么附加影响的时候最好不要使用 volatile
修饰。不用 volatile
能给GCC留下优化代码的空间。
side effects
. If so, you may need to use the volatile qualifier to disable
certain optimizations.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 */ );
如果编译器发现输出操作数在asm语句后没被使用,asm语句可以被优化掉。
另外循环语句中发现结果不变的情况下,编译器也会把asm语句从循环中移到外面。
如果希望阻止上面这些优化,可以带上volatile
限定符。否则,不要带这个限定符可以或者更好的性能。
asm语句没有输出操作数或者asm goto语句默认是volatile的。
constraint 限定字符,用于控制最终的代码生成过程。这里只介绍一些常用的限定符。更多的还是要实际接触到再去查资料。
操作数语法为: [符号名] “限制字符” (C变量名)
如果操作数指定了这个constraints,操作数将被存储在通用寄存器中。变量会被被保存在一个由GCC自己选择的寄存器中,这个寄存器作为中转,并且在内存中的操作数的值也会按这个寄存器值被更新。
r | Register(s) |
---|---|
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %adl |
S | %esi, %si |
D | %edi, %di |
顾名思义,对操作数的操作完全作用于内存中。寄存器 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))
。
编译器可以识别到输出操作数对寄存器的更改,但是指令中仍然可能操作到其他寄存器或者产生副作用,比如一些标记位的变化,为了告知编译器这些变化,需要把它们列在 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.
除了指明寄存器外,还有两个特殊的参数:
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语句,需要在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"
);
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函数的全部。
ARM GCC Inline Assembler Cookbook
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。