What is the correct syntax for AMD monitorx instruction?

Viewed 87

Ryzen supports the monitorx instructions, as indicated by the cpuid flag. Unfortunately the visual studio masm assembler doesn't seem to like these instructions, and there is very little documentation online for how to use them.

The following code (which is very based on AMD's own documentation) reports the error A2070 "invalid instruction operands:

push rbx
mov eax, 5844h
mov ecx, 0
mov edx, 0
monitorx eax, ecx, edx
pop rbx
ret

I understand that this code isn't very useful, but it shouldn't be throwing build time errors, so what's the deal?

2 Answers

The problem is that eax, ecx, and edx are 32 bit registers, but it was being assembled in 64 bit mode. Because the first operand is pointer size, it must be 64 bits. The following code will work on 64 bit programs:

push rbx
mov eax, 5844h
mov ecx, 0
mov edx, 0
monitorx rax, rcx, rdx
pop rbx
ret

In machine code, the operands are implicit. In assembly syntax, plain monitorx works for most assemblers. The operands can be specified to document the instruction, or in some assemblers to specify an override to the address-size.

AMD's manual explicitly says that ECX and EDX are 32-bit operands. (They say there are no hints or extensions defined, so ECX must be 0 else #GP, and EDX is ignored by current CPUs.)

For the address operand, they document it as rAX, which I think means it can be EAX or RAX. In the pseudocode example, they use MONITORX EAX, ECX, EDX, but the 32-bit EAX means that's a 32-bit-mode example.

This is in their vol.3 manual, Nov. 2021 rev 3.33, the latest linked from https://developer.amd.com/resources/developer-guides-manuals/.


NASM 2.15.05 (support added for monitorx in version 2.12.01)

The operand list can be specified or omitted. If specified, the first operand must be EAX or RAX, and the next two must be ECX and EDX. (It's an error to specify RCX or RDX).

But NASM doesn't infer 32-bit address-size in 64-bit mode from using EAX.
Segment overrides can be specified with NASM prefixes.
Example listing from nasm -l/dev/stdout -felf64 foo.asm:

     1 00000000 0F01FA             monitorx
     2 00000003 0F01FA             monitorx rax, ecx, edx
     3 00000006 0F01FA             monitorx eax, ecx, edx       ; address-size override not inferred from EAX

     4 00000009 670F01FA           a32 monitorx                 ; address-size prefix, NASM style
     5 0000000D 640F01FA           fs monitorx rax, ecx, edx    ; segment override works, too

ndisasm -b64 disassembles it as just monitorx, fs monitorx, or a32 monitorx without listing the implicit operands. GNU Binutils objdump is the same in -Mintel syntax mode, but lists operands in AT&T mode. (Backwards, with %rax or %eax as the first operand, not reversed like AT&T syntax should be!)


YASM is too old, hasn't been updated in years. I haven't tried FASM.


GNU assembler (GNU Binutils) 2.36.1

Unlike NASM, GAS does infer the address-size from RAX vs. EAX. (Or in 32-bit mode, infer 16-bit address size from monitorx ax, ecx, edx)

GAS does not check sizes on the last two operands, only that they're some version of ECX, EDX (other than dl/dh). For example, monitorx rax, rcx, dx assembles without complaint. Using ebx or dl for the last operand gave "Error: operand type mismatch for 'monitorx'", which is rather unhelpful because it's not the type that's the problem, it's which register it is. (This is typical of GAS error messages, often less helpful to humans than NASM.)

Intel syntax mode, .intel_syntax noprefix
objdump -d -Mintel disassembly   |  source
                                 |    .intel_syntax noprefix
0f 01 fa      monitorx           |    monitorx
0f 01 fa      monitorx           |    monitorx rax, ecx, edx
67 0f 01 fa   addr32 monitorx    |    monitorx eax, ecx, edx       # address-size inferred from EAX
67 0f 01 fa   addr32 monitorx    |    addr32 monitorx              # address-size prefix
64 0f 01 fa   fs monitorx        |    fs monitorx rax, ecx, edx    # segment override works, too
AT&T syntax mode

GAS surprisingly uses the same operand-order as Intel syntax, not reversed like you'd expect for AT&T syntax. So %rax / %eax is still first.

GNU Binutils objdump chooses to include operands when disassembling into AT&T syntax, unlike with -Mintel.

 objdump -d -Matt  disassembly (trimmed)   |   source
-------------------------------------------+---------------------------
0f 01 fa      monitorx %rax,%ecx,%edx      | monitorx
0f 01 fa      monitorx %rax,%ecx,%edx      | monitorx %rax, %ecx, %edx
67 0f 01 fa   monitorx %eax,%ecx,%edx      | monitorx %eax, %ecx, %edx       # address-size inferred from EAX
67 0f 01 fa   monitorx %eax,%ecx,%edx      | addr32 monitorx              # address-size prefix
64 0f 01 fa   fs monitorx %rax,%ecx,%edx   | fs monitorx %rax, %ecx, %edx    # segment override works, too

clang 13.0.0 with LLVM's built-in assembler / llvm-objdump

llvm-objdump gives unhelpful output, not indicating the presence of address-size or segment overrides:

       0: 0f 01 fa                      monitorx
       3: 0f 01 fa                      monitorx
       6: 67 0f 01 fa                   monitorx
       a: 67 0f 01 fa                   monitorx
       e: 64 0f 01 fa                   monitorx

Similarly, LLVM's built-in assembler doesn't seem to accept operands at all, just saying "invalid operand for instruction".

             |  .intel_syntax noprefix
0f 01 fa     |  monitorx
             |  #monitorx rax, ecx, edx     # error
             |  #monitorx eax, ecx, edx     # error
67 0f 01 fa  |  addr32 monitorx             # address-size prefix works
64 0f 01 fa  |  fs monitorx                 # segment override works

Same deal in AT&T syntax mode, whether you reverse the operand-list or not. It doesn't accept monitorx %edx, %ecx, %rax or monitorx %rax, %ecx, %edx.

Either way, LLVM does accept monitorx and correctly assembles prefixes.
It's only the LLVM-objdump disassembler that fails to show prefixes in any way.

Related