移植AT&T inline-asm inb / outb包装器以与gcc -masm = intel一起使用

时间:2019-03-02 19:43:57

标签: c gcc x86 inline-assembly osdev

我目前正在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'

2 个答案:

答案 0 :(得分:4)

如果需要使用-masm=intel,则需要确保内联程序集采用Intel语法。 Intel语法是dst,src(AT&T语法是相反的)。 related answer有点有关NASM的Intel变体 1 (不是GAS的变体)和AT&T语法之间的一些区别的有用信息:

  

Stackoverflow Answer中提供了有关如何将NASM Intel语法转换为GAS的AT&T语法的信息,此IBM article中提供了许多有用的信息。

     

[snip]

     

通常最大的区别是:

     
      
  • 使用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和英特尔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]]约束(变量名称)

位置:

  

asmSymbolicName

     

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

     

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

无论您使用-masm=intel还是-masm=att选项进行编译,此版本均应运行 2


脚语

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

答案 1 :(得分:4)

对于GNU C内联汇编https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html,可以使用方言替代方式编写使用或不使用-masm=intel的代码(对于其他人使用的标头,这是个好主意可能包括。)

它的工作方式类似于"{at&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(或多或少)像lock ed指令或{{ 1}},添加一个mfence Clobber以停止编译时对其上的内存访问的重新排序。

如果端口I / O可以触发您最近写入的其他内存的DMA读取您可能还需要一个"memory"垃圾桶 。 (现代的x86具有相干的DMA,因此您不必显式刷新它,但是您不能让编译器在"memory"之后重新排序它,甚至不能优化掉看似无效的存储。)


GAS不支持保存旧模式,因此,在串联asm中使用outb会使您无法知道是否切换回.intel_syntax noprefix

但这通常还是不够的:在填充模板时,您需要让编译器以与语法模式匹配的方式格式化操作数。例如端口号需要扩展为.att_syntax$imm(AT&T),而不是%dxdx,而不带imm前缀。

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

但是您仍然需要自己8(%rdi, %rax, 4)来反转操作数列表;编译器不会尝试为您执行此操作。根据简单的规则,它纯粹是将文本替换为模板。