【问题标题】:Porting AT&T inline-asm inb / outb wrappers to work with gcc -masm=intel移植 AT&T inline-asm inb / outb 包装器以使用 gcc -masm=intel
【发布时间】:2019-07-24 12:45:06
【问题描述】:

我目前正在使用我的 x86 操作系统。我尝试从here 实现 inb 函数,它给了我Error: Operand type mismatch for `in'

这也可能与outbio_wait 相同。

我正在使用 Intel 语法 (-masm=intel),但我不知道该怎么做。

代码:

#include <stdint.h>
#include "ioaccess.h"

uint8_t inb(uint16_t port)
{
    uint8_t ret;
    asm volatile ( "inb %1, %0"
                   : "=a"(ret)
                   : "Nd"(port) );
    return ret;
}

使用 AT&T 语法,这确实有效。


对于 outb,我在反转操作数后遇到了不同的问题:

void io_wait(void)
{
    asm volatile ( "outb $0x80, %0" : : "a"(0) );
}

Error: operand size mismatch for `out'

【问题讨论】:

  • 您在编辑中添加的后续内容足够相关,我们可以将其变成一个潜在有用的问题,以配合 MichaelPetch 的涵盖 inb 和 outb 的答案。

标签: c gcc x86 inline-assembly osdev


【解决方案1】:

如果您需要使用-masm=intel,您需要确保您的内联汇编采用 Intel 语法。 Intel 语法是 dst, src(AT&T 语法是相反的)。这有点related answer 有一些有用的信息,关于 NASM 的 Intel 变体1(不是 GAS 的变体)和 AT&T 语法之间的一些差异:

有关如何将 NASM 英特尔语法转换为 GAS 的 AT&T 语法的信息可以在此Stackoverflow Answer 中找到,并且在此IBM article 中提供了许多有用的信息。

[剪辑]

总的来说,最大的区别是:

  • 对于 AT&T 语法,源位于左侧,目标位于右侧,而英特尔则相反。
  • 使用 AT&T 语法,寄存器名称前面带有 %
  • 在 AT&T 语法中,立即数前面带有 $
  • 内存操作数可能是最大的区别。 NASM 使用 [segment:disp+base+index*scale] 代替 GAS 的 segment:disp(base, index, scale) 语法。

您的代码中的问题是源操作数和目标操作数必须与您使用的原始 AT&T 语法相反。这段代码:

asm volatile ( "inb %1, %0"
               : "=a"(ret)
               : "Nd"(port) );

需要:

asm volatile ( "inb %0, %1"
               : "=a"(ret)
               : "Nd"(port) );

关于您的更新:问题在于 Intel 语法中的立即数没有以 $ 开头。这行有问题:

asm volatile ( "outb $0x80, %0" : : "a"(0) );

应该是:

asm volatile ( "outb 0x80, %0" : : "a"(0) );

如果你有一个合适的 outb 函数,你可以这样做:

#include <stdint.h>
#include "ioaccess.h"

uint8_t inb(uint16_t port)
{
    uint8_t ret;
    asm volatile ( "inb %0, %1"
                   : "=a"(ret)
                   : "Nd"(port) );
    return ret;
}

void outb(uint16_t port, uint8_t byte)
{
    asm volatile ( "outb %1, %0"
                   :
                   : "a"(byte),
                     "Nd"(port) );
}

void io_wait(void)
{
    outb (0x80, 0);
}

支持 AT&T 和 Intel dialects 的稍微复杂一点的版本:

asm 模板中的多种汇编语言在 x86 等目标上, GCC 支持多种汇编语言。 -masm 选项控制 GCC 使用哪种方言作为内联汇编程序的默认值。这 -masm 选项的目标特定文档包含列表 支持的方言,以及默认方言(如果选项是) 未指定。理解这些信息可能很重要,因为 使用一种方言编译时可以正常工作的汇编代码 如果使用另一个编译可能会失败。请参阅 x86 选项。

如果您的代码需要支持多种汇编语言(例如 例如,如果您正在编写需要支持 各种编译选项),使用这种形式的结构:

{ 方言0 |方言1 |方言2... }

在 x86 和 x86-64 目标上,有两种方言。 Dialect0 是 AT&T 语法,Dialect1 是 Intel 语法。这些功能可以通过这种方式重新设计:

#include <stdint.h>
#include "ioaccess.h"

uint8_t inb(uint16_t port)
{
    uint8_t ret;
    asm volatile ( "inb {%[port], %[retreg] | %[retreg], %[port]}"
                   : [retreg]"=a"(ret)
                   : [port]"Nd"(port) );
    return ret;
}

void outb(uint16_t port, uint8_t byte)
{
    asm volatile ( "outb {%[byte], %[port] | %[port], %[byte]}"
                   :
                   : [byte]"a"(byte),
                     [port]"Nd"(port) );
}

void io_wait(void)
{
    outb (0x80, 0);
}

