装配内联 AT&T 类型不匹配

Posted

技术标签:

【中文标题】装配内联 AT&T 类型不匹配【英文标题】:Assembly inline AT&T Type mismatch 【发布时间】:2017-07-13 19:07:17 【问题描述】:

我正在学习汇编,但没有发现任何可以帮助我做到这一点的东西。甚至可能吗?我无法完成这项工作。

我希望这段代码采用“b”值,将其放入%eax,然后在我的输出中移动%eax 的内容并打印该ASCII 字符,在本例中为“0”。

char a;
int b=48;
__asm__ ( 
//Here's the "Error: operand type mismatch for `mov'
"movl %0, %%eax;"
"movl %%eax, %1;"

:"=r"(a)
:"r" (b)
:"%eax"
);

printf("%c\n",a);

【问题讨论】:

由于内联汇编器本质上是编译器和平台特定的,因此您需要识别两者才能获得一个体面的答案。 我想知道它与 AT&T 有什么关系... a 是一个字符,所以 8 位。您正在尝试将其移动到 32 位寄存器中。您应该酌情使用 8 位寄存器或符号/零扩展。当然你的代码不好的做法,如果你想在一个特定的寄存器中的东西,要求编译器把它放在那里不要使用mov 【参考方案1】:

导致错误的指令是这个:

movl %0, %%eax

因此,为了弄清楚它导致错误的原因,我们需要了解它所说的内容。这是一个 32 位的 MOV 指令(AT&T 语法中的 l 后缀表示“长”,又名 DWORD)。目标操作数是 32 位 EAX 寄存器。源操作数是第一个输入/输出操作数a。换句话说,这是:

"=r"(a)

表示char a; 将用作仅输出寄存器。

因此,内联汇编器想要做的是生成如下代码:

movl %dl, %eax

(为了论证,假设a 分配在dl 寄存器中,但它可以很容易地分配在任何8 位寄存器中)。问题是,该代码无效,因为存在操作数大小不匹配。源操作数和目标操作数的大小不同:一个是 32 位,另一个是 8 位。这行不通。

解决方法是 movzx/movsx 指令(随 80386 引入)将 8(或 16)位源操作数移动到 32 位目标操作数,分别使用零扩展或符号扩展。在 AT&T 语法中,将 8 位源移动到 32 位目标的形式是 movzbl(用于零扩展,与无符号值一起使用)或 movsbl(用于符号扩展,与有符号值一起使用)。

但是等等——这是错误的解决方法。由于另一个原因,您的代码无效:a 未初始化!不仅a 未初始化,而且您通过输出约束告诉内联汇编器它是一个仅输出 操作数(= 符号)!所以你不能从中读取——你只能存储到它里面。

你有你的操作数符号倒退。您真正想要的是以下内容:

__asm__( 
        "movl %1, %%eax;"
        "movl %%eax, %0;"

        : "=r"(a)
        : "r" (b)
        : "%eax"
       );

当然,这仍然会导致操作数大小不匹配,但它现在位于 second 汇编指令中。这告诉内联汇编器发出的是以下代码:

    movl $48,  %edx
    movl %edx, %eax
    movl %eax, %dl

这是无效的,因为 32 位源 (%eax) 无法移动到 8 位目标 (%dl)。你不能用movzx/movsx 来解决这个问题,因为那是用来扩展,而不是截断。编写方式如下:

    movl $48,  %edx
    movl %edx, %eax
    movb %al,  %dl

最后一条指令是 8 位移动,从 8 位源寄存器到 8 位目标寄存器。

在内联汇编中,这会写成:

__asm__( 
        "movl %1, %%eax;"
        "movb %%al, %0;"

        : "=r"(a)
        : "r" (b)
        : "%eax"
       );

