1.C语言内嵌汇编是汇编语言吗
内嵌汇编使用的是真正的汇编指令,但它的编写方式和使用体验与独立的汇编源程序有很大不同。编译器在其中扮演了一个“翻译官”和“协调者”的角色。程序员可以把其中一部分工作交给编译器来实现,而不是像汇编一样什么都要自己做。
常见的使用场景: 性能极致优化:对极其关键的热点代码进行手动优化。 访问特殊硬件功能:执行标准C语法没有直接对应的处理器指令,如管理中断、访问特定系统寄存器等
2.基本语法
__asm__ __volatile__ (
"汇编指令模板"
: "输出操作数列表" /* 可选 */
: "输入操作数列表" /* 可选 */
: "破坏列表" /* 可选 */
);
2.1 汇编指令模板与操作数占位符
在指令模板中,使用 %0、%1、%2等来引用后面的操作数。它们按顺序对应:输出操作数从 %0开始编号,然后是输入操作数依次编号。在AT&T语法中,寄存器名前需要加两个 %,如 %%eax,以区别于操作数占位符。汇编指令模板包含真正的汇编指令,用双引号括起。多条指令间常用 \n\t分隔以确保格式正确。
2.2 操作数约束:包括输入和输出
约束字符告诉编译器如何分配寄存器或内存: "r":允许编译器将变量分配在任何通用寄存器。 "a", "b", "c", "d"等:指定必须使用某个特定寄存器(如 eax, ebx, ecx, edx)。 "m":直接操作变量的内存地址。 "=":表示操作数是只写的(输出操作数)。 "+":表示操作数既是输入又是输出。 0-9(数字):指示该输入操作数必须与第n个输出操作数使用相同的存储位置(寄存器或内存)。如果某个输入参数选择与前面某个输出参数使用相同的存储位置,那么输入参数占用一个序号,不能跳过。例如下面的第二个输入参数input2序号是%2:
int result, input1 = 10, input2 = 20;
__asm__ volatile (
"addl %2, %0" // 汇编模板:将 %2 (input2) 加到 %0 (result/input1) 上
: "=r"(result) // 输出:%0 对应 result
: "0"(input1), // 输入1:匹配约束,与 %0 共享位置
"r"(input2) // 输入2:%2 对应 input2
);
2.3 破坏列表(Clobber List)
如果汇编指令修改了不在输入/输出操作数列表中指定的寄存器或内存,必须在破坏列表中声明。破坏列表就是你要告诉编译器,你的汇编代码“悄悄”改动了哪些资源,而这些资源并没有在你的输入/输出操作数列表中声明。这是为了防止编译器在不知情的情况下,依赖这些被“破坏”的资源原有值,导致程序出错。 寄存器:用寄存器名,如 "%eax", "%ebx"。 内存:如果指令修改了内存,需添加 "memory",这会告诉编译器不要假定内存中的值在汇编执行后保持不变。<br> 常见的几个例子 (1)下面的代码显式使用了 %eax寄存器
int value = 10;
__asm__ __volatile__ (
"movl $1, %%eax\n\t" // 显式将1放入eax寄存器
"addl %%eax, %0" // 将eax的值加到输出变量上
: "+r"(value) // value是读写操作数
: // 无输入
: "%eax" // 必须声明!因为eax没在输入输出中指定
);
这里,%eax是我们主动使用的,不属于任何操作数约束,所以必须在破坏列表里声明 "%eax"。 (2)指令影响了标志位 很多算术和逻辑运算指令(如 addl, subl, cmpl)会改变 EFLAGS(或ARM中的CPSR)标志寄存器 中的条件码(Condition Code,简称 cc),比如溢出位、零标志位等。如果后续的C代码包含条件判断,编译器可能假设标志位未被改变。因此,任何影响标志位的汇编指令后都必须加上 "cc"。
int x = 5;
__asm__ __volatile__ (
"addl $3, %0" // 加法操作会影响标志位
: "+r"(x)
:
: "cc" // 声明标志位被修改
);
(3)汇编代码修改了内存 当你的汇编指令直接向某个内存地址写入数据,并且这个内存地址不是通过特定的输入/输出操作数绑定的(例如,通过指针修改了指向的内容),就需要声明 "memory"。
int data = 100;
int *ptr = &data;
__asm__ __volatile__ (
"movl $5, (%0)" // 将5写入ptr指向的内存地址
:
: "r"(ptr) // 输入是ptr的值(一个地址)
: "memory" // 声明内存被修改
);
// 执行后,data的值变为5