4 ms·
Admittedly, I never wrote an assembler, but the encoding of x86-64 seems pretty convoluted [0] [1], with much of the information smeared across the some bits in
by trealira 2y ago
Admittedly, I never wrote an assembler, but the encoding of x86-64 seems pretty convoluted [0] [1], with much of the information smeared across the some bits in the Mod/RM and SIB bytes and then extended by prefix bytes. It's more complicated than you would assume from having only written assembly in text and then having the assembler encode the instructions.
One of the things that sticks out to me is that on x86-64, operations on 32-bit registers implicitly zero out the upper 32-bits of the corresponding 64-bit register (e.g. "MOV EAX, EBX" also zeroes out the upper 32-bits of RAX), except for the opcode 0x90, which, logically, would be the accumulator encoding of "XCHG EAX, EAX" but is special-cased to do nothing because it's the traditional NOP instruction for x86. So you have to use the other encoding, 87 C0.
The fact that AArch64 has instructions that are almost always 4 bytes, and the fact that they clearly thought out their instruction set more (e.g. instead of having CMP, they just use SUBS (subtract and store condition codes) and store the result in the zero register, which is always zero), is what made me say that it seemed more regular. I hadn't studied it in as close detail as x86-64, though. But, you've written an an ARM disassembler and I haven't; you seem to know more about it than me, and so I believe what you're saying.
> AArch64 also has some register constraints
x86-64 also has some, in that many instructions can't encode the upper 8 bits of a 16-bit register (AH, BH, DH, CH) if a REX prefix is used.
[0]: https://wiki.osdev.org/X86-64_Instruction_Encoding https://wiki.osdev.org/X86-64_Instruction_Encoding
[1]: http://www.c-jump.com/CIS77/CPU/x86/lecture.html http://www.c-jump.com/CIS77/CPU/x86/lecture.html
- WalterBright 2y ago> you've written an an ARM disassembler Here's my AArch64 disassembler work in progress: https://github.com/dlang/dmd/blob/master/compiler/src/dmd/backend/arm/disasmarm.d https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba... I add to it in tandem with writing the code generator. It helps flush out bugs in both by doing this. I.e. generate the instruction, the disassemble it and compare with what I thought it should be. It's quite a bit more complicated than the corresponding x86 disassembler: https://github.com/dlang/dmd/blob/master/compiler/src/dmd/backend/x86/disasm86.d https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba...
- bonzini 2y agox86 has a lot of special cases and it's becoming harder and harder to find non-code material with all instructions in a nice tabular format. But overall the encoding isn't that complicated. What's complex in the architecture is the amount of legacy stuff that still lives in privileged code. > x86-64 also has some, in that many instructions can't encode the upper 8 bits of a 16-bit register (AH, BH, DH, CH) if a REX prefix is used. You can't use both the high registers and the extended low registers in the same instruction, indeed. But in practice nobody is using the high registers anymore so it's a relatively rare limitation.
- jcranmer 2y agoOne of the projects I work on every now and then is the "World's Worst X86 Decoder", where I'm trying to essentially automatically uncover x86 assembly semantics without ever having to build a list of x86 instructions, and this has forced me to look at the x86 ISA in a different way. The basic format I build for an opcode is this: enum Group1Prefix { Lock = 0xf0, Repnz = 0xf2, Repz = 0xf3 } enum Group2Prefix { Cs = 0x2e, Ds = 0x3e, Es = 0x26, Fs = 0x64, Gs = 0x65, Ss = 0x36 } enum Group3Prefix { OpSize = 0x66 } enum Group4Prefix { AddrSize = 0x67 } enum ModernPrefix { Rex { w: bool }, Vex { w: bool, l: bool }, } struct Opcode { pub group1: Option<Group1Prefix>, pub group2: Option<Group2Prefix>, pub group3: Option<Group3Prefix>, pub group4: Option<Group4Prefix>, pub modern_prefix: Option<ModernPrefix>, pub opcode_map: u8, pub opcode: u8, } And all opcodes can optionally have an immediate of 1, 2, 3, 4, or 8 bytes and optionally have a ModR/M byte (which is a separate datastructure because I don't enter that information myself, I simply run a sandsifter-like program to execute every single opcode and work out the answer). This isn't quite accurate to how an assembler would see it, as Intel will sometimes pack instructions with 0 or 1 operands into a ModR/M byte, so that, e.g., 0F.01 eax, [mem] is actually the SGDT instruction and 0F.01 ecx, [mem] is SIDT (and 0F.01 eax, ecx is actually VMCALL). As long as you're internally making a distinction between the different operand forms of instructions (e.g., 8-bit ADD versus 16-bit versus 32-bit versus 64-bit, and register/register versus register/immediate versus register/memory), it's actually not all that difficult to deal with the instruction encoding, or even mapping IR to those instructions, at least until EVEX prefixes enter the picture.
- trealira 2y agoThat's pretty cool; thanks for showing me. I guess x86 instruction encoding isn't as bad as I thought, then.