GCC内联汇编错误:“int”的操作数大小不匹配。

4

首先,如果有人知道标准C库中一个打印字符串但不查找二进制零的函数,而需要指定字符数,请告诉我!

否则,我有以下问题:

void printStringWithLength(char *str_ptr, int n_chars){

asm("mov 4, %rax");//Function number (write)
asm("mov 1, %rbx");//File descriptor (stdout)
asm("mov $str_ptr, %rcx");
asm("mov $n_chars, %rdx");
asm("int 0x80");
return;

}

GCC在“int”指令中报告以下错误:
"Error: operand size mismatch for 'int'"

有人能告诉我问题出在哪里吗?


1
你可以轻松地编写自己的 for 循环和 putc 函数。 - Retired Ninja
我知道,但这里还有其他原因,我不想谈论 :D - toskana98
哦,我喜欢一个好的毫无意义的秘密。https://dev59.com/2nI-5IYBdhLWcg3whImk - Retired Ninja
2
那个标准库函数被称为fwrite - Antti Haapala -- Слава Україні
2
这不是内联汇编的工作方式。例如变量名在内联汇编中是不可用的,如果您想要使用这些内容,您需要使用扩展内联汇编。请仔细阅读该文章! - fuz
2个回答

10

你的代码存在一些问题。让我一步一步来解释。

首先,int $0x80 系统调用接口仅适用于 32 位代码。在 64 位代码中不应使用它,因为它只接受 32 位参数。 在 64 位代码中,请使用 syscall 接口。系统调用相似,但某些编号不同。

其次,在 AT&T 汇编语法中,立即数必须以美元符号为前缀。因此,应该是 mov $4, %rax,而不是 mov 4, %rax。后者会尝试将地址 4 的内容移动到 rax 中,这显然不是你想要的。

第三,你不能仅仅引用内联汇编中的自动变量名称。如果需要,你必须使用扩展汇编告诉编译器你要使用哪些变量。例如,在你的代码中,你可以这样做:

asm volatile("mov $4, %%eax; mov $1, %%edi; mov %0, %%esi; mov %2, %%edx; syscall"
    :: "r"(str_ptr), "r"(n_chars) : "rdi", "rsi", "rdx", "rax", "memory");

第四,gcc是一个优化编译器。默认情况下,它假定内联汇编语句像纯函数一样,输出是显式输入的纯函数。如果输出没有被使用,asm语句可以被优化掉,或者如果使用相同的输入运行,则可以从循环中提取出来。
但是像write这样的系统调用具有需要保留的副作用,因此它不是纯的。您需要让asm语句以与C抽象机器相同的次数和顺序运行。asm volatile将使这种情况发生。(没有输出的asm语句是隐式易失性的,但是当副作用是asm语句的主要目的时,将其明确表示是一个好习惯。此外,我们确实希望使用输出操作数告诉编译器RAX已经被修改,以及是一个输入,这是我们无法使用clobber实现的。)
你需要始终使用扩展内联汇编语法准确描述汇编的输入、输出和 clobber,否则会干扰编译器(它假定寄存器未更改,除非它们是输出或 clobber)。相关:如何指示内联 ASM 参数所指向的内存可能被使用? 表明一个指针输入操作数仅仅意味着指向的内存不是一个输入。使用虚拟的 "m" 输入或 "memory" clobber 强制所有可达内存保持同步。

你应该简化你的代码,不要编写自己的mov指令将数据放入寄存器,而是让编译器完成这个过程。例如,你的汇编变成了:

ssize_t retval;
asm volatile ("syscall"            // note only 1 instruction in the template
    : "=a"(retval)                 // RAX gets the return value
    : "a"(SYS_write), "D"(STDOUT_FILENO), "S"(str_ptr), "d"(n_chars)
    : "memory", "rcx", "r11"       // syscall destroys RCX and R11
  );

