大部分内容翻译提取自某国外HOW-TO文档,原地址:
http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
gcc内联汇编语法采用AT&T格式,主要不同有五个方面
1. 源操作数和目标操作数的位置与intel格式不同
2. 寄存器命名,AT&T在寄存器前加百分号(%)
3. 字面值前加美元符号($),且字面值16进制不用h作为结尾,而是使用前缀0x
4. 操作数大小由b(byte 8-bit),w(word 16-bit),l(long 32-bit)跟在操作符号后而非在寄存器等之前加xxx ptr来指定大小
5. 内存寻址采用 disp(base, index, scale)来对应intel的[base + index*scale + disp] (disp和scale常量前不加美元符号($))
简单的一些转换举例:
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 |
基础版
asm("movl %ecx %eax");
__asm__("movl %ecx %eax");
两种格式都可以,前后加双下划线主要是为了避免“asm”已经被使用。
提高版
asm ( 这里写指令
: 输出操作数 /* 可选 */
: 输入操作数 /* 可选 */
: 可能被破坏的寄存器列表 /* 可选 */
);
__asm__ ( 这里写指令
: 输出操作数 /* 可选 */
: 输入操作数 /* 可选 */
: 可能被破坏的寄存器列表 /* 可选 */
);
注意,虽然输出操作数可选,但是没有输出操作数却有输入操作数的时候需要将分号写出来而不能省略。
输出操作数和输入操作数为一个双引号里面的限制符然后是一个括号里面一个C表达式。限制符在后文会详细说到,主要是让编译器明确C表达式和汇编语句中的寄存器或内存等的关系。
可能被破坏的寄存器列表是由于在汇编语句中一些寄存器的值可能被改变,执行汇编语言后,可能还要继续执行C的语句,那么在gcc进行编译的时候就不能相信这些被改变了的寄存器的值,也就是不能默认它们没变,所以需要将其明确指出来,避免后面造成一些莫名其妙的错误。格式即为在双引号中指明寄存器名字,多个的话,用逗号隔开。
举例:
asm ("cld\n\t"
"rep\n\t"
"stosl"
: /* 没有输出操作数 */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
这里的c、a、D即限制符,count/fill_value/dest为C的变量。
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* 输出 */
:"r"(a) /* 输入 */
:"%eax" /* 被改变了的寄存器 */
);
这里有几个点需要注意:
1. a为输入,b为输出,“r”和“=r”为限制符,b对应汇编语句中的%1,而a对应汇编语句中的%0.
2. “r”和“=r”为限定符,输出值应该有”=“作为一个限定符,表示该值为只可写的。
3. 寄存器前有两个百分号(%),这是为了方便区分寄存器和操作数(原文没有提及是否为必须)
4. 最后的eax为被改变了的寄存器的声明,这样的话gcc就不会用它来存其他的值了
即稍作变化的汇编语言指令,但是如我们刚才看到的”提高版“内联汇编语法和”基础版“内联汇编语法的区别,基础版是直接使用内联汇编指令,而提高版却有如%1, %0以及%%eax等和一般汇编语句不一样的地方,这样的指令则为”汇编语句模板“(assembler template)。
操作数在汇编语言中主要指指令中涉及到的操作数,而在内联汇编中即为C表达式。其语法即为前文所述,在输出或输入操作数的列表中由“限制符”(C表达式)的格式来给出。
asm ("leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);
快速将x乘5然后放在five_times_x中
asm ("leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);
作用同上,但是这里使用同一寄存器,不过没有指明具体哪一个
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);
作用同上,且指明使用ecx寄存器。
这三个示例中我们都没有指明被改变的寄存器,前两个是因为由gcc来指定谁被改变,所以我们不需要指明,而最后一个我们已经通过限定符告诉了gcc ecx将最后给x,所以也没有必要指明了。
一些指令可能会改变一些寄存器,为了避免gcc再使用里面的值而认为其没有改变,所以我们需要将它们明确出来。另外,我们不需要将输入输出中的寄存器放在这个列表中,因为gcc知道在内联汇编中使用到了它们,而其他的就需要我们自己来指明了。
另外,如果我们的指令会改变条件代码寄存器,我们应该在列表中添加“cc”。如果我们的指令用一些不可预料的方式更改了内存,需要在列表中添加“memory”,如果我们所影响到的内存没有在输入和输出中被列出来,我们还需要使用volitle。
volatile的主要目的是避免gcc因为优化而更改这段代码,对其作修改和删除添加等操作,避免出现不可预料的错误。与asm同样,volatile也可以在前后加双下划线来避免已经被使用过了。
限制符 | 代表的意义 |
---|---|
r | Register(s) |
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %dl |
S | %esi, %si |
D | %edi, %di |
2. 内存操作数限制符 m
与寄存器限制符不同的是,有一些操作数在内存中,对它们进行操作会直接在内存中发生变化,所以需要使用到内存操作数限制符。
3.匹配(位)限制符
有的时候一个变量可以同时是输入也是输出。
例如
asm ("incl %0" :"=a"(var):"0"(var));
这里,eax寄存器将同时作为输入和输出,0的意思是,和第0个输出变量有一样的限定符。
在:
. 输入和输出是同一个变量,输出写回到输入变量中的时候
. 分离输入和输出变量是乜有必要的时候
可以使用这样的方法。
4. 其他限制符
限制符 | 作用 |
---|---|
o | 可以为内存操作数,但是只能是可以偏移的地址,例如加上一小段偏移到这个地址可以为有效的地址的时候 |
V | 不可偏移的地址 |
i | 字面整数值,包括了编译器可以知道的符号常量 |
n | 已知数值的字面整数值。一些系统不支持小于word长度的编译器的常量,这样的话,这些操作数的限制符应该为n而不是i |
g | 任意寄存器、内存和字面整数值,除了不是通用寄存器的寄存器的都可以 |
以下为x86系统所拥有的
限制符 | 作用 |
---|---|
r | 寄存器限制符 |
q | 寄存器a,b,c或者d |
I | 0到31的常量(为32位移位准备的) |
J | 0到63的常量(为64位移位准备的) |
K | 0xff |
L | 0xffff |
M | 0,1,2,3(为lea指令的移位准备的) |
N | 0到255的常量(为out指令准备的) |
f | 浮点数寄存器 |
t | 第一个(栈顶)的浮点数寄存器 |
u | 第二个浮点数寄存器 |
A | 指明a或者d寄存器,这在64为整数值需要用d寄存器返回高位而用a寄存器返回低为的时候很有用 |
在使用限定符的时候,为了有更精确的控制,GCC还有一些限制符调整符,主要有:
1. “=” 等号 表示这个操作数对于这个指令是仅可写的,之前的值将被丢弃,被输出数据所代替
2. “&” 表示这个操作数是一个很早就会被修改的操作数,也就是说在用到输入操作数的指令之前就已经被修改过了。这样的话,这个操作数不会存在一个作为输入操作数的寄存器里也不会作为任何内存地址的一部分。一个输入操作数仅在这样的操作数仅仅在输入操作数用到之前被用到之后不在用的时候才可以和输入操作数发生关联。