【问题标题】:Call scanf in X64 gcc inline asm在 X64 gcc 内联 asm 中调用 scanf
【发布时间】:2018-09-18 07:14:35
【问题描述】:

我有一个动态分配的 2d int 数组,称为 image,以及一个称为 format 的格式字符串。 然后我使用两个嵌套的 for 循环从标准输入中获取输入,并将它们存储在二维数组中。所以我可以动态解析不同长度的输入中的整数。 例如,如果我有一个 3x3 2d 数组,我将需要使用内联 asm 将数组中的元素地址推送 9 次,并推送到格式字符串。然后我调用scanf,完成后平衡堆栈。

顺便说一句:假设数组的宽度和高度已知。

这是我在 Windows 上的代码(X64 系统,以 x32 代码编译)。它工作正常。

for (int i = 0; i < height; i++) {
    for (int j = width-1; j >=0; j--) {
        int tmp_addr = (int)&image[i][j];
        __asm push tmp_addr;
    }
    int pop_size = (width+1) * 4;
    __asm {
        push format;
        call func_scanf;
        mov read_size, eax;
        add esp, pop_size;
    }
}

移植到Linux(X64系统,X64代码编译)时,代码不起作用。

for (int i = 0; i < height; i++) {
    for (int j = width-1; j >=0; j--) {
        long tmp_addr = (long)&image[i][j];
        //__asm push tmp_addr;
        __asm__ __volatile__(
            "push %0\n\t"
            ::"g"(tmp_addr)
        );
    }

    int pop_size = (width+1) * sizeof(long);
    /*__asm {
        push format;
        call func_scanf;
        mov read_size, eax;
        add esp, pop_size;
    }*/
    __asm__ __volatile__(
        "push %0\n\t"
        "call *%1\n\t"
        "mov %%rax,%2\n\t"
        "add %3,%%rsp"
        ::"g"(format),"g"(func_scanf),"g"(read_size),"g"(pop_size)
        :"%rax","%rsp"
    );
}

执行此代码时,错误提示分段错误。 会出什么问题? 谢谢!

【问题讨论】:

  • 也许我遗漏了什么,但你不能循环调用scanf 吗?
  • 你到底为什么要在汇编程序中编码?您正在使用scanf(),因此效率显然不是原因 - 或者,如果是,您需要进行审查并显示证明这种编码可以产生影响的测量结果,因为坦率地说,这是非常不可能的有可衡量的性能优势。所以,你只是喜欢让你的生活更艰难吗?还有其他人需要查看代码吗?它是不可维护的(不清楚它是否可以轻松可靠地开发,但它肯定是不可维护的)。
  • C 代码中已经有很多地方存在未定义和实现定义的行为。在使用内联汇编(无论出于何种原因)进行说明之前,请学习编写正确且可维护的 C 代码。如果没有minimal reproducible example,就真的不可能知道你的代码在哪里/为什么被破坏了。
  • 使用内联 asm 只调用库函数就像让 Usain Bolt 遛狗并期望它能够更快地完成任务。
  • format 字符串的内容是什么?这也可能会造成一些麻烦。

标签: c arrays linux scanf


【解决方案1】:

Linux 上的 x86_64 代码使用 a completely different calling convention,而不是 Windows 上的 x86 代码。特别是,它会在开始使用堆栈之前尝试在寄存器中传递许多参数。此外,可变参数有一些微妙的额外规则(例如,您必须在 rax 中指定实际使用的 XMM 寄存器的数量,如果没有使用则为 0)。

scanf 期望在寄存器中找到前六个指针参数,但是您将它们放在堆栈上,并且寄存器包含垃圾值(无论调用时发生的情况如何);当取消引用它们中的任何一个以写入读取值时,您会得到一个段错误。

此外,现代编译器一般不使用rbp 作为访问局部变量和参数的帧指针,省略了帧指针,通过rsp 访问局部变量。随着你的推送,你在编译器不知道的情况下移动了堆栈指针,现在你的推送和函数调用的返回之间的每个堆栈访问都将被破坏。你hand try to hand-hold the compiler around this,但这是肮脏的生意,容易坏。

更糟糕的是:如果 gcc 认为你的函数是一个叶函数(如果唯一的函数调用在你的汇编代码中,它可能会这样认为,这对编译器是不透明的)它可能会利用 red zone,把东西低于rsp 的当前值。您的推送和函数调用可能会覆盖此数据。你can try to fight even this,但又是丑陋的东西。

所以:很明显为什么您的代码不起作用,而且要使其在 x86_64 调用约定上正常工作非常复杂 - 您必须将内容放入不同的寄存器或堆栈中,具体取决于迭代,并找到一种方法告诉 gcc 您正在弄乱堆栈指针并避免使用红色区域。

我不清楚的是:这件事有什么意义?如果您需要读取许多值,如果值的数量是固定的,您可以在纯 C 中执行 scanf 的“正常”调用。相反,如果要读取的值的数量仅在运行时知道,从您的评论,

它就像"%d %d %d ....",并且动态地改变它的长度。

只需使用适合读取单个值的格式字符串多次调用scanf

for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
        scanf("%d", &image[i][j]);
    } 
} 

这将与您的代码具有完全相同的语义(在它工作的平台上)。顺便说一下,添加一些错误处理(= 检查scanf 的返回值),您的程序会在遇到无效值时停止读取,并继续处理image 中的未初始化值。

如果性能有问题,就放弃scanf - 无论如何你都可以通过手动编写标记化代码然后调用strtol 轻松击败它;通过手动编写转换代码,您甚至可以比strtol(如果您不关心语言环境)更快。

无论如何,深入到汇编级别来构造对scanf 的可变参数调用是一个糟糕的、不可移植的解决方案来寻找问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-08
    • 1970-01-01
    • 1970-01-01
    • 2012-10-20
    • 1970-01-01
    • 2020-02-17
    相关资源
    最近更新 更多