我还给出了约束符号名称,而不是使用 %0%1 以使内联程序集更易于阅读和维护。从 GCC 文档中,每个约束具有以下形式:

[ [asmSymbolicName] ] 约束(cvariablename)

地点:

asmSymbolicName

指定操作数的符号名称。通过将其括在方括号中来引用汇编程序模板中的名称(即“%[Value]”)。名称的范围是包含定义的 asm 语句。任何有效的 C 变量名称都是可接受的,包括已在周围代码中定义的名称。同一 asm 语句中的两个操作数不能使用相同的符号名。

当不使用 asmSymbolicName 时,在汇编器模板的操作数列表中使用操作数的(从零开始的)位置。例如,如果有三个输出操作数,则在模板中使用“%0”来引用第一个,“%1”代表第二个,“%2”代表第三个。

无论您使用-masm=intel 还是-masm=att 选项编译,此版本都应该可以工作2


脚注

  • 1虽然 NASM 英特尔方言和 GAS(GNU 汇编器)英特尔语法相似,但还是有一些区别。一种是 NASM Intel 语法使用 [segment:disp+base+index*scale],其中可以在 [] 内指定一个段,而 GAS 的 Intel 语法需要使用 segment 的段外部: [disp+base+index*scale].
  • 2虽然代码可以运行,但您应该将所有这些基本函数直接放在ioaccess.h 文件中,并从包含它们的.c 文件中删除它们。因为您将这些基本函数放在单独的.c 文件(外部链接)中,所以编译器无法尽可能地优化它们。您可以将函数修改为static inline 类型并将它们直接放在标题中。然后,编译器将能够通过消除函数调用开销来优化代码,并减少对额外加载和存储的需求。您将希望使用高于-O0 的优化进行编译。考虑-O2-O3
  • 关于操作系统开发的特别说明
    1. 有许多玩具操作系统(OSDev Wiki 上的示例、教程和甚至代码)无法使用优化。许多失败是由于bad/poor inline assembly 或使用未定义的行为。内联汇编应该作为最后的手段。如果您的内核没有在优化的情况下运行,则可能不是编译器中的错误(有可能只是不太可能)。
    2. 请注意@PeterCordes 回答中有关可能触发 DMA 读取的端口访问的建议。

【讨论】:

【解决方案2】:

可以使用 GNU C 内联 asm https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html 的方言替代方法编写有或没有 -masm=intel 的代码(这是其他人可能包含的标头的好主意。)

它的工作方式类似于"{at&amp;t stuff | intel stuff}":编译器根据当前模式选择要保留| 的哪一侧。

AT&T 与 Intel 语法之间的主要区别在于操作数列表是相反的,所以通常你有类似 "inb {%1,%0 | %0,%1}" 的东西。

这是@MichaelPetch 使用方言替代的漂亮功能的一个版本

// make this a header: these single instructions can inline more cheaply
// than setting up args for a function call
#include <stdint.h>

static inline
uint8_t inb(uint16_t port)
{
    uint8_t ret;
    asm volatile ( "inb {%1, %0 | %0, %1}"
                   : "=a"(ret)
                   : "Nd"(port) );
    return ret;
}

static inline
void outb(uint16_t port, uint8_t byte)
{
    asm volatile ( "outb {%1, %0 | %0, %1}"
                   :
                   : "a"(byte),
                     "Nd"(port) );
}

static inline
void io_wait(void) {
    outb (0x80, 0);
}

Linux/Glibc sys/io.h 宏有时使用 %w1 来扩展对 16 位寄存器名称的约束,但使用正确大小的类型也可以。

如果您想要这些内存屏障版本以利用 in / out (或多或少)像 locked 指令或 mfence 一样序列化这一事实,请添加 @987654333 @clobber 停止编译时内存访问的重新排序。

如果端口 I/O 可以触发对您最近写入的其他内存的 DMA 读取您可能还需要一个 "memory" clobber 来完成此操作。 (现代 x86 具有一致的 DMA,因此您不必显式刷新它,但您不能让编译器在 outb 之后对其进行重新排序,甚至优化掉一个明显的死存储。)


GAS 不支持保存旧模式,因此在内联汇编中使用 .intel_syntax noprefix 会让您无法知道是否切换回 .att_syntax

但这通常是不够的:您需要让编译器在填写模板时以匹配语法模式的方式格式化操作数。例如端口号需要扩展到 $imm%dx (AT&T) 与 dximm 没有 $ 前缀。

或者对于内存操作数,[rdi + rax*4 + 8]8(%rdi, %rax, 4)

但是您仍然需要自己处理使用{ | } 反转操作数列表;编译器不会尝试为您执行此操作。它只是根据简单的规则将文本替换到模板中。

【讨论】:

    猜你喜欢
    • 2013-10-25
    • 2020-01-22
    • 1970-01-01
    • 1970-01-01
    • 2012-01-12
    • 2013-03-04
    • 2014-03-25
    • 1970-01-01
    • 2011-10-16
    相关资源
    最近更新 更多