How register is encoded in an ARM64 mov instruction?

Viewed 1563

I would like to understand which bit, in a ARM64 mov instruction, are responsible for the register information. I compile my code using clang, targeting aarch64 architecture.

For example, i obtain this instruction with the following machine code:

01418C52 MOVZ            W1, #0x6208

Looking at the documentation "Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile" page C6-1123 enter image description here

Rd is the field holding the register information as specify in the documentation :

Is the 32-bit name of the general-purpose destination register, encoded in the "Rd" field.
Is the 64-bit name of the general-purpose destination register, encoded in the "Rd" field.

Using the website armconverter, i change the value of the register.

I obtain the following code as expected :

02418C52 MOVZ            W2, #0x6208

The hexadecimal value from the left (least significant) changes from 0x01 to 0x02. It seems that the code is little-endian but the documentation is big-endian. But if i change the letter of the register from W to X, another bit is shifted.

02418CD2 MOVZ            X2, #0x6208

The last value at the right is changed from 0xC52 to 0xCD2. Why ?

>>> bin(0xCD2)
'0b110011010010'
>>> bin(0xC52)
'0b110001010010'

From the documentation it is the most significant bit from the field sf who are responsible for the selection of the register based on the size of the immediate value (32b or 64b).

32-bit (sf == 0)

MOVZ <Wd>, #<imm>{, LSL #<shift>}
64-bit (sf == 1)

MOVZ <Xd>, #<imm>{, LSL #<shift>}

But the bit is not at the right location. Perhaps i'm using the wrong documentation. I would like to understand which field in the 32 bits instruction are responsible for the register value.

Thanks

3 Answers

Your confusion comes down entirely to endianness.

From the manual:

B2.6.2      Instruction endianness

                In Armv8-A, A64 instructions have a fixed length
                of 32 bits and are always little-endian.

Disassemblers, on the other hand, have a habit of showing raw bytes - for A64 that is a rather unfortunate choice, but I would assume it stems from the handling of variable-length instruction sets like x86(_64) and ARM/Thumb, where this does make sense.

But in short, when your disassembler shows 01418C52 then those are raw bytes and should be read as 0x528c4101.
Or displayed graphically:

+------+----------+----------+----------+----------+
| Byte |    01    |    41    |    8C    |    52    |
+------+----------+----------+----------+----------+
| Bits | 00000001 | 01000001 | 10001100 | 01010010 |
+------+----------+----------+----------+----------+
                ^                         ^
                |                         |
Least significant bit           Most significant bit

That's really just how little-endian works.

GNU and LLVM tools get this right: aarch64-linux-gnu-objdump -d shows 528c4102, the 32-bit integer interpretation of the 4 bytes. llvm-objdump -d shows 02 41 8c 52, the raw byte sequence. Both of those are equivalent and not misleading.

But https://armconverter.com/ stupidly groups it up into 02418C52 (in its default "GDB" mode). This is bad. If you wanted to manually encode some AArch64 shellcode, you'd use .long 0x528c4102 (on a little-endian assembler targeting e.g. like x86, AArch64, or whatever) to get a representation of MOVZ W2, #0x6208.

By convention, a single string of digits without spaces has place-values that increase from right to left, and represent a single integer value of some width. It's not you, it's https://armconverter.com/ that's the problem.

armconverter has a "GDB/LLDB" toggle that fixes it to 528C4102 in LLDB mode, which it calls "big endian". But it's not a "big endian" byte sequence, there are no spaces so it's the 32-bit integer value. 02418C52 is the integer you'd get if you interpret the 4 bytes as big-endian (opposite of what an AArch64 CPU does), 528C4102 is the correct little-endian interpretation of those 4 bytes.

I think armconverter is using "big endian" to actually mean "byte reverse before removing spaces between bytes". This is braindead misuse of terminology. Again, both GNU binutils and LLVM disassemblers get this right, the problem is purely armconverter.

Following up to prior comments and answers

