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 约束副作用的主要内容,如果未能解决你的问题,请参考以下文章

在调用asm函数之前调用printf与否的神秘副作用?

ASM:端口 3c8h 和 3c9h 有啥作用?

使用内联 PTX asm() 指令时,'volatile' 有啥作用?

spring-core 中 asm 包的作用

GCC 内联汇编的副作用

ASM字节码操作 ClassWriter COMPUTE_FRAMES 的作用 与 visitMaxs 的关系