高级CGNU C/C++ 内联汇编——进阶——约束详解

Posted 从善若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级CGNU C/C++ 内联汇编——进阶——约束详解相关的知识,希望对你有一定的参考价值。

本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。


博客内容主要围绕:
       5G协议讲解
       算力网络讲解(云计算,边缘计算,端计算)
       高级C语言讲解
       Rust语言讲解

文章目录

GNU C/C++ 内联汇编——进阶——约束详解

ASM 操作码的 constraints


Simple Constraints ——Basic use of constraints

最简单的约束是一个由字母组成的字符串,每个字母描述一种允许的操作数,以下是允许的字母:

Constraints含义
空格空白字符将被忽略,可以插入除第一个位置以外的任何位置。这使得不同操作数的每个选项在机器描述中对齐,即使它们有不同数量的约束和修饰符。
‘m’允许内存操作数,它可以是计算机支持的任何类型的地址。注意,用于通用内存约束的字母可以由后端使用TARGET_MEM_CONSTRAINT宏重新定义。
‘o’允许使用内存运算对象,但仅当该地址是可偏移的(offsettable)。这意味着可以向地址添加一个小整数(实际上是操作数的字节宽度,由其机器模式决定),结果也是一个有效的内存地址。

例如,一个常量地址是可偏移的;一个寄存器和一个常量之和的地址(只要一个稍大的常量也在机器支持的地址偏移量范围内)也是一样的;但自动递增或自动递减地址是不可偏移的。更复杂的间接/索引地址可以偏移,也可以不偏移,这取决于机器支持的其它寻址模式。

请注意,在一个可以被另一个操作数匹配的输出操作数中,约束字母’ o ‘只有在同时伴有’ < ‘(如果目标机器有前任寻址)和’ > '(如果目标机器有前增量寻址)时才有效。
‘V’一种不可偏移的内存运算对象。换句话说,任何符合m约束但不符合o约束的东西。
‘<’允许具有自减法寻址的内存操作数(无论是前置或后置减法)。在内联asm中,只有当操作数在可以处理副作用的指令中恰好使用一次时,才允许此约束。在inline asm模式的约束字符串中不使用带有’ < '的操作数或在多个指令中使用它是无效的,因为副作用不会被执行或将被执行不止一次。
‘>’允许使用具有自动递增寻址(前置递增或后置递增)的内存运算对象。在inline asm中,应用与’ < '相同的限制。
‘r’允许在通用寄存器中使用寄存器运算对象。
‘i’允许使用立即整型操作数(具有常值的操作数)。这包括其值只有在汇编时或以后才知道的符号常量。
‘n’允许具有已知数值的立即整数操作数。许多系统不能为小于一个字宽的操作数支持汇编时常量。这些操作数的约束应该使用’ n ‘而不是’ i '。
‘I’, ‘J’, ‘K’, … ‘P’在’ I ‘到’ P ‘范围内的其他字母可以以一种依赖于机器的方式定义,以允许在指定范围内具有明确整数值的整数操作。例如,在68000上,’ I '被定义为表示值1到8的范围。这是移位指令中允许作为移位计数的范围。
‘E’允许立即浮点操作数 (表达式代码const_double),但前提是目标浮点数格式与运行编译器的主机的格式相同。
‘F’允许立即浮点操作数 (表达式代码const_double或const_vector)。
‘G’, ‘H’’ G ‘和’ H '可以以一种依赖于机器的方式定义,以允许特定值范围内的直接浮点运算对象。
‘s’允许一个值不明确的立即整数操作数。

这可能看起来很奇怪,如果 insn(指令) 允许常量操作数的值在编译时未知,那么它肯定允许任何已知值。那么为什么要用 ‘s’ 而不是 ‘I’ 呢?因为,有时它可以生成更好的代码。

例如,在68000的全字指令中,可以使用一个立即操作数;但是,如果当前值介于-128和127之间,那么将该值加载到寄存器并使用寄存器可以得到更好的代码。这是因为载入寄存器可以通过 ’ moveq ’ 指令完成。我们将字母 ‘K’ 定义为 “-128到127范围外的任何整数” ,然后在操作数约束中指定 ‘Ks’ 。
‘g’任何通用寄存器、内存、立即整数 的操作数都被允许。
‘X’任何操作数都是允许的。
‘0’, ‘1’, ‘2’, … ‘9’允许匹配指定操作数的操作数。如果一个数字和同一个选项中的字母一起使用,这个数字应该放在最后。

这个数字个数允许大于一个数字。如果连续遇到多个数字,则将它们解释为一个十进制整数。不太可能产生歧义,因为到目前为止,将’ 10 '解释为匹配操作数1或操作数0都是不可取的。如果需要这样做,可以使用多个替代方案。

