奇怪的'asm'操作数有不可能的约束错误

时间:2017-09-02 18:13:18

标签: c linux gcc x86 inline-assembly

我正在尝试编译一个简单的C程序(Win7 32位,Mingw32 Shell和GCC 5.3.0)。 C代码是这样的:

#include <stdio.h>
#include <stdlib.h>

#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
    :\
    :"a" (addr),\
     "m" (*(n)),\
     "m" (*(n+2)),\
     "m" (*(n+4)),\
     "m" (*(n+5)),\
     "m" (*(n+6)),\
     "m" (*(n+7))\
    )

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")


char *n;
char *addr;

int main(void) {
  char *n = (char *)malloc(100*sizeof(int));
  char *addr =  (char *)malloc(100*sizeof(int));
  set_tss_desc(n, addr);
  free(n);
  free(addr);
  return 0;
}

_set_tssldt_desc(n,addr,type)是一个宏,它的主体是汇编代码。 set_tss_desc(n,addr)是另一个与_set_tssldt_desc(n,addr,type)非常相似的宏。在main函数中调用set_tss_desc(n,addr)宏。

当我尝试编译此代码时,编译器显示以下错误:

$ gcc test.c
    test.c: In function 'main':
    test.c:5:1: error: 'asm' operand has impossible constraints
     __asm__ ("movw $104,%1\n\t" \
     ^
    test.c:16:30: note: in expansion of macro '_set_tssldt_desc'
     #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
                                  ^
    test.c:25:3: note: in expansion of macro 'set_tss_desc'
       set_tss_desc(n, addr);
       ^

奇怪的是,如果我在main函数中注释调用指出,代码编译成功。

int main(void) {
  char *n = (char *)malloc(100*sizeof(int));
  char *addr =  (char *)malloc(100*sizeof(int));
  //I comment it out and code compiled.
  //set_tss_desc(n, addr); 
  free(n);
  free(addr);
  return 0;
}

或者,如果我在汇编代码的输出部分删除了一些变量,它也会编译。

#include <stdio.h>
#include <stdlib.h>

#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
    :\
    :"a" (addr),\
     "m" (*(n)),\
     "m" (*(n+2)),\
     "m" (*(n+4)),\
     "m" (*(n+5)),\
     "m" (*(n+6))\
    )
//I DELETE "m" (*(n+7)) , code compiled

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")


char *n;
char *addr;

int main(void) {
  char *n = (char *)malloc(100*sizeof(int));
  char *addr =  (char *)malloc(100*sizeof(int));
  set_tss_desc(n, addr); 
  free(n);
  free(addr);
  return 0;
}

有人可以向我解释为什么会这样,以及如何解决这个问题?

1 个答案:

答案 0 :(得分:2)

As @MichealPetch says,你正以错误的方式接近这一点。如果您正在尝试为lgdt设置操作数,请在C中执行此操作,并仅对lgdt指令本身使用inline-asm。请参阅代码wiki和代码wiki。

相关:用于搞乱英特尔描述符表的C结构/联合:How to do computations with addresses at compile/linking time?。 (这个问题想要将表生成为静态数据,因此要求在编译时将地址分解为低/高一半)。

另外:Implementing GDT with basic kernel用于某些C + asm GDT操作。或许不是,因为那里的答案只是说问题中的代码是有问题的,没有详细的修复。

Linker error setting loading GDT register with LGDT instruction using Inline assembly得到了Michael Petch的答案,其中包含一些指向更多指南/教程的链接。

回答具体问题仍然有用,即使正确的修正是https://gcc.gnu.org/wiki/DontUseInlineAsm

在启用优化的情况下编译正常。

对于-O0,gcc没有注意到或利用操作数彼此都是小的常量偏移的事实,并且可以使用具有偏移寻址模式的相同基址寄存器。它希望将指向每个输入内存操作数的指针放入一个单独的寄存器中,但是会用完寄存器。 -O1或更高,CSE会做您期望的事情。

您可以在一个简化示例中看到这一点,其中最后3个内存操作数被注释,并且更改asm字符串以包含所有操作数的asm注释。来自gcc5.3 -O0 -m32 on the Godbolt compiler explorer

#define _set_tssldt_desc(n,addr,type)     \
__asm__ ("movw $104,%1\n\t"               \
    "#operands: %0, %1, %2, %3\n"         \
     ...

void simple_wrapper(char *n, char *addr) {
  set_tss_desc(n, addr);  
}


        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ebx
        movl    8(%ebp), %eax
        leal    2(%eax), %ecx
        movl    8(%ebp), %eax
        leal    4(%eax), %ebx
        movl    12(%ebp), %eax
        movl    8(%ebp), %edx
#APP    # your inline-asm code
        movw $104,(%edx)
        #operands: %eax, (%edx), (%ecx), (%ebx)
#NO_APP
        nop                    # no idea why the compiler inserted a literal NOP here (not .p2align)
        popl    %ebx
        popl    %ebp
        ret

但启用优化后,您就会

simple_wrapper:
        movl    4(%esp), %edx
        movl    8(%esp), %eax
#APP
        movw $104,(%edx)
        #operands: %eax, (%edx), 2(%edx), 4(%edx)
#NO_APP
        ret

注意后面的操作数如何使用base + disp寻址模式。

你的约束完全是倒退。你写的内存是你告诉编译器是一个输入操作数。它会假设内存没有被asm语句修改,所以如果你在C中加载它,那么可能会在asm之前移动该负载。和其他可能的破损。

如果您使用过"=m"输出操作数,则此代码是正确的(但与让编译器为您执行此操作相比仍然效率低下。)

您可以编写自己的asm来从单个内存输入操作数进行偏移,但是您需要做一些事情来告诉编译器有关asm语句读取的内存的信息。例如"=m" (*(struct {char a; char x[];} *) n)告诉它您从n开始编写整个对象。 (见this answer)。

AT&amp; T语法x86内存操作数始终是可以离开的,因此如果你这样做,你可以使用2 + %[nbase]而不是单独的操作数

asm("movw $104,    %[nbase]\n\t"
    "movw $123, 2 + %[nbase]\n\t"
    : [nbase] "=m" (*(struct {char a; char x[];} *) n)
    : [addr] "ri" (addr)
);
气体将警告2 + (%ebx)或其最终结束,但没关系。

对于您编写的每个位置使用单独的内存输出操作数将避免告诉编译器您编写哪个内存的任何问题。但是你弄错了:你已经告诉编译器你的代码没有使用n+1,而实际上你正在使用movw $104来存储从n开始的2个字节。所以这应该是uint16_t内存操作数。如果这听起来很复杂,https://gcc.gnu.org/wiki/DontUseInlineAsm。就像迈克尔所说,用C struct在C中做这个部分,并且只对需要它的单个指令使用内联asm。

使用更少的更宽存储指令显然会更有效。 IDK你接下来打算做什么,但任何相邻的常量应该合并到一个32位的存储,如mov $(104 + 0x1234 << 16), %[n0]或其他东西。再次,https://gcc.gnu.org/wiki/DontUseInlineAsm