8

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.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Robert B
  • 3,195
  • 2
  • 16
  • 13
  • 2
    `N` is for **constants** only. `d` picks the register size based on your operand, so you need to cast it to 16 bit. – Jester Jun 08 '18 at 22:52
  • Thanks -- I edited to use a template parameter instead, which I think should be constant. – Robert B Jun 09 '18 at 00:05
  • Your template version works with all gcc versions on godbolt.org. – Jester Jun 09 '18 at 00:10
  • I know, right? Now try it with N = 0x80. In clang, this complains that 128 is out of range for constraint N. I'll add the full code to try in the question. – Robert B Jun 09 '18 at 00:25
  • That's funny. It works with `"N" (0x80)` though, so it's not the constraint per se. – Jester Jun 09 '18 at 00:30
  • 2
    Further testing shows the actual value emitted is `-128` so it's converted to signed somewhere along the way. This also happens if you just do `uint8_t port = 0x80;` and even if you use `"N" ((uint8_t)port)`. PS: at least the assembler accepts `-128` and emits the correct machine code for it. I used lower case `n` to achieve this, but that of course doesn't limit the range to 8 bit only. – Jester Jun 09 '18 at 00:37
  • 2
    Probably should be able to get away with `[port] "N" (static_cast(N))` – Michael Petch Jun 09 '18 at 02:24
  • That indeed works. – Jester Jun 09 '18 at 02:29
  • Maybe I'll report it as a bug, see what happens. – Robert B Jun 09 '18 at 03:16
  • 2
    It would appear to be a bug to me. There is a similar type of bug (but not exactly the same one) that can be found here: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85344 That was reported a couple of months ago. What is interesting is that one workaround that was suggested seems to work here. Use `[port] "N" (N & 0xff)` although casting as I suggested doesn't seem much different. – Michael Petch Jun 09 '18 at 03:38

1 Answers1

1

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.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • `N & 0xff` has type `int` thanks to the integer promotion rules; it's the right value with a wider type. If the bug is in sign-extending narrow unsigned inputs to asm statements, then `(int)N` should work so the actual input to the asm statement is an int, zero-extended by the cast before it becomes an operand for the asm constraint. Or `(unsigned)N` should work around the bug equally well. – Peter Cordes Aug 06 '22 at 07:05