这被称为匹配约束(matching constrain),它的真正含义是,汇编程序只有一个操作数来填充asm区分的两个角色(在输出中分配与相应输入中相同的寄存器)。例如,加法指令使用两个输入操作数和一个输出操作数,但在大多数CISC机器上,加法指令实际上只有两个操作数,其中一个是输入-输出操作数:

addl #35,r12
‘p’允许一个有效的内存地址的操作数。用于“load address” and “push address”指令。

约束中的’ p '必须和 address_operand一起使用,作为match_operand中的谓词。这个谓词将match_operand中指定的模式转换为有效地址下的内存引用(memory reference)模式。
other-letters其他字母可以以机器相关的方式定义,以代表特定的寄存器类或其他任意操作数类型。’ d ', ’ a ‘和’ f '是在68000/68020上定义的,代表数据、地址和浮点寄存器。

Multiple Alternative Constraints

       有时一条指令有多个可选的操作数集。例如,在68000上,一条 logical-or 指令可以将寄存器或立即数存入到内存中,也可以将任何类型的操作数存入寄存器中,但它不能将一个值从memory location 存入到另一个memory location。

       这些约束被表示为多个选项。可选方案可以用每个操作数的一系列字母来描述。操作数的总体约束是由第一个选项中的这个操作数的字母构成的,然后逗号开始第二个可选项的描述,以此类推,直到最后一个选项一条指令的所有操作数必须有相同数量的可选操作

       所以,68000的 logical-or 第一种方式可以写成 “+m” (output) : “ir” (input)。第二个可以是 “+r” (output): “irm” (input)。因为两个内存位置不能在一条指令中使用,所以不能写成 “+rm” (output) : “irm” (input)。使用多个选项,可以写成 “+m,r” (output) : “ir,irm” (input)。给编译器提供所有可选的方案,有助于编译器根据当前环境选择最高效的实现方案

       在模板中没有办法确定选择了哪个选项。然而,你可以用 __builtin_constant_p 这样的内置函数来包装你的asm语句,以达到预期的结果。


Constraint Modifier Characters——更精确地控制 Constraints 的影响

以下是约束修饰符字符:

Modifier含义
‘=’这个操作数被这条指令写入,先前的值被丢弃并被新数据替换。在有多个选项的约束中,必须在第一个选项中添加“=”约束修饰符,之后的其它选项会有同样的效果,且不允许再添加“=”约束修饰符。
‘+’这个操作数同时被指令读写。

当编译器修复操作数以满足约束时,它需要知道哪些操作数被指令读取,哪些操作数被指令写入。’ = ‘标识一个只被写入的操作数;’ + '标识可读可写的操作数。所有其它操作数都假定只被读取。

如果在约束中指定 ’ = ’ 或 ’ + ’ ,则将其放在约束字符串的第一个字符中。在有多个选项的约束中,必须在第一个选项中添加“+”约束修饰符,之后的其它选项会有同样的效果,且不允许再添加“+”约束修饰符。
‘&’这个操作数是一个 early-clobber 操作数,它在指令使用输入操作数之前被写入了值。因此,这个操作数不能位于被指令读取的寄存器中,也不能位于作为任何内存地址组成部分的寄存器中。

在有多个选项的约束中,有时一个选项需要’ & ‘,而其他选项则不需要。例如,看68000的“movdf” insn。有多个选项的约束‘&’仅仅对包含有‘&’的那个约束起作用,其它没有‘&’的约束不受影响。

如果一个被指令读取的操作数在写入早期结果之前仅作为一个输入,则可以将其绑定到early-clobber操作数。当只有部分读操作数会受到early-clobber的影响时,这种方法通常可以让GCC生成更好的代码。例如,ARM的’ mulsi3 ’ 指令。

此外,如果early-clobber操作数也是一个读/写操作数,则该操作数只有在被使用后才被写入。

’ & ‘通常与’ = ‘或’ + '一起使用。由于early-clobber操作数总是被写入,只读的early-clobber操作数是病态的,将被编译器拒绝。
‘%’声明这个操作数可以和下一个操作数互换。这意味着在生成指令时,这个操作数和下一个操作数的顺序可以互换。此修饰符只能用于输入操作数,而且不能在最后一个操作数上。’ % ’ 会应用到所有的可选约束中,并且必须以第一个字符的形式出现在约束中。

例如,asm(" fadd %0, %1, %2" : “=f” © : “%f” (a), “f” (b) );

这个asm计算操作数a和b的和,并将结果写入操作数c。%修饰符表示,如果编译器可以通过交换操作数a和b生成更好的代码,那么可以交换操作数a和b。

GCC只能处理asm中的一个交换对。如果使用多个 ‘%’,编译器可能会报告错误。注意,如果两个约束选项完全相同,则不需要使用修饰符,这只会在重载过程中浪费时间。


Special constraints for some particular machines

