C表达式中的汇编指令

摘要:
C表达式中的汇编指令asm为gcc中的关键字,asm表达式为在C代码中嵌套汇编指令,该表达式只是单纯的替换出汇编代码,并不对汇编代码的含义进行解析。结果保存在res也就是cvariablename中InputOperands输入操作数使C变量和表达式中的值可用于汇编代码。例如,计算可能需要其他寄存器,或者处理器可能会由于特定汇编程序指令而破坏寄存器的值。为了将这些更改通知编译器,在Clobber列表中列出这些会产生副作用的条目。
C 表达式中的汇编指令

asm 为 gcc 中的关键字,asm 表达式为在 C代码中嵌套汇编指令,该表达式只是单纯的替换出汇编代码,并不对汇编代码的含义进行解析。

asm 表达式有两种形式,第二种 asm-qualifiers 包含了 goto 语句。
第一种形式为常见的用法,AssemblerTemplate 和 OutputOperands 必须存在, 其中 Clobbers 存在需要 InputOperands 也出现。

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate 
                      : 
                      : InputOperands
                      : Clobbers
                      : GotoLabels)

Qualifiers 的类型

  • volatile, 避免编译器的优化
  • inline, 内敛限定符,最小的体积
  • goto, 包含跳转指令

参数

  • AssemblerTemplate
    - 汇编指令模板是包含汇编器指令的文字字符串,编辑器替换引用输入,编译器不会解析该指令的含义。
  • OutputOperands
    - 由 AssemblerTemplate 中的指令修改的C变量的逗号分隔列表,允许使用空列表。
  • InputOperands
    - 由 AssemblerTemplate 中的指令读取的C变量的逗号分隔列表,允许使用空列表。
  • Clobbers
    - 用逗号分隔的寄存器列表或由 AssemblerTemplate 修改的值,不能出现在 OutputOperands 和 InputOperands 中被提及,允许使用空列表。
  • GotoLabels
    - 当使用asm的goto形式时,此部分包含 AssemblerTemplate 中的代码可能跳转到的所有C标签的列表。

AssemblerTemplate

汇编指令由一个字符串给出,多条汇编指令结合在一起使用的时候,中间以 隔开,如

