您告诉编译器您要将输出放入%0,它可以为"=r" 选择任何寄存器。但是,您永远不会在模板中写 %0。
当您可以使用 %0 作为临时变量时,您却无缘无故地使用了两个临时变量。
像往常一样,您可以通过添加像 # 0 = %0 这样的 cmets 并查看编译器的 asm 输出来调试您的内联 asm。 (不是反汇编,只是gcc -S 看看它填充了什么。例如# 0 = %ecx。(你没有使用早期的clobber "=&r",所以它可以选择相同的寄存器作为输入)。
此外,这还有 2 个其他错误:
无法编译。在 ECX 中使用 "c" 约束请求 2 个不同的操作数无法工作,除非编译器可以在编译时证明它们具有相同的值,因此 %1 和 %2 可以是同一个寄存器。 https://godbolt.org/z/LgR4xS
您在不告诉编译器您正在读取指向的内存的情况下取消引用指针输入。使用"memory" clobber 或虚拟内存操作数。 How can I indicate that the memory *pointed* to by an inline ASM argument may be used?
或者更好的https://gcc.gnu.org/wiki/DontUseInlineAsm,因为它对这个没用;只需让 GCC 自己发出 movzb 负载。 unsigned char* 不会受到严格别名 UB 的影响,因此您可以安全地将任何指针投射到 unsigned char* 并取消引用它,甚至不必使用 memcpy 或其他黑客来与更广泛的未对齐或类型双关访问的语言规则作斗争。
但是如果你坚持使用内联汇编,阅读手册和教程,链接https://stackoverflow.com/tags/inline-assembly/info。在坚持使用内联汇编之前,您不能只是将代码扔到墙上:您必须了解为什么您的代码是安全的,才能对它的安全抱有任何希望。内联汇编有很多方法可以正常工作,但实际上被破坏了,或者正在等待与不同的周围代码一起破坏。
这是一个安全且不完全糟糕的版本(除了内联汇编中不可避免的优化失败部分)。您仍然需要 both 加载的 movzbl 加载,即使返回值只有 8 位。 movzbl 是加载字节的自然有效方式,替换而不是与完整寄存器的旧内容合并。
unsigned char read(void *index, void *data)
{
uintptr_t value;
asm (
" movzb (%[idx]), %k[out] \n\t"
" movzb (%[arr], %[out]), %k[out]\n"
: [out] "=&r" (value) // early-clobber output
: [idx] "r" (index), [arr] "r" (data)
: "memory" // we deref some inputs as pointers
);
return value;
}
注意输出中的 early-clobber:这会阻止 gcc 选择相同的输出寄存器作为输入之一。在第一次加载时销毁[idx] 寄存器是安全的,但我不知道如何在一个asm 语句中告诉GCC。您可以将 asm 语句拆分成两个单独的语句,每个语句都有自己的输入和输出操作数,通过局部变量将第一个的输出连接到第二个的输入。那么谁都不需要 early-clobber,因为它们只是包装单个指令,比如 GNU C 内联 asm 语法旨在做得很好。
Godbolt with test caller to see how it inlines / optimizes 被调用两次,使用 i386 clang 和 x86-64 gcc。例如在寄存器中请求 index 会强制执行 LEA,而不是让编译器看到 deref 并让它为 *index 选择寻址模式。此外,在添加到 unsigned sum 时,编译器完成了额外的 movzbl %al, %eax,因为我们使用了窄返回类型。
我使用了uintptr_t value,所以它可以编译为 32 位和 64 位 x86。 使 asm 语句的输出比函数的返回值更宽是没有害处的,并且例如,如果 GCC 选择 AL 作为 8 位输出变量,我们就不必使用像 movzbl (%1), %k0 这样的大小修饰符来让 GCC 打印 32 位寄存器名称(如 EAX)。
我确实决定实际使用 %k[out] 来获得 64 位模式的优势:我们想要 movzbl (%rdi), %eax,而不是 movzb (%rdi), %rax(浪费 REX 前缀)。
不过,您不妨声明函数返回unsigned int 或uintptr_t,这样编译器就知道它不必重做零扩展。 OTOH 有时它可以帮助编译器知道值范围仅为 0..255。你可以告诉它你使用if(retval>255) __builtin_unreachable() 或其他东西产生了一个正确的零扩展值。或者你可以不使用内联asm。
您不需要asm volatile。 (假设您希望在结果未使用时让它优化掉,或者被提升出循环以获得恒定输入)。你只需要一个"memory" clobber,所以如果它被使用了,编译器就知道它在读取内存。
(A "memory" clobber 将所有内存视为输入,所有内存都视为输出。因此它不能 CSE,例如提升出循环,因为据编译器所知,一次调用可能会读取一些内容前一篇写道。所以在实践中,"memory" 破坏者与asm volatile 一样糟糕。即使在不触及输入数组的情况下对该函数的两次背靠背调用也会强制编译器发出两次指令。)
您可以使用虚拟内存输入操作数来避免这种情况,因此编译器知道这个 asm 块不会修改内存,只读取它。但是,如果您真的关心效率,则不应该为此使用内联 asm。
但就像我说的那样,使用内联 asm 的理由为零:
这将在 100% 便携且安全的 ISO C 中做同样的事情:
// safe from strict-aliasing violations
// because unsigned char* can alias anything
inline
unsigned char read(void *index, void *data) {
unsigned idx = *(unsigned char*)index;
unsigned char * dp = data;
return dp[idx];
}
如果您坚持每次访问都发生并且不被优化掉,您可以将一个或两个指针投射到volatile unsigned char*。
或者甚至可能是atomic<unsigned char> *,这取决于你在做什么。 (这是一个 hack,更喜欢 C++20 atomic_ref 以原子方式加载/存储通常不是原子的对象。)