AArch64 family

Special constraints含义
k堆栈指针寄存器(SP)
w浮点寄存器,高级SIMD向量寄存器或SVE向量寄存器
x像w,但限制寄存器0到15(含15)。
y像w,但限制寄存器0到7(包括7)。
Upl低8个SVE谓词寄存器之一(P0到P7)
Upa任何一个SVE谓词寄存器(P0到P15)
I整型常量,在ADD指令中作为立即操作数有效
J在 SUB 指令中作为立即操作数有效的整型常量
K可与32位逻辑指令一起使用的整型常量
L可以与64位逻辑指令一起使用的整型常量
M整型常量,在32位MOV伪指令中作为立即操作数有效。根据值的不同,MOV可以被汇编成几个不同的机器指令
N整型常量,在64位MOV伪指令中作为立即操作数有效
S绝对符号地址或标签引用
Y浮点常数零
Z整数常数为零
Ushpc相对地址的高部分(第12位及以上)(4GB的内存)
Q一种使用单一基寄存器而没有偏移量的内存地址
Ump一种内存地址,适用于SI、DI、SF和DF模式下的 load/store 指令对。

x86 family

Special constraints含义
R遗留寄存器-在所有i386处理器上可用的8个整数寄存器(a, b, c, d, si, di, bp, sp)。
q任何可以访问rl(register low)的寄存器。32位模式下,a、b、c、d;在64位模式下,任何整数寄存器。
Q任何可访问rh(register high)的寄存器; a, b, c和d寄存器。
a寄存器 a
b寄存器 b
c寄存器 c
d寄存器 d
Ssi 寄存器
Ddi 寄存器
Aa和d寄存器。用于将双字(double word)结果返回在 ax:dx 寄存器的指令。返回单个字(single word)的值将以ax或dx的形式分配。例如在i386上实现rdtsc:

unsigned long long rdtsc (void)
unsigned long long tick;
__asm__ __volatile__(“rdtsc”:"=A"(tick));
return tick;

这在x86-64上是不正确的,因为它将在ax或dx中分配tick。你必须使用下面的变量代替:

unsigned long long rdtsc (void)
unsigned int tickl, tickh;
__asm__ __volatile__(“rdtsc”:"=a"(tickl),"=d"(tickh));
return ((unsigned long long)tickh << 32)|tickl;
Ucall-clobbered 整数寄存器
f任何80387浮点(堆栈)寄存器
t80387浮点堆栈(%st(0))的栈顶
u从80387浮点堆栈(%st(1))顶部开始的第二位
y任何MMX寄存器
x任何SSE寄存器
v任何EVEX可编码的SSE寄存器(%xmm0-%xmm31)
Yz第一个SSE寄存器(%xmm0)
I范围在0…31的整数常量,用于一个32位的移位操作
J范围在0…63的整数常量,用于一个64位的移位操作
K带符号的8位整数常量
L0xFF或0xFFFF
M0, 1, 2,或3 (lea指令的偏移)
N无符号8位整数常量(用于 in 和 out 指令)
G标准80387浮点常量
CSSE常数零操作数
e32位有符号整型常量,或已知适合该范围的符号引用(用于符号扩展的x86-64指令中的立即操作数)。
We32位有符号整型常量,或一个已知适合该范围的符号引用(用于需要 non-VOIDmode 立即操作数的符号扩展转换操作)
Wz32位无符号整型常量,或一个已知适合该范围的符号引用(用于需要 non-VOIDmode 立即操作数的符号扩展转换操作)
Wd128位整数常量,其中高64位和低64位字都满足e约束
Z32位无符号整型常量,或一个已知适合该范围的符号引用(用于零扩展的x86-64 立即操作数)
TvVSIB地址操作数
Ts没有段寄存器的地址操作数

这里是从善若水的博客,感谢您的阅读⌨🖥🖱


文章链接

《GNU C/C++ 内联汇编编程指南全集》
《GNU C/C++ 内联汇编——入门级》
《GNU C/C++ 内联汇编——进阶——语法详解》
《GNU C/C++ 内联汇编——进阶——约束详解》
《GNU C/C++ 内联汇编——补充介绍》
《GNU C/C++ 内联汇编——实例参考》
《GNU C/C++ 内联汇编——Intel与ATT汇编语法对比》




以上是关于高级CGNU C/C++ 内联汇编——进阶——约束详解的主要内容,如果未能解决你的问题,请参考以下文章

高级CGNU C/C++ 内联汇编——进阶——语法详解

高级CGNU C/C++ 内联汇编——Intel与ATT汇编语法对比

高级CGNU C/C++ 内联汇编——实例参考

高级CGNU C/C++ 内联汇编——实例参考

高级CGNU C/C++ 内联汇编——入门级

高级CGNU C/C++ 内联汇编——补充介绍