【问题标题】:How to use ins instruction with GNU assembler如何在 GNU 汇编器中使用 ins 指令
【发布时间】:2021-03-04 19:50:32
【问题描述】:

如何在 GNU 汇编器中使用 x86 ins 指令?指令参考建议使用语法INS m8/16/32, DX,例如m16(我假设)是任何 16 位通用寄存器,其唯一目的是表示是否应该读取字节/字/双字,对吧?

现在,很遗憾,as 拒绝 ins %ax,%dxError: operand type mismatch for 'ins',这是为什么呢?

作为记录,我知道我可以简单地使用 insb 等,但我通过 C++ 程序中的内联汇编调用此指令,并且要读取的输入的大小取决于模板参数(和字符串处理在编译时不是很实用)。

编辑:这是我现在所拥有的,供参考(我不太喜欢宏)

#define INS(T) \
  __asm__ __volatile__("repne \n\t" \
                       "ins" T \
                       : "=D" (dest), "=c" (count) \
                       : "d" (port), "0" (dest), "1" (count) \
                       : "memory", "cc")

template<typename T>
void ins(uint16_t port, uint32_t dest, uint32_t count);

template<>
void ins<uint8_t>(uint16_t port, uint32_t dest, uint32_t count)
{ INS("b"); }

template<>
void ins<uint16_t>(uint16_t port, uint32_t dest, uint32_t count)
{ INS("w"); }

template<>
void ins<uint32_t>(uint16_t port, uint32_t dest, uint32_t count)
{ INS("l"); }

【问题讨论】:

  • 它应该是一个 memory 引用,而不是一个寄存器。英特尔语法中的想法是,您可以编写 ins dword ptr [rdi], dx 用于 32 位读取(又名 insd),ins word ptr [rdi], dx 用于 16 位读取 insw 等。您甚至可以编写 ins dword ptr [foo], dx 和进行 32 位读取,但无论如何数据都会写入[rdi]。尽管如此,AT&T 汇编语法根本不支持这一点,指定大小的唯一方法是使用操作数大小后缀。
  • 可能能够通过将编译器切换为使用 Intel 语法来破解它,但我认为这会很痛苦。我会尝试弄清楚如何在编译时选择正确的insb / insw / insl 之一。我不明白你为什么需要字符串处理;只需编写所有三个并使用模板专业化或switch 或其他任何东西来执行正确的。
  • (别忘了小心你的操作数、约束和破坏;该指令修改了rdi。你知道你不能在非特权代码中执行这些指令,除非你已经做过诸如iopl()之类的事情?我假设您正在编写内核或裸机嵌入式代码,只是其中任何一个都是C ++有点不寻常。)
  • @NateEldredge:好吧,我明白了。我猜我将不得不处理一些代码重复。我想我正确地处理了约束,我会检查编译器的输出,是的,我正在编写一个内核。
  • 其实我觉得可以用操作数修饰符来完成。写一个答案...

标签: x86 g++ inline-assembly att ioports


【解决方案1】:

它应该是内存引用,而不是寄存器。英特尔语法中的想法是,您可以编写 ins dword ptr [rdi], dx 用于 32 位读取(又名 insd),ins word ptr [rdi], dx 用于 16 位读取 insw 等等。您甚至可以编写 ins dword ptr [foo], dx 和进行 32 位读取,但无论如何数据都会写入[rdi]。尽管如此,AT&T 汇编语法根本不支持这一点,指定大小的唯一方法是使用操作数大小后缀。

在 GCC 内联汇编中,您可以使用 z 操作数修饰符获得与操作数大小(基于其类型)匹配的操作数大小后缀 b,w,l,q。见the GCC manual, section "Extended Asm" under "x86 operand modifiers"。因此,如果您适当地使用类型并添加一个引用实际目标(而不是指向它的指针)的操作数,您可以执行以下操作:

template<typename T>
void ins(uint16_t port, T *dest, uint32_t count) {
    asm volatile("rep ins%z2"
        : "+D" (dest), "+c" (count), "=m" (*dest)
        : "d" (port)
        : "memory");
}

Try it on godbolt

这里重要的是目标是T * 而不是通用的uint32_t,因为大小是从T 类型推断出来的。

我还用+ 读写约束替换了重复的输入和输出操作数。挑剔的是,“cc”clobber 是不必要的,因为rep ins 不会影响任何标志,但它在 x86 上是多余的,因为假定每个内联 asm 无论如何都会破坏标志。

