Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you explain gcc's inline assembly constraints for the IN, OUT instructions of i386?

As far as I can tell, the constraints used in gcc inline assembly tell gcc where input and output variables must go (or must be) in order to generate valid assembly. As the Fine Manual says, "constraints on the placement of the operand".

Here's a specific, working example from a tutorial.

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

inb is AT&T syntax-speak for the i386 IN instruction that receives one byte from an I/O port.

Here are the specs for this instruction, taken from the i386 manual. Note that port numbers go from 0x0000 to 0xFFFF.

IN AL,imm8  // Input byte from immediate port into AL
IN AX,imm8  // Input word from immediate port into AX
IN EAX,imm8 // Input dword from immediate port into EAX
IN AL,DX    // Input byte from port DX into AL
IN AX,DX    // Input word from port DX into AX
IN EAX,DX   // Input dword from port DX into EAX

Given a statement like uint8_t x = inb(0x80); the assembly output is, correctly, inb $0x80,%al. It used the IN AL,imm8 form of the instruction.

Now, let's say I just care about the IN AL,imm8 form, receiving a uint8_t from a port between 0x00 and 0xFF inclusive. The only difference between this and the working example is that port is now a uint8_t template parameter (to make it effectively a constant) and the constraint is now "N".

template<uint8_t port>
static inline uint8_t inb()
{
    uint8_t ret;
    asm volatile ( "inb %1, %0"
                   : "=a"(ret)
                   : "N"(port) );
    return ret;
}

Fail!

I thought that the "N" constraint would mean, "you must have a constant unsigned 8-bit integer for this instruction", but clearly it does not because it is an "impossible constraint". Isn't the uint8_t template param a constant unsigned 8-bit integer?

If I replace "N" with "Nd", I get a different error:

./test.h: Assembler messages:
./test.h:23: Error: operand type mismatch for `in'

In this case, the assembler output is inb %dl, %al which obviously is not valid.

Why would this only work with "Nd" and uint16_t and not "N" and uint8_t?

EDIT:

Here's a stripped-down version I tried on godbolt.org:

#include <cstdint>

template<uint8_t N>
class Port {
 public:
  uint8_t in() const {
    uint8_t data;

    asm volatile("inb %[port], %%al"
                     :  
                     :  [port] "N" (N)
                     :  // clobbers
    );
    return data;    
  }
};

void func() {
    Port<0x7F>().in();
}

Interestingly, this works fine, except if you change N to anything between 0x80 and 0xFF. On clang this generates a "128 is out of range for constraint N" error. This generates a more generic error in gcc.

like image 590
Robert B Avatar asked Jun 08 '18 22:06

Robert B


1 Answers

Based on how constraints are documented your code should work as expected.

This appears to still be a bug more than a year later. It appears the compilers are converting N from an unsigned value to a signed value and attempting to pass that into an inline assembly constraint. That of course fails when the value being passed into the constraint can't be represented as an 8-bit signed value. The input constraint "N" is suppose to allow an unsigned 8-bit value and any value between 0 and 255 (0xff) should be accepted:

N

Unsigned 8-bit integer constant (for in and out instructions).

There is a similar bug report to GCC's bugzilla titled "Constant constraint check sign extends unsigned constant input operands".

In one of the related threads it was suggested you can fix this issue by ANDing (&) 0xff to the constant (ie: N & 0xff). I have also found that static casting N to an unsigned type wider than uint8_t also works:

#include <cstdint>

template<uint8_t N>
class Port {
 public:
  uint8_t in() const {
    uint8_t data;

    asm volatile("inb %[port], %0"
                     : "=a"(data)
                     :  [port] "N" (static_cast<uint16_t>(N))
                     :  // clobbers
    );
    return data;    
  }
};

void func() {
    Port<0x7f>().in();
    Port<0x80>().in();
//    Port<0x100>().in();    // Fails as expected since it doesn't fit in a uint8_t
}

To test this you can play with it on godbolt.

like image 55
Michael Petch Avatar answered Dec 05 '22 15:12

Michael Petch