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

问题描述 投票:3回答:2

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

这与outbio_wait也可能相同。

我使用的是英特尔语法(-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'

c gcc x86 inline-assembly osdev
2个回答
4
投票

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

有关如何将NASM Intel语法转换为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选项。

如果您的代码需要支持多个汇编语言(例如,如果您正在编写需要支持各种编译选项的公共头文件),请使用以下形式的构造:

{dialect0 | dialect1 | dialect2 ...}

在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选项进行编译,此版本都应该有效


脚注

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

4
投票

编写使用或不使用-masm=intel的代码是可能的,使用GNU C inline asm https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html的方言替代品(对于其他人可能包含的标题,这是一个好主意。)

它像"{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(或多或少)序列化像locked指令或mfence的事实,添加一个"memory" clobber来阻止编译时重新排序内存访问。

如果端口I / O可以触发您最近写的其他内存的DMA读取,那么您可能还需要一个"memory" clobber。 (现代x86具有连贯的DMA,所以你不必显式刷新它,但是你不能让编译器在outb之后重新排序,或者甚至优化掉一个看似死的商店。)


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

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

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

但你仍然需要自己负责用{ | }来反转操作数列表;编译器不会尝试为您执行此操作。根据简单的规则,它纯粹是对模板的文本替换。

© www.soinside.com 2019 - 2024. All rights reserved.