【讨论】:

  • 我是赞成票,但我相信这个 asm 声明应该是 volatile,因为它依赖于 x86 端口上的状态,并且可能因运行而异。您不希望编译器优化掉它可能认为多余的端口读取。
  • @MichaelPetch:指定输出操作数时 volatile 会有所不同吗?我什至没有想到这一点,只是出于习惯添加了易失性。
  • @Peter 如果没有输出 = 和输入/输出 + 操作数,那么 ASM 语句是隐式 volatile 并且不需要指定 volatile。在这种情况下,有输入/输出操作数,所以如果你需要它是易失的,你需要指定它。使用 x86 端口和内存映射 IO (MMIO) 时,您需要指定 volatile 以让代码知道编译器不知道的外部更改可能不会在每次操作完成时产生相同的结果。
  • @Peter:是的,如果编译器认为输出操作数之后没有使用,它可以优化掉asm。我无法在这个例子中实现它,但这里有一个更简单的例子:godbolt.org/z/E75qGe
  • @MichaelPetch:我找到了类似的bug 4348
【解决方案2】:

此答案是对 OP 在主要编辑之前提出的问题的第一个版本的回应。这解决了INS 指令的 GNU 汇编器的 AT&T 语法。

INS/INSB/INSW/INSD — Input from Port to String 的指令集参考表明 INS 指令实际上只有 3 种形式。采用字节(B)、字(W)或双字(D)的格式。最终从 DX 中的端口读取 BYTE(b)、WORD(w) 或 DWORD(l) 并写入或 ES:RDI、ES:EDI、ES:DI时间>。没有任何形式的 INS 指令将寄存器作为目标,不像 IN 可以写入 AL/AX/EAX

注意:在IN 指令中,端口被认为是源,并且是AT&T 语法中的第一个参数,格式为instruction src, dest

在 GNU 汇编器中,最简单的方法是简单地使用以下 3 种形式中的任何一种:

insb      # Read BYTE from port in DX to [RDI] or ES:[EDI] or ES:[DI]
insw      # Read WORD from port in DX to [RDI] or ES:[EDI] or ES:[DI]
insl      # Read DWORD from port in DX to [RDI] or ES:[EDI] or ES:[DI]

在 16 位代码中,这些指令可以:

insb      # Read BYTE from port in DX to ES:[DI]
insw      # Read WORD from port in DX to ES:[DI]
insl      # Read DWORD from port in DX to ES:[DI]

在 32 位代码中,这些指令可以:

insb      # Read BYTE from port in DX to ES:[EDI]
insw      # Read WORD from port in DX to ES:[EDI]
insl      # Read DWORD from port in DX to ES:[EDI]

在 64 位代码中,这些指令可以:

insb      # Read BYTE from port in DX to [RDI]
insw      # Read WORD from port in DX to [RDI]
insl      # Read DWORD from port in DX to [RDI]

为什么汇编器也支持长格式?主要用于文档目的,但有一个细微的变化可以用长格式表示,那就是内存地址的大小(而不是要移动的数据的大小)。在 GNU 汇编器中,这在 16 位代码中受支持:

insb (%dx),%es:(%di)      # also applies to INSW and INSL
insb (%dx),%es:(%edi)     # also applies to INSW and INSL

16 位代码可以使用 16 位或 32 位寄存器来形成内存操作数,这就是您可以覆盖它的方式(还有另一个使用 addr 覆盖的方法,如下所述)。在 32 位代码中可以这样做:

insb (%dx),%es:(%di)      # also applies to INSW and INSL
insb (%dx),%es:(%edi)     # also applies to INSW and INSL

可以在 32 位代码的内存操作数中使用 16 位寄存器。很少有使用案例特别有用,但处理器支持它。在 64 位代码中,您可以在内存操作数中使用 32 位或 64 位寄存器,因此在 64 位代码中这是可能的:

insb (%dx),%es:(%rdi)      # also applies to INSW and INSL
insb (%dx),%es:(%edi)      # also applies to INSW and INSL

在 GNU 汇编器中有一种更短的方法来更改内存地址大小,即使用 INSB/INSW/INSLaddr16addr32addr64 覆盖。以 16 位代码为例,这些是等价的:

addr32 insb               # Memory address is %es:(%edi). also applies to INSW and INSL
insb (%dx),%es:(%edi)     # Same as above

在 32 位代码中,这些是等效的:

addr16 insb               # Memory address is %es:(%di). also applies to INSW and INSL
insb (%dx),%es:(%di)      # Same as above

在 64 位代码中,这些是等效的:

addr32 insb               # Memory address is %es:(%edi). also applies to INSW and INSL
insb (%dx),%es:(%edi)     # Same as above

【讨论】:

    猜你喜欢
    • 2010-10-18
    • 2011-11-09
    • 2014-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    相关资源
    最近更新 更多