ASM 约束副作用
Posted
技术标签:
【中文标题】ASM 约束副作用【英文标题】:ASM constraints side effects 【发布时间】:2019-08-07 01:42:50 【问题描述】:我很难理解某些特定的影响 GCC 中用于内联汇编的约束。
在下面的示例中,如果我在输出上使用“=X”并在所有输入上使用“X”运行,则 2 打印输出
0x562f39629260, 100
0x14, 100
这表明指向我分配的缓冲区的指针已更改。导致 Segfault 是尝试读取缓冲区的内容 在我的汇编代码之后。
相反,如果我在输出中输入“+X”或在输入中输入“m”,那么 地址保持不变,打印输出:
0x55571bb83260, 100
0x55571bb83260, 100
而且我可以安全地读取缓冲区而不会出现段错误。
我不明白如何或为什么应该/可以修改此指针? 有没有办法安全地选择约束? gcc 在线文档 对此没有提供太多见解。
非常感谢,
int main()
long size = 100;
char * buffer = (char*)malloc(size*sizeof(char));
printf("%p, %d\n",buffer, size);
__asm__(
"mov %[out], %%rcx \n"
"mov %[size], %%rbx \n"
"loop: \n"
"movb $1, (%%rcx) \n"
"add $1, %%rcx \n"
"sub $1, %%rbx \n"
"jnz loop \n"
: "=X"(buffer) //outputs
: [out]"X"(buffer), [size]"X"(size) //inputs
: "rbx", "rcx" //clobbers
);
printf("%p, %d\n",buffer, size);
return 0;
【问题讨论】:
为什么buffer
是一个输出?另外,您到底为什么要使用X
约束?当然=X
打破buffer
...你基本上告诉编译器它应该从它想要的任何地方为buffer
获取一个新值,而你甚至没有引用那个参数。
【参考方案1】:
=X
中的 =
表示这是一个 OUTPUT ONLY 约束(与 +
的更新约束相反)。这意味着汇编代码应该向操作数(%0
)写入一些内容,这将是输出的值。但是由于您的汇编代码从不写入%0
,因此您会得到该位置的任何垃圾(可能是寄存器分配器选择的寄存器)。
尝试在 asm 代码中添加 mov %%rcx,%0
行,看看实际发生了什么。
你真正想要的很可能更像是:
__asm__ volatile (
"mov %[size], %%rbx \n"
"loop: \n"
"movb $1, (%[out]) \n"
"add $1, %[out] \n"
"sub $1, %%rbx \n"
"jnz loop \n"
: [out]"+r"(buffer) //outputs
: [size]"X"(size) //inputs
: "rbx", "memory" //clobbers
);
请注意,这会使buffer
指向插入的值之后(在缓冲区的末尾)——不清楚这是否是您想要的。你可以对尺寸做同样的事情,让它变得更简单:
__asm__ volatile (
"loop: \n"
"movb $1, (%[out]) \n"
"add $1, %[out] \n"
"sub $1, %[size] \n"
"jnz loop \n"
: [out]"+r"(buffer), [size]"+X"(size) //outputs
: //inputs
: "memory" //clobbers
);
虽然完全不使用 asm 会更简单(并且对优化器来说更好):
do *buffer++ = '\1'; while (--size);
总结一下下面所有的 cmets,你可能想要的是这样的:
long size = 100;
char buffer[100];
char *temp;
__asm__(
"loop: \n"
"movb $1, (%[out]) \n"
"add $1, %[out] \n"
"sub $1, %[size] \n"
"jnz loop \n"
: [out]"=r"(temp), [size]"+X"(size), "=m"(buffer) //outputs
: "0"(buffer) // inputs
) // no clobbers
对整个缓冲区使用"=m"
约束而不是内存破坏和易失性意味着如果不使用任何结果,则可以消除死代码
对在缓冲区上移动的指针使用临时值意味着可以保留原始缓冲区(开始)值。
如果必须对缓冲区使用 malloc,"=m"(*(char (*)[100])buffer)
可用于获取对整个缓冲区的约束。
但是,我坚持我之前的评论,即不使用 asm 编写它会更好;它更简单,更容易理解,编译器的优化器可能会为你向量化它。
【讨论】:
+@MichaelPetch:"+m"(*buffer)
只告诉编译器 first 字节是输入/输出。这对于新分配的内存来说可能已经足够了,但是当前推荐的语法是转换为指向数组的指针并取消引用。我最近在Looping over arrays with inline assembly 上使用该语法的未知/已知大小版本更新了我的答案:对于这种情况,您可以使用[dummy] "+m" (*(char(*)[100]) buffer)
以及 作为您实际使用的[out] "+r"(buffer)
。如果这在没有早期破坏者的情况下是安全的,那么 dummy 可以使用相同的指针
@PeterCordes :我在出去的路上用手机输入了它,当时我并没有真正考虑。我实际上想指出的是,malloc
在这里毫无意义,因为它没有被释放。无论如何我都会在堆栈上放置一个 100 个字符的本地数组(char buffer[100]
)然后会考虑过: [out]"+m"(buffer)
。在这种情况下,它将是整个数组。更改数组的创建方式是我的初衷,然后我放弃了我后悔发表的评论..
@MichaelPetch:哦,太好了,我没有考虑过取消引用指向数组的指针的“基本情况”,但现在你得到整个数组内容是有道理的,而不是衰减 - "+m"(buffer)
的指向指针。
@Chris:请参阅我的第一条评论,了解一种使此安全没有 "memory"
clobber 和没有volatile
的方法。 (使用你的,数组内容的副作用通过输出约束对编译器不可见,因此如果你不使用 size
或之后的指针的修改副本,它可以优化 asm 语句。只有一个没有输出的asm
是隐式易失的)。顺便说一句,我应该使用"=m" (*(char(*)[100]) buffer)
,因为它是仅输出。我们不想告诉编译器我们用"+m"
读取了未初始化的数据,而实际上我们并没有。
@JeanMi:使用int *asm_input = buffer;
tmp var,或使用单独的输入和输出变量(输入具有"0" (buffer)
匹配约束),并带有虚拟输出。或者只是将您的 asm 块放在 static inline
包装函数中,这样您就可以以这种方式获取变量的副本,而不会影响调用者的变量副本。但实际上,对于 x86,您通常应该使用内在函数。编译器绝对不总是完美的,但它更便携,通常足以使用内在函数。 (而且你必须是专家才能击败编译器,例如agner.org/optimize)以上是关于ASM 约束副作用的主要内容,如果未能解决你的问题,请参考以下文章