racerxdl / riscv-online-asm

RISC-V Online Assembler using Emscripten, Gnu Binutils
https://riscvasm.lucasteske.dev
39 stars 9 forks source link

Hex dump mangles data endianness #4

Closed jeffcutsinger closed 1 year ago

jeffcutsinger commented 1 year ago

I spent a lot of time scratching my head because I was using this tool to check the output of an assembler I was writing. I have trouble sorting out endianness (I can never figure out which end of the number is being referred to). I was looking at the risc-v spec and wondering if the little-endianness of the instructions was somehow "baked in" to the description. Based on this tool, it seemed to be. I figured the hex dump must be a direct reflection of the binary output by the assembler, but it's not. The binary output is little-endian, which of course makes sense for a little-endian processor. But the hex dump reads in 4 bytes at a time and outputs their big-endian representation. Even if your intent is to output a big-endian representation, this isn't correct; the big-endian representations for 2 2-byte numbers and 1 4-byte number aren't the same.

At this point, I'm not sure what the purpose of the hex dump is. It doesn't add information that isn't already in the disassembly (since the disassembly already outputs big-endian representations of the instructions). It seems to me that the most useful thing would be to output individual bytes in address order, so that the instructions are shown in their native little-endian representation.

Here's an example to show what I mean. With this input:

.global _boot
.text

_boot:
    addi x1 , x0,   1000
    .word 0x3e800093
   # "Same" as above, but bytewise
    .byte 0x3e
    .byte 0x80
    .byte 0x00
    .byte 0x93

You get this hex dump:

3e800093
3e800093
9300803e

and this disassembly:

   0:   3e800093            li  ra,1000
   4:   3e800093            li  ra,1000
   8:   803e                    c.mv    zero,a5
   a:   9300                    0x9300
racerxdl commented 1 year ago

It's the little endian output that goes directly for FPGA bitstream load (for example, the memfiles: https://github.com/racerxdl/riskow/blob/main/gcc/excp.mem)

It just gets the buffer and shows up in a sequence of little endian 32 bit unsigned integers.

Also:

    .word 0x3e800093
   # "Same" as above, but bytewise
    .byte 0x3e
    .byte 0x80
    .byte 0x00
    .byte 0x93

This is not correct. All C compilers nowadays defaults to little endian and you're sequencing it as big endian. The correct would be:

    .word 0x3e800093
   # "Same" as above, but bytewise
    .byte 0x93
    .byte 0x00
    .byte 0x80
    .byte 0x3e

Which will give you the right results:

3e800093
3e800093
3e800093
file.elf:     file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <_boot>:
   0:   3e800093            li  ra,1000
   4:   3e800093            li  ra,1000
   8:   3e800093            li  ra,1000

The output from the compiler is correct. The 0x representation is always big endian in that assembly format (regardless of what the output is), that's why its confusing when you lay down the bytes directly on memory like that.

The disassemble just gets confused because its interpreting as instructions (which the specified byts are only valid for Compressed instructions which endianess is different as you mentioned).

jeffcutsinger commented 1 year ago

Right, I'm not saying that the byte-wise sequence I entered is correct, or that there is specifically a bug in the code that would prevent it from functioning. I should have left the example out.

What I'm saying is that the hex dump displayed is confusing. Could you explain the utility of having a hex dump that works in groups of 4 bytes & displays them in 4-byte big-endian order? What is it providing that is different from the hex display that is already in the disassembly?

samsoniuk commented 1 year ago

Hi Jeff,

Just 2 cents from the HW side of the problem... the hexdump output is really not in the correct endian, but such file is typically loaded on riscv cores that runs on verilog simulations, like in the riscow top.v [1]:

$readmemh("gcc/rom.mem", ROM);

As expected, the instructions are readable and strings are byte swapped, but the root of the problem is that because the memories and buses are typically described as [31:0], which is big-endian (ohh well, all SW people says endian makes no difference, but it makes ALL difference on HW!).

Unfortunately, a fully correct approach may be generate the binary file and load it with $readmemb() on the simulation, but this means that the memories and buses described as [31:0] will not work, so people will need a weird bus swap on every sub-system, something like:

wire instruction_bus[31:0] = { memory_bus[7:0], memory_bus[15:8], memory_bus[23:16], memory_bus[31:24] };

Well, most riscv cores just wire them directly:

wire instruction_bus[31:0] = memory_bus[31:0];

But this is how a big-endian system works, not little-endian... it is more clean for HW people, so they workaround it via hexdump. Well, not the best solution for sure: the best solution is a fully big-endian design, but SW people does not understand why, so we are cursed to this mess for all the future! D:

[1] https://github.com/racerxdl/riskow/blob/main/top.v

jeffcutsinger commented 1 year ago

I just took a look at the new version. The raw objdump would have clarified things for me, so I consider this issue resolved.

jeffcutsinger commented 1 year ago

@racerxdl I'd like to say thank you for making this tool. I did have some confusion with it, but I still find it very useful. And also I just think it's neat.

@samsoniuk I've only dabbled in HW so it's good to see that perspective, thank you for sharing.