我知道使用内联汇编解决以下问题是一个坏主意。我目前正在学习内联汇编作为linux内核类的一部分,这是该类赋值的一部分。
开头,下面是一段几乎正确的代码片段,而不是段错误。它是一个函数,它将src
的子字符串从索引s_idx
开始,并在索引e_idx
结束(排他地),仅使用内联汇编复制到预分配的dest
。 / p>
static inline char *asm_sub_str(char *dest, char *src, int s_idx, int e_idx) {
asm("addq %q2, %%rsi;" /* Add start index to src (ptrs are 64-bit) */
"subl %k2, %%ecx;" /* Get length of substr as e - s (int is 32-bit) */
"cld;" /* Clear direction bit (force increment) */
"rep movsb;" /* Move %ecx bytes of str at %esi into str at %edi */
: /* No Ouputs */
: "S" (src), "D" (dest), "g" (s_idx), "c" (e_idx)
: "cc", "memory"
);
return dest;
}
此代码的问题是第二个输入参数的约束。使用gcc
的默认优化和-ggdb
进行编译时,会生成以下程序集:
Dump of assembler code for function asm_sub_str:
0x00000000004008e6 <+0>: push %rbp
0x00000000004008e7 <+1>: mov %rsp,%rbp
0x00000000004008ea <+4>: mov %rdi,-0x8(%rbp)
0x00000000004008ee <+8>: mov %rsi,-0x10(%rbp)
0x00000000004008f2 <+12>: mov %edx,-0x14(%rbp)
0x00000000004008f5 <+15>: mov %ecx,-0x18(%rbp)
0x00000000004008f8 <+18>: mov -0x10(%rbp),%rax
0x00000000004008fc <+22>: mov -0x8(%rbp),%rdx
0x0000000000400900 <+26>: mov -0x18(%rbp),%ecx
0x0000000000400903 <+29>: mov %rax,%rsi
0x0000000000400906 <+32>: mov %rdx,%rdi
0x0000000000400909 <+35>: add -0x14(%rbp),%rsi
0x000000000040090d <+39>: sub -0x14(%rbp),%ecx
0x0000000000400910 <+42>: cld
0x0000000000400911 <+43>: rep movsb %ds:(%rsi),%es:(%rdi)
0x0000000000400913 <+45>: mov -0x8(%rbp),%rax
0x0000000000400917 <+49>: pop %rbp
0x0000000000400918 <+50>: retq
这与第二个输入参数的约束设置为"m"
而不是"g"
时生成的程序集相同,这使我相信编译器正在有效地选择"m"
约束。在使用gdb逐步执行这些指令时,我发现有问题的指令是+35
,它将起始偏移量索引s_idx
添加到src
中的%rsi
指针。问题当然是s_idx
只有32位,而静态上该位置的64位整数的高4字节不一定是0.在我的机器上,它实际上是非零并导致除了混淆%rsi
的高4字节,这导致指令+43
中的段错误。
当然,上面的解决方案是将参数2
的约束更改为"r"
,因此它被放置在自己的64位寄存器中,其中前4个字节被正确归零并称之为天。相反,我的问题是,当表达式"g"
表示参数"m"
的值时,为什么gcc会将"r"
约束解析为"%q2"
而不是2
将用作64位值?
我不太了解gcc如何解析内联汇编,我知道在汇编中没有真正的输入感,但我认为gcc可以识别s_idx
对long
的有效隐式转换{1}}当它在第一个内联指令中用作64位值时。 FWIW,如果我明确地将"g" (s_idx)
更改为"g" ((long) s_idx)
,则gcc会将"g"
约束解析为"r"
,因为(long) s_idx
是临时值。我认为gcc也可以隐含地做到这一点吗?
答案 0 :(得分:4)
但我认为gcc可以识别
s_idx
在第一个内联指令中用作64位值时long
的有效隐式转换。
不,在编译周围的代码时,gcc只会查看约束,而不是asm
模板字符串()。填充%
模板操作数的gcc部分与周围代码的寄存器分配和代码生成完全分开。
没有任何东西检查是否理智或理解正在使用模板操作数的上下文。也许你有一个16位输入,并希望将其复制到vmovd %k[input], %%xmm0
/ vpbroadcastw %%xmm0, %%ymm0
的向量寄存器中。高16位被忽略,因此您不希望gcc浪费时间零或为您进行符号扩展。但是你肯定想使用vmovd
而不是vpinsrw $0, %[input], %%xmm0
,因为那会更多uops并且具有错误的依赖性。对于所有gcc知道或关心,你可以在asm注释行中使用操作数,如"# low word of input = %h2 \n
。
GNU C inline asm的设计使得约束可以告诉编译器它需要知道的一切。因此,您需要手动将s_idx
投射到long
。
您不需要为ECX转换输入,因为sub
指令会隐式地将结果零扩展(到RCX中)。您的输入是签名类型,但可能您希望差异始终是正面的。
必须始终假定寄存器输入具有超出输入类型宽度的高垃圾。这类似于x86-64 System V调用约定中的函数args可以具有{{3}但是(我假设)没有关于扩展到32位的不成文规则。 (请注意,在函数内联之后,你的asm语句的输入可能不是函数args。你不想使用__attribute__((noinline))
,正如我所说它无论如何都不会有帮助。 )
让我相信编译器正在有效地选择&#34; m&#34;约束
是的,gcc -O0
在每个C语句之间将所有内容泄露到内存中(因此如果在断点处停止,您可以使用调试器更改它)。因此,内存操作数是编译器最有效的选择。它需要一个加载指令才能将其恢复到寄存器中。即值{em>在<{1}}语句之前的内存中 ,位于asm
。
(clang在多选项限制条件下很糟糕,甚至在-O0
时也会选择内存,即使这意味着首先出现溢出,但gcc也没有这个问题。)
-O3
(和gcc -O0
)将使用立即数作为clang
约束。 g
。在你的情况下,你得到:
"g" (1234)
...
addq $1234, %rsi;
subl $1234, %ecx;
rep movsb
...
之类的输入即使在"g" ((long)s_idx)
也会使用注册,就像-O0
或任何其他临时结果一样(只要x+y
已经不存在s_idx
)。有趣的是,即使long
和(unsigned)
的大小相同,并且强制转换没有指令,即使int
也会产生注册操作数。在这一点上,您确切地看到了unsigned
的优化程度,因为您得到的更多依赖于gcc内部设计的设计方式,而不是有意义或有效的方式。
启用优化后编译如果您想看到有趣的asm。请参阅can have garbage in the upper 32 bits,特别是关于Matt Godbolt的CppCon2017关于查看编译器输出的讨论的链接。
虽然在没有优化的情况下检查asm对于内联asm来说也是好的;如果它只是寄存器,你可能没有意识到使用gcc -O0
覆盖的问题,尽管 仍然是一个问题。检查它在q
中如何内联到几个不同的调用者中也很有用(特别是如果你使用一些编译时常量输入进行测试)。
除了上面讨论的高垃圾问题,你修改输入操作数寄存器而不告诉编译器。
通过使其中一些-O3
读/写输出来解决此问题意味着默认情况下您的asm语句不再是"+"
,因此如果输出未使用,编译器将对其进行优化。 (这包括在函数内联之后,因此volatile
对于独立版本是足够的,但是如果调用者忽略返回值则不在内联之后。)
你确实使用了return dest
clobber,因此编译器会假设你读/写内存。您可以告诉它您读取和写入的内存,因此它可以更有效地优化您的副本。请参阅How to remove "noise" from GCC/clang assembly output?:您可以使用虚拟内存输入/输出约束,例如"memory"
"m" (*(const char (*)[]) src)
格式化:请注意在每行末尾使用char *asm_sub_str_fancyconstraints(char *dest, char *src, int s_idx, int e_idx) {
asm (
"addq %[s_idx], %%rsi; \n\t" /* Add start index to src (ptrs are 64-bit) */
"subl %k[s_idx], %%ecx; \n\t" /* Get length of substr as e - s (int is 32-bit) */
// the calling convention requires DF=0, and inline-asm can safely assume it, too
// (it's widely done, including in the Linux kernel)
//"cld;" /* Clear direction bit (force increment) */
"rep movsb; \n\t" /* Move %ecx bytes of str at %esi into str at %edi */
: [src]"+&S" (src), [dest]"+D" (dest), [e_idx]"+c" (e_idx)
, "=m" (*(char (*)[]) dest) // dummy output: all of dest
: [s_idx]"g" ((long long)s_idx)
, "m" (*(const char (*)[]) src) // dummy input: tell the compiler we read all of src[0..infinity]
: "cc"
);
return 0; // asm statement not optimized away, even without volatile,
// because of the memory output.
// Just like dest++; could optimize away, but *dest = 0; couldn't.
}
以提高可读性;否则asm指令全部在一行上,仅由\n\t
分隔。 (如果你正在检查你的asm模板是如何计算出来的话,它会很好地组合,但不是很容易阅读。)
将(使用gcc -O3)编译为
;
我放get string length in inline GNU Assembler。一个更简单的版本修复了错误,但仍使用asm_sub_str_fancyconstraints:
movslq %edx, %rdx # from the (long long)s_idx
xorl %eax, %eax # from the return 0, which I changed to test that it doesn't optimize away
addq %rdx, %rsi;
subl %edx, %ecx; # your code zero-extends (e_idx - s_idx)
rep movsb;
ret
clobber + "memory"
来获得更正确的编译时优化成本,而不是告诉编译器读取和写入哪个内存的版本。
早期clobber :请注意asm volatile
约束:
如果由于一些奇怪的原因,编译器知道"+&S"
地址和src
相等,它可以对两个输入使用相同的寄存器(s_idx
)。这会导致在esi/rsi
中使用之前修改s_idx
。声明保持sub
的寄存器早期被破坏(在最后一次读取所有输入寄存器之前)将强制编译器选择不同的寄存器。
请参阅上面的Godbolt链接,了解导致破解但没有src
的早期破坏者。 (但只有无意义的&
)。早期删除声明通常是多指令asm语句所必需的,以防止更真实的破坏可能性,因此请务必牢记这一点,并且只有当您确定任何只读时才能将其删除输入以与输出或输入/输出操作数共享寄存器。 (当然使用特定寄存器约束限制了这种可能性。)
我在src = (char*)s_idx;
中遗漏了e_idx
中的早期咒语声明,因为唯一的&#34;免费&#34;参数为ecx
,并将它们放在同一个寄存器中将导致s_idx
和sub same,same
根据需要运行0次迭代。
让编译器进行数学运算当然会更有效率,只需要在正确的寄存器中询问rep movsb
的输入。特别是如果rep movsb
和e_idx
都是编译时常量,那么迫使编译器s_idx
立即转移到寄存器然后减去另一个立即数是愚蠢的。
甚至更好,根本不使用inline asm。 (但如果你真的希望mov
测试它的性能,那么内联asm是一种方法.gcc还有调整选项来控制rep movsb
内联的方式,如果有的话。)
如果你可以避免使用 this + a couple other versions on the Godbolt compiler explorer with gcc + clang ,那么内联的asm答案是完整的。