1. asm格式
GNU的C编译器使用asm关键字:asm段格式如下:asm ("assembly code");
一些汇编器使用制表符字符缩进指令以便区分和标签。GNU编译器不需要这样做,单为保持一致使用这样方式。asm("mov $1, %eax\n\tmov $0, %ebx\n\tint $0x80");
这样格式有些混乱,下面这样方式书写:
asm("mov $1, %eax\n\t"
"mov $0, %ebx\n\t"
"int $0x80");
将asm放入到代码中例如:
#include <stdio.h>
int main()
{
int a = 10;
int b = 20, result;
result = a * b;
asm ("nop");
printf("The result is %d\n", result);
return 0;
}
C源代码中生成的汇编代码,其中#APP
和 #NO_APP
符号表示是asm生成的段落 内容。
1.1 使用全局变量
基本内联汇编代码,可以使用程序定义的全局C变量。
#include <stdio.h>
int a = 10;
int b = 20;
int result
int main()
{
asm ("pusha\n\t"
"movl a, %eax\n\t"
"movl b, %ebx\n\t"
"imull %ebx, %eax\n\t"
"movl %eax, result\n\t"
"popa");
printf("the answer is %d\n", result);
return 0;
}
注意
:数据变量必须被声明为全局的,不能在asm段中使用局部变量。
1.2 使用volatile修饰符
使用volatile修饰符的添加不希望优化这个代码段。asm volatile("assembly code");
1.3 使用替换的关键字
ANSI C规范把关键字asm用于其他用途,不能用于内联汇编。ANSI C想使用内联汇编,则使用__asm__
关键字替代asm__asm__("assembly code");
2.扩展asm
基本asm有其局限性:
- 1.输入和输出都必须是全局变量
- 2.内联汇编不能改变任何寄存器的值。
GNU汇编器提供asm段的扩展格式来帮助解决这些问题。asm扩展版的格式如下:
asm("assembly code"
:ouput locations
:input operands
:changed registers);
有4个部分组成,使用冒号组成:
- 汇编代码:使用基本asm格式相同的语法内联汇编代码
- 输出位置:包含内联汇编输出值的寄存器和内存位置的列表
- 输入操作数:包含内联汇编代码的输入值的寄存器和内存位置的列表
- 改动的寄存器:内联代码改变的任何其他寄存器的列表
其中,输入和输出可以为空,只有最后一个冒号可以省略。
2.1 指定输入和输出值
扩展格式,可以从寄存器和内存位置给输入
值和输出
值赋值。输入值和输出值列表的格式是:"constraint" (variable)
其中:
- variable是程序中C变量。注意:扩展asm格式中,局部和全局变量都可以使用。
- constraint定义把变量存放到哪里(输入来说)或从哪里传送变量(输出来说)。
- 使用它定义变量存放在寄存器中还是在内存位置中。
约束constraint是单一字符的代码,约束代码中:
约束 | 描述 |
a | 使用%eax,%ax或者%al寄存器 |
b | 使用%ebx,%bx或者%bl寄存器 |
c | 使用%ecx,%cx或者%cl寄存器 |
d | 使用%edx,%dx或者%dl寄存器 |
S | 使用%esi或者%si寄存器 |
D | 使用%edi或者%di寄存器 |
r | 使用任何通用的寄存器 |
q | 使用%eax,%ebx,%ecx,或者%edx寄存器之一 |
A | 对于64位值使用%eax和%ebx寄存器 |
f | 使用浮点寄存器 |
t | 使用第一个(顶部的)浮点寄存器 |
u | 使用第二个浮点寄存器 |
m | 使用变量的内存位置 |
o | 使用偏移内存位置 |
V | 只是用直接内存位置 |
i | 使用立即整数值 |
n | 使用值已知的立即整数值 |
g | 使用任何可用的寄存器或者内存位置 |
除了约束之外,输出值包含约束修饰符,指示编译器如何处理输出值,可以使用如下来修饰输出值:
输出修饰符 | 描述 |
+ | 可以读取和写入操作数 |
= | 只能写入操作数 |
% | 如果必要,操作数可以和下一个操作数切换 |
& | 在内联函数完成之前,可以删除或者重新使用操作数 |
例如:asm("assembly code" : "=a"(result) :"d"(data1), "c"(data2));
- 把C变量data1放入到EDX寄存器中
- 把data2存放到ECX寄存器中
- 内联汇编的结果放入到EAX中,然后传递给变量result
注意
:输入是C变量给寄存器,而输出是寄存器给C变量。
2.2 使用寄存器
如果输入和输出都给寄存器,几乎和平常一样使用寄存器,需注意的是扩展asm中,在汇编代码中引用寄存器,必须使用两个百分号符号。
#include <stdio.h>
int main()
{
int data1 = 10;
int data2 = 20;
int result;
// edx为data1,ecx为data2
// eax赋值给result
asm ("imull %%edx, %%ecx\n\t"
"movl %%ecx, %%eax"
: "=a"(result)
: "d"(data1), "c"(data2));
printf("The result is %d\n", result);
return 0;
}
MOVS指令输入值包含输出位置,volatile很重要,编译器因为没有输出值,认为这个asm没有必要而删除asm段:
#include <stdio.h>
int main()
{
char input[30] = {"This is a test message.\n"};
char output[30];
int length = 25;
asm volatile (
"cld\n\t"
"rep movsb"
:
:"S"(input), "D"(output), "c"(length)
);
printf("%s", output)
return 0;
}
2.3 使用占位符
很多输入和输出的情况,可以使用占位符(placeholder)。占位符是前面加上百分号的数字。按照内联汇编代码中列出的每个输入值和输出值在列表中的位置。每个值被赋予一个从零开始的数字。然后可以在汇编代码中使用占位符表示值。
asm("assembly code"
:"=r"(result)
:"r"(data1), "r"(data2));
其中:
- %0将表示包含变量值result的寄存器
- %1将表示包含变量值data1的寄存器
- %2将表示包含变量值data2的寄存器
注意
:占位符提供在内联汇编代码中利用寄存器和内存位置的方法。汇编代码中使用占位符只作为原始的数据:
imull %1, %2
movl %2, %0
示例如下:
#include <stdio.h>
int main()
{
int data1 = 10;
int data2 = 20;
int result;
asm ("imull %1, %2\n\t"
"movl %2, %0"
:"=r"(result)
:"r"(data1),"r"(data2));
printf("The result is %d\n", result)
return 0;
}
gdb调试:info reg
整个asm被看作为一条语句,使用stepi来调试asm汇编
2.4 引用占位符
如果内联汇编代码中的输入值和输出值共享程序中相同的C变量,可以指定使用占位符作为约束值。
修改上面个汇编代码如下:
asm("imull %1, %0"
:"=r"(data2)
:"r"(data1),"0"(data2));
2.5 替换占位符
如果很多占位符,就会混乱。从GNU3.1开始允许声明替换名称作为占位符。格式如下:%[name]"constraint"(variable)
定义的值name成为内联汇编代码中的变量新的占位符标识符,下面例子:
asm("imull %[value1], %[value2]"
:[value2]"=r"(data2)
:[value1]"r"(data1),"0"(data2));
使用占位替换占位符名称的方式和使用普通的占位符相同。
2.6 改动的寄存器列表
改动寄存器列表:如果改动了不在输入和输出列表中的寄存器,要进行申明这些寄存器。
#include <stdio.h>
int main()
{
int data1 = 10;
int result;
asm ("imull %1, %2\n\t"
"movl %2, %0"
:"=r"(result)
:"r"(data1),"0"(result)
:"%eax");
printf("The result is %d\n", result)
return 0;
}
让编译器正确避免使用eax寄存器,因为在内联汇编代码中声明了要使用它。
在改动寄存器列表中使用memory,通知编译器这个内存位置在内联汇编代码中被改动。
2.7 使用内存位置
虽然内联汇编代码使用寄存器比较快,但也可以使用C变量的内存位置。约束m
用于引用输入和输出值中的内存位置。要求使用寄存器的汇编指令,仍然必须使用寄存器,所以不许不得不定义保存数据的中间寄存器。
#include <stdio.h>
int main()
{
int dividend = 20;
int divisor = 5;
int result;
asm("divb %2\n\t"
"movl %%eax, %0"
:"m"(result)
:"a"(dividend), "m"(divisor));
printf("The result is %d\n", result);
return 0;
}
2.8 处理跳转
内联汇编代码中是使用标签有两个限制。
- 只能跳转到相同的asm段内的标签。
- 不同asm不适用相同的标签,不适用C关键字。(局部标签解决)
#include <stdio.h>
int main()
{
int a= 10;
int b= 20;
int result;
asm ("cmp %1, %2\n\t"
"jge greater\n\t"
"movl %1, %0\n\t"
"jmp end\n"
"greater:\n\t"
"movl %2, %0\n"
"end:"
:"=r"(result)
:"r"(a),"r"(b));
printf("The larger value is %d\n", result)
return 0;
}
使用局部标签方法:条件分支和无条件分支都允许指定一个数字加上方向标志作为标签,方向标志指出处理器应该向那个方向查找数字型标签。第一个遇到的标签会被采用。
#include <stdio.h>
int main()
{
int a= 10;
int b= 20;
int result;
asm ("cmp %1, %2\n\t"
"jge 0f\n\t"
"movl %1, %0\n\t"
"jmp 1f\n"
"0:\n\t"
"movl %2, %0\n"
"1:"
:"=r"(result)
:"r"(a),"r"(b));
printf("The larger value is %d\n", result)
return 0;
}
backward和forward
3.使用内联汇编代码
和对待C宏函数一样,可以声明包含内联汇编的宏函数。内联汇编代码必须使用扩展asm格式,便于输入正确的输入值和输出值。
定义一个内联汇编宏函数的一个例子:
#define GREATER(a, b, result) ({\
asm("cmp %1, %2\n\t" \
"jge 0f\n\t" \
"movl %1, %0\n\t" \
"jmp 1f\n" \
"0:\n\t" \
"movl %2, %0\n" \
"1:" \
:"=r"(result) \
:"r"(a), "r"(b));})