The sf bit is never at bit 7 it is always at bit 31 for this instruction, the ARM view from the document you posted is the only proper way to view the instruction. Never try to byte swap that view of the instruction. Fix the data or even better use a tool that works rather than a buggy/broken one.

so.s

movz w1,#0x6208

gnu binutils

aarch64-none-elf-as so.s -o so.o
aarch64-none-elf-objdump -d so.o

so.o:     file format elf64-littleaarch64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   528c4101    mov w1, #0x6208  

clang/llvm

clang -c so.s -o so.o
llvm.objdump so.o
    
Disassembly of section .text:

0000000000000000 <$x.0>:
    0: 01 41 8c 52      mov w1, #25096

now this is different than 01418c52, the spacing implies these are bytes not a whole word and then that can indicate there may be some endianness involved. I do not agree necessarily that disassemblers byte swap, they may as in this case show a byte view vs a word or halfword view yes. And then if a halfword view you have to know what order they show in memory/to the processor:

mov.w r10,r11

0:  ea4f 0a0b   mov.w   r10, r11

0xEA4F is the first half of the instruction in this case.

Both clang/llvm and binutils use the same file format as shown so you can disassemble the clang/llvm generated binary with binutils

aarch64-none-elf-objdump -d so.o
Disassembly of section .text:

0000000000000000 <.text>:
    0:  528c4101    mov w1, #0x6208                 // #25096

There are different forms of big endian. As documented for armv8

If I have the 32 bit little endian (default/normal) word 0x11223344 at address 0x1000 then little endian the BYTES view is

0x1000: 0x44
0x1001: 0x33
0x1002: 0x22
0x1000: 0x11

(not 11223344 that is a word view)

for big endian the BYTE view of the same data at the same time is

0x1000: 0x44
0x1001: 0x33
0x1002: 0x22
0x1000: 0x11

Which is the same, known as byte invariant or BE-8. For armv6 and later big endian is BE-8, byte invariant. (ARMv4 and v5 are word invariant BE-32)

A word access though does vary as one would expect:

0x1000: 0x11223344 little endian DATA
0x1000: 0x44332211 big endian DATA
0x1000: 0x11223344 little endian INSTRUCTION fetch
0x1000: 0x11223344 big endian INSTRUCTION fetch

Instruction endianness

In ARMv8-A, A64 instructions have a fixed length of 32 bits and are always little-endian.

The tool you are using is simply broken and if the goal of the tool is to assemble and show you the machine code or vice versa, and it cannot do this simple task (which it clearly cannot), then I would simply avoid the site as a whole. If they cannot do something this simple then they do not understand the instruction set well enough. Their "gdb" big endian switch is not a solution it is just one more thing that is broken.

The ARM documentation is correct and binutils is easy to use. clang/llvm a little harder, I can provide a build script if you want. Although binutils objdump has its issues it is still the best set of tools for this kind of work. You can easily go back and forth from assembly language and machine code.

movz w1,#0x6208
.inst 0x528c4101

aarch64-none-elf-as so.s -o so.o
aarch64-none-elf-objdump -d so.o
so.o:     file format elf64-littleaarch64

Disassembly of section .text:

0000000000000000 <.text>:
   0:   528c4101    mov w1, #0x6208                 // #25096
   4:   528c4101    mov w1, #0x6208                 // #25096

(can as well with clang/llvm)

Disassembly of section .text:

0000000000000000 <$x.0>:
       0: 01 41 8c 52   mov w1, #25096
       4: 01 41 8c 52   mov w1, #25096

You can see from the document segment you posted the instruction starts with x1010010 which can either be 0x52 or 0xD2 the (broken) tool shows 02418C52 which quickly indicates they may have byte swapped the machine code (further investigation is required if you see such a thing as it could be dumb luck) if you had not see a 0x52 nor 0xD2 in the data then it is not the same instruction, or there is some other issue.

If you want to see the machine code for this architecture just use binutils or clang/llvm or some other easy to use, not-broken, tool.

Related