在X64 gcc内联asm中调用scanf

时间:2018-04-08 23:03:26

标签: c arrays linux scanf

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

BTW:假设数组的宽度和高度已经知道。

这是我在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"
    );
}

执行此代码时,错误显示Segmentation fault。 怎么可能出错? 谢谢!

1 个答案:

答案 0 :(得分:0)

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认为你的函数是一个leaf函数(并且如果唯一的函数调用在你的汇编代码中,对编译器是不透明的,它可能会这样认为)它可能正在利用red zone,把东西放在rsp的当前值之下。您的推送和函数调用可能会覆盖此数据。你can try to fight even this,但又一次,它是丑陋的东西。

所以:很清楚为什么你的代码不起作用,而且很清楚,在x86_64调用约定上使它正常工作非常复杂 - 你&# 39; d必须根据迭代将东西放在不同的寄存器或堆栈中,并找到告诉gcc你正在弄乱堆栈指针并避免使用红色区域的方法。

我不清楚的是:这件事的重点是什么?如果你需要读取很多值,如果值的数量是固定的,你可以做一个&#34; normal&#34;在简单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的可变参数调用是一种可怕的,不可移植的解决问题的解决方案。