但是,这不是使用内联汇编的正确方法。您已经在内联汇编块内手动硬编码了EAX 寄存器,这意味着您必须破坏它。这样做的问题是,当涉及到寄存器分配时,它把编译器的双手束缚在背后。您应该要做的是将进出内联汇编块的所有内容放入输入和输出操作数中。这使编译器能够以最优化的方式处理所有寄存器分配。代码应该如下所示:

char a;
int  b = 48;
int temp;
__asm__( 
        "movl %2, %0\n\t"
        "movb %b0, %1"

        : "=r"(temp),
          "=r"(a)
        : "r" (b)
        :
       );

这里发生了很多变化:

我引入了另一个临时变量(适当地命名为temp)并将其添加到仅输出操作数列表中。这会导致编译器自动为其分配一个寄存器,然后我们在 asm 块中使用它。 现在我们让编译器进行寄存器分配,我们不需要 clobber 列表,所以它是空的。 movb 指令的源操作数需要b 修饰符,以确保使用该寄存器的字节大小部分,而不是整个 32 位寄存器。 我没有在每条 asm 指令的末尾使用分号,而是使用了\n\t(最后一条除外)。这是推荐用于内联汇编块的内容,它可以让您获得更好的汇编输出列表,因为它与编译器内部所做的相匹配。

更好的办法是为操作数引入符号名称,使代码更具可读性:

char a;
int  b = 48;
int temp;
__asm__( 
        "movl %[input], %[temp]\n\t"
        "movb %b[temp], %[dest]"

        : [temp]  "=r"(temp),
          [dest]  "=r"(a)
        : [input] "r" (b)
        :
       );

而且,在这一点上,如果您还没有注意到,您会发现这段代码非常愚蠢。您不需要所有这些临时人员和注册注册改组。你可以这样做:

    movl $48, %eax

48 的值已经在al 中,因为al 是32 位寄存器eax 的低8 位。

或者,你可以这样做:

    movb $48, %al

这只是将值 48 显式移动到 8 位寄存器 al 的 8 位。

但是,事实上,如果你调用printf,参数必须作为int(不是char,因为它是一个可变参数函数)传递,所以你绝对想要:

    movl $48, %eax

当您开始使用内联汇编时,编译器无法轻易通过它进行优化,因此您会得到低效的代码。您真正需要的是:

int a = 48;
printf("%c\n",a);

生成以下汇编代码:

    pushl   $48
    pushl   $AddressOfFormatString
    call    printf
    addl    $8, %esp

或者,等效地:

    movl    $48, %eax
    pushl   %eax
    pushl   $AddressOfFormatString
    call    printf
    addl    $8, %esp

现在,我想你是在对自己说:“是的,但是如果我这样做,那么我就不会使用内联汇编!”对此我的回应是:完全正确。您在这里不需要内联汇编,事实上,您应该使用它,因为它只会导致问题。它更难编写,导致代码生成效率低下。

如果您想学习汇编语言编程,请获得一个汇编器并使用它——而不是 C 编译器的内联汇编器。 NASM 和YASM 一样,是一个受欢迎的绝佳选择。如果您想坚持使用 Gnu 汇编程序以便坚持使用这种曲折的 AT&T 语法,请运行 as

【讨论】:

【参考方案2】:

由于a 被定义为字符(char a;),:"=r"(a) 将分配一个 8 字节的寄存器。 32 字节寄存器EAX 无法加载 8 字节寄存器 - movl %dl, %eax (movl %0, %%eax) 将导致此错误。有符号扩展和零扩展指令 movzxmovsx(Intel 语法),在 AT&T 语法中:movs...movz... 用于此目的。

改变

movl %0, %%eax;

movzbl %0, %%eax;

【讨论】:

以上是关于装配内联 AT&T 类型不匹配的主要内容,如果未能解决你的问题,请参考以下文章

内联汇编中的 vpcmpeqb

内联asm:'out'的操作数类型不匹配

SSE2 和内联装配插入结构

请验证 AT&T 装配线的含义

简单 g++ 内联汇编程序中的错误

需要一个很好的 AT&T 汇编语法来源