gcc 5.3 内联汇编程序错误?
gcc 5.3 inline assembler bug?
我有这个 32 位代码:
unsigned long long load(volatile unsigned long long *target) {
unsigned long long result;
__asm__ __volatile__
(
"movl %%ecx, %%edx\n\t"
"movl %%ebx, %%eax\n\t"
"lock cmpxchg8b %0\n\t"
"movl %%edx, 4%1\n\t"
"movl %%eax, %1\n\t"
: "+m" (*target)
: "o" (result)
: "eax", "ebx", "ecx", "edx", "memory", "cc"
);
return result;
}
当它用 gcc 5.3 版编译时,代码的尾部生成此汇编代码(为清楚起见略作编辑):
lock cmpxchg8b (%esi)
movl %edx, 48(%esp)
movl %eax, 8(%esp)
movl 8(%esp), %eax
movl 12(%esp), %edx
调用 cmpxchg8b
的结果在 EDX:EAX 中。生成的代码将 EDX 存储在 48(%esp),从 12(%esp) 重新加载 EDX,因此返回值是无意义的。其他版本的 gcc 可以做到这一点。
有人知道这个错误的解决方法吗?还是我误解了有关 gcc 的内联汇编的一些基本知识(这不会让我感到惊讶)?
o
约束意味着一个小整数可以添加到地址,结果也是一个有效的内存地址,所以正确的表达是4+%1
。如果生成的地址恰好使用负偏移量(例如 -8(%ebp)
),您的版本可能会意外工作,在这种情况下,替换后它变为 4-8%(ebp)
。如果偏移量是正数,就像在损坏的情况下 8(%esp)
它当然会扩展到 48(%esp)
这是错误的。 4+%1
在这两种情况下都能正常工作,因为 4+-8(%esp)
和 4+8(%esp)
一样有效。这与编译器版本没有直接关系。
就是说,这个内联 asm 不是很有效,如果您只是将它们声明为输出并将其留给编译器来处理,则可以避免存储 eax
和 edx
的整个过程照顾它:
unsigned long long load(volatile unsigned long long *target) {
unsigned long long result;
__asm__ __volatile__
(
"movl %%ecx, %%edx\n\t"
"movl %%ebx, %%eax\n\t"
"lock cmpxchg8b %0\n\t"
: "+m" (*target), "=&A" (result)
:
: "cc"
);
return result;
}
另请注意,ebx
和 ecx
都没有被修改,因此将它们列为 clobbers 毫无意义,当然也没有触及其他内存,因此 memory
也可以删除。
以上所有都不是真正必要的,因为 gcc 具有原子内置函数,所以整个事情归结为 __atomic_load_n(target, __ATOMIC_SEQ_CST)
。编译器也知道 lock cmpxchg8b
很慢,并且可以 select 更有效的指令以适合目标环境。这个内置函数也更便携。
我有这个 32 位代码:
unsigned long long load(volatile unsigned long long *target) {
unsigned long long result;
__asm__ __volatile__
(
"movl %%ecx, %%edx\n\t"
"movl %%ebx, %%eax\n\t"
"lock cmpxchg8b %0\n\t"
"movl %%edx, 4%1\n\t"
"movl %%eax, %1\n\t"
: "+m" (*target)
: "o" (result)
: "eax", "ebx", "ecx", "edx", "memory", "cc"
);
return result;
}
当它用 gcc 5.3 版编译时,代码的尾部生成此汇编代码(为清楚起见略作编辑):
lock cmpxchg8b (%esi)
movl %edx, 48(%esp)
movl %eax, 8(%esp)
movl 8(%esp), %eax
movl 12(%esp), %edx
调用 cmpxchg8b
的结果在 EDX:EAX 中。生成的代码将 EDX 存储在 48(%esp),从 12(%esp) 重新加载 EDX,因此返回值是无意义的。其他版本的 gcc 可以做到这一点。
有人知道这个错误的解决方法吗?还是我误解了有关 gcc 的内联汇编的一些基本知识(这不会让我感到惊讶)?
o
约束意味着一个小整数可以添加到地址,结果也是一个有效的内存地址,所以正确的表达是4+%1
。如果生成的地址恰好使用负偏移量(例如 -8(%ebp)
),您的版本可能会意外工作,在这种情况下,替换后它变为 4-8%(ebp)
。如果偏移量是正数,就像在损坏的情况下 8(%esp)
它当然会扩展到 48(%esp)
这是错误的。 4+%1
在这两种情况下都能正常工作,因为 4+-8(%esp)
和 4+8(%esp)
一样有效。这与编译器版本没有直接关系。
就是说,这个内联 asm 不是很有效,如果您只是将它们声明为输出并将其留给编译器来处理,则可以避免存储 eax
和 edx
的整个过程照顾它:
unsigned long long load(volatile unsigned long long *target) {
unsigned long long result;
__asm__ __volatile__
(
"movl %%ecx, %%edx\n\t"
"movl %%ebx, %%eax\n\t"
"lock cmpxchg8b %0\n\t"
: "+m" (*target), "=&A" (result)
:
: "cc"
);
return result;
}
另请注意,ebx
和 ecx
都没有被修改,因此将它们列为 clobbers 毫无意义,当然也没有触及其他内存,因此 memory
也可以删除。
以上所有都不是真正必要的,因为 gcc 具有原子内置函数,所以整个事情归结为 __atomic_load_n(target, __ATOMIC_SEQ_CST)
。编译器也知道 lock cmpxchg8b
很慢,并且可以 select 更有效的指令以适合目标环境。这个内置函数也更便携。