其中SYS_WRITE<sys/syscall.h>中定义,而STDOUT_FILENO<stdio.h>中定义。我不打算向您解释扩展内联汇编的所有细节。通常使用内联汇编是一个坏主意。如果您感兴趣,请阅读文档。(来源: https://stackoverflow.com/tags/inline-assembly/info)

第五,应该尽量避免使用内联汇编。例如,要执行系统调用,请使用unistd.h中的syscall函数:

syscall(SYS_write, STDOUT_FILENO, str_ptr, (size_t)n_chars);

这段代码功能正确,但是它不能内联到您的代码中,因此如果要真正内联系统调用而不是调用libc函数,请使用MUSL的包装器宏。第六点,始终检查您想要调用的系统调用是否已经在C标准库中可用。在这种情况下,它是可用的,所以你只需要写。
write(STDOUT_FILENO, str_ptr, n_chars);

我希望你能避免所有这些问题。

第七点,如果您更喜欢使用stdio,请改用fwrite

fwrite(str_ptr, 1, n_chars, stdout);

2
你使用的 "S"(str_ptr) 实际上存在问题。这并不能保证在内联和优化时该字符串的内容实际上会首先被转储到内存中。一个可能昂贵但快速的解决方法是指定 memory 破坏或更好的方法是,如果你知道特定参数引用了内存,可以添加一个额外的(未使用的)内存约束,例如 "m" (*(const struct {char ch; char str[];} *) ptr_str),以欺骗编译器确保整个数组在执行内联汇编之前被转储到内存中。 - Michael Petch
2
最近的一些SO问题和答案(#1和#2)引起了人们对这个问题的兴趣,最终导致了@DavidWohlferd在该主题上发布了一篇GCC邮件。其中一个问题来自另一个与syscall相关的问题,优化导致仅显示了一些输出。如果有什么不同之处,它表明内联汇编是危险的,除非您知道所有细节。 - Michael Petch
1
@MichaelPetch 谢谢您宝贵的建议。 - fuz
1
@PeterCordes 这也适用于你。 - fuz
1
在这里,您确实需要使用 asm volatile执行IO的可见副作用是需要优化来保留的!因此,如果输出未使用,则不能让编译器优化掉asm语句,与"memory" clobber分开。(您的 asm("syscall" :: ...) 是有问题的,因为您没有告诉编译器 "a" 也是带返回值写入的,并且 RCX 和 R11 被破坏了。它隐含地是易失性的,因为您没有输出操作数,但如果您使用 "+a" 或单独的 "=a" 来制作可用的通用包装器,那么情况就会改变。) - Peter Cordes
显示剩余4条评论

4
你的代码有很多问题(而且使用内联汇编的理由很少),没有必要尝试实际纠正所有这些问题。相反,通过POSIX函数/ libc包装器按照man页中记录的常规方式使用write(2)系统调用,或者使用ISO C <stdio.h>fwrite(3)
#include <unistd.h>

static inline
void printStringWithLength(const char *str_ptr, int n_chars){
    write(1, str_ptr, n_chars);
    // TODO: check error return value
}

为什么你的代码无法汇编:

在AT&T语法中,立即数始终需要使用$修饰符。如果使用asm("int $0x80"),则您的代码将会被汇编。

汇编器会抱怨0x80,这是对绝对地址0x80的内存引用。除了立即数之外,没有任何int形式可以将中断向量作为其他东西传递。我不确定为什么它会抱怨大小,因为在AT&T语法中,内存引用没有隐含的大小。


那样做将使其汇编,此时您将会收到链接错误
In function `printStringWithLength':
5 : <source>:5: undefined reference to `str_ptr'
6 : <source>:6: undefined reference to `n_chars'
collect2: error: ld returned 1 exit status

(来自Godbolt编译器资源管理器)

mov $str_ptr, %rcx

将符号str_ptr的地址mov-immediate到%rcx中。在AT&T语法中,您不必在使用它们之前声明外部符号,因此假定未知名称为全局/静态标签。如果您有一个名为str_ptr的全局变量,则该指令将引用其地址(这是链接时常量,因此可以用作立即数)。
正如其他人所说,这是使用GNU C内联汇编的完全错误方式。请参阅标签wiki以获取更多指南链接。
此外,您正在使用错误的ABI。 int $ 0x80 是x86 32位系统调用ABI,因此它无法与64位指针一起使用。请参阅UNIX&Linux x86-64系统调用的调用约定是什么
还请参阅标签wiki。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接