asm("inc %0
	inc %0" : "=r"(res) : "0"(res));

/APP
# 11 "asm.c" 1
        inc %rax
        inc %rax
# 0 "" 2
/NO_APPs

需要转义的字符:%, =, {, }, |

故在ATT汇编中,对寄存器进行操作的需要双 %%, 如 inc %%rax.

OutputOperands

操作数之间用逗号分隔。 每个操作数具有以下格式:

[ [asmSymbolicName] ] constraint (cvariablename)
  • asmSymbolicName
    - 为操作数指定名称,格式为 %[name]
    c // res = num asm("movq %[num], %[res]" : [res] "=r"(res) : [num] "m"(num));
    - 如果未指定名称使用数字, 从 output 域开始,第一个参数为 %0, 一次类推, 这里的 res 为 %0, num 为 %1
    c // res = num asm("movq %1, %0" : "=r"(res) : "m"(num));
  • constraint
    - 一个字符串常量,用于指定对操作数的存储的 约束, 需要以 "=" 或 "+" 开头
  • cvariablename
    - 指定一个C左值表达式来保存输出,通常是一个变量名。 括号是语法的必需部分

第一个参数为增加可读性使用的,现在我们有代码如下

int64_t res;
int64_t num = 1;

asm("movq %[num], %[res]" : [res] "=r"(res) : [num] "m"(num));
asm("movq %1, %0" : "=r"(res) : "m"(num));
asm("movq %1, %0" : "=m"(res) : "m"(num));
asm("movq %1, %0" : "=r"(res) : "r"(num));

// 对应的汇编代码, 只保留asm表达式中的代码
# 13 "asm.c" 1
        movq -16(%rbp), %rax  // asm-1
 # 0 "" 2
/NO_APP

/APP
 # 15 "asm.c" 1
        movq -16(%rbp), %rax  // asm-2
 # 0 "" 2
/NO_APP

/APP
 # 17 "asm.c" 1
        movq -16(%rbp), -8(%rbp)  // asm-3
 # 0 "" 2
/NO_APP

/APP
 # 19 "asm.c" 1
        movq %rax, %rax  // asm-4
 # 0 "" 2
/NO_APP
  1. 使用名称替换和数字替换效果一样,见 asm-1 和 asm-2
  2. 约束的用法,这里使用比较简单通用的的两种情况,r 为通过寄存器寻址操作,m 通过内存寻址操作,所以看到当约束了 r 就对应寄存器的操作。
  3. 结果保存在 res 也就是 cvariablename 中

InputOperands

输入操作数使C变量和表达式中的值可用于汇编代码。

[ [asmSymbolicName] ] constraint (cexpression)
  • asmSymbolicName 和输出列表的用法完全一致
  • constraint 约束不能使用 =+. 可以使用 "0", 这表明在输出约束列表中(从零开始)的条目,指定的输入必须与输出约束位于同一位置。
int64_t res = 3;
int64_t num = 1;
asm("addq %1, %0" : "=g"(res) : "0"(num));

// 输入输出位置相同
        movq	$3, -8(%rbp)
        movq	$1, -16(%rbp)
        movq	-16(%rbp), %rax
/APP
# 32 "asm.c" 1
        addq %rax, %rax
# 0 "" 2
/NO_APP
  • cexpression 可以不为左值,作为汇编表达式的输入值即可

Clobbers

破坏列表,主要用于指示编译器生成的汇编指令。

从asm表达式中看到输出操作数中列出条目的更改编译器是可以确定的,但内联汇编代码可能不仅对输出进行了修改。 例如,计算可能需要其他寄存器,或者处理器可能会由于特定汇编程序指令而破坏寄存器的值。 为了将这些更改通知编译器,在Clobber列表中列出这些会产生副作用的条目。 破坏列表条目可以是寄存器名称,也可以是特殊的破坏列表项(在下面列出)。 每个内容列表条目都是一个字符串常量,用双引号引起来并用逗号分隔。

  • 寄存器

      ```c
      asm volatile("movc3 %0, %1, %2"
              : /* No outputs. */
              : "r"(from), "r"(to), "g"(count)
              : "%rbx", "%rcx", "%rdx", "memory");
    
      /APP
      # 25 "asm.c" 1
              movc3 %rax, %r8, -72(%rbp)
      # 0 "" 2
      /NO_APP
      ```
    
      可以看到使用到了 rax 寄存器,然后修改程序在 Clobbers 增加 %rax, 结果如下
    
      ```c
      asm volatile("movc3 %0, %1, %2"
              : /* No outputs. */
              : "r"(from), "r"(to), "g"(count)
              : "%rax", "%rbx", "%rcx", "%rdx", "memory");
    
      /APP
      # 25 "asm.c" 1
              movc3 %r8, %r9, -72(%rbp)
      # 0 "" 2
      /NO_APP
      ```
      编译器在产生的汇编代码中就未使用 %rax 寄存器了。
    
  • 特殊破坏列表项
    - "cc", 表示汇编代码修改了标志寄存器
    - "memory", 为了确保内存中包含正确的值,编译器可能需要在执行asm之前将特定的寄存器值刷新到内存中

编译器为了破坏列表项的值受到破坏,当这些条目是寄存器时,不对其进行使用;为特殊参数时,重新刷新得到最新的值。

约束

  • 一些基础的约束
约束名说明
whitespace空白字符被忽略
m允许使用内存操作数,以及机器通常支持的任何类型的地址
o允许使用内存操作数,但前提是地址是可偏移的
V允许使用内存操作数,不可偏移的内存地址,与 "o'互斥
r允许在通用寄存器中使用的寄存器操作数,其中可以指定寄存器,如 a(%rax), b(%rbx)
i允许使用立即整数操作数
n允许使用具有已知数值的立即整数操作数, ‘I’, ‘J’, ‘K’, … ‘P’ 更应该使用 n
F允许使用浮点立即数
g允许使用任何寄存器,内存或立即数整数操作数,但非通用寄存器除外
X允许任何操作数, ‘0’, ‘1’, ‘2’, … ‘9’
p允许使用有效内存地址的操作数
  • 标识符约束
标识符说明
=表示此操作数是由该指令写入的:先前的值将被丢弃并由新数据替换
+表示该操作数由指令读取和写入
&表示(在特定替代方法中)此操作数是早期指令操作数,它是在使用输入操作数完成指令之前写入的,故输入操作数部分不能分配与输出操作数相同的寄存器
%表示该操作数与后续操作数的可交换指令

内核示例

  1. x86 的内存屏障指令。
// 避免编译器的优化,声明此处内存可能发生破坏
#define barrier() asm volatile("" ::: "memory")
// 在32位的CPU下,lock 指令为锁总线,加上一条内存操作指令就达到了内存屏障的作用,64位的cpu已经有新增的 *fence 指令可以使用
// mb() 执行一个内存屏障作用的指令,为指定CPU操作;破坏列表声明 cc memory 指示避免编译器进行优化
#ifdef CONFIG_X86_32
#define mb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "mfence", 
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#define rmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "lfence", 
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#define wmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "sfence", 
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#else
#define mb() 	asm volatile("mfence":::"memory")
#define rmb()	asm volatile("lfence":::"memory")
#define wmb()	asm volatile("sfence" ::: "memory")
#endif
  1. x86 下获取 current 的值
DECLARE_PER_CPU(struct task_struct *, current_task);

#define this_cpu_read_stable(var)	percpu_stable_op("mov", var)

static __always_inline struct task_struct *get_current(void)
{
        return this_cpu_read_stable(current_task);
}

#define percpu_stable_op(op, var)			
({							
        typeof(var) pfo_ret__;				
        switch (sizeof(var)) {				
        case 8:						
                asm(op "q "__percpu_arg(P1)",%0"	
                : "=r" (pfo_ret__)			
                : "p" (&(var)));			
                break;					
        }						
        pfo_ret__;					
})

current_task 为一个 struct task_struct 类型的指针,追踪宏调用,在x86-64 下命中了 case 8: 的汇编代码, 展开的代码为

asm("mov" "q ""%%""gs" ":" "%" "P1"",%0" : "=r" (pfo_ret__) : "p" (&(current_task)));
// 变换一下为
asm("movq %%gs:%P1, %0" : "=r"(pfo_ret__) : "p"(&(current_task)));

这行代码的含义为将 约束输入部分必须为有效的地址(p约束), 将CPU id(通过段寄存器gs和偏移通过GDT得到,这里后文分析了)通过寄存器(r约束)赋值给 pfo_ret__.

参考

GCC文档
C语言ASM汇编内嵌语法zz

免责声明:文章转载自《C表达式中的汇编指令》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇VS2013自动注释插件使用webpack搭建vue开发环境下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

将Delphi的对象方法设为回调函数

心血来潮,为了实现更好的通用性和封装性,需要把类方法作为回调函数,搜得一篇好文,节选转发。命名似乎应该是MethodToCallback才合适,可惜调试时总是报错,debugging。 原文地址:http://blog.csdn.net/linzhengqun/article/details/1451088 Win32的API有一些需要回调函数,说白...

X ORACLE19c的RAC集群部署-详细完整篇

本文为19年12月份搭建的环境并亲自安装的整体过程日志,如有疑问,可留言共同探讨。 ORACLE19c--RAC集群安装 第一步:配置/etc/hosts 192.168.1.61xiaosheng61 192.168.1.62xiaosheng62 192.168.10.61xiaosheng61prv 192.168.10.62xiaosheng...

Java并发机制和底层实现原理

  Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码转化为汇编指令在CPU上执行。Java中的并发机制依赖于JVM的实现和CPU的指令。      Java语言规范第三版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量...

汇编语言学习笔记--王爽

##第一章 基础知识1 机器语言是机器指令的集合。机器指令是机器可以正确执行的命令,是一列二进制数字。2 汇编语言的主题是汇编指令。编译器将汇编指令转换成机器指令。 - 汇编指令:机器码的助记符,有对应机器码; - 伪指令:无对应机器码,由编译器执行; - 其他符号:如+等,由编译器识别,无对应机器码。 3 指令和数据是应用上的概念,在内存或磁盘上,都是二...

测试CPU速度

function CPUSpeed:Double; const DelayTime=500; var TimerHi,TimerLo:Dword; PriorityCals,Priority:Integer; begin PriorityClass :=GetPriorityClass(GetCurrentProcess); Priority :=GetT...

内中断(学习汇编)

中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。 注意,这里所说的中断信息,是为了便于理解而采用的一种逻辑上的说法。它是对几个具有先后顺序的硬件操作所产生的事件的统一描述。 “中断信息”是要求CPU马上进行某种处理,并向所要进行的该种处理提供了必备的参数的通知信息。 中断信息可以来自CPU的内部和外部。 内中断的产生 对...