Hi all,
My LLVM port proceeds apace, and the assembler portion of things is stabilizing. One of the biggest questions I'm dealing with now, is how LLVM decides to use 8 bit vs 16 bit address forms in an instruction. I'm interested in getting feedback from you regarding my current approach. I'm hoping it will cover everyone's use cases now and in the future.
The 65xx series of processors has multiple types of instructions that can be encoded differently, depending on the size of the target address. For example, consider:
Code:
lda hello,x
In the 6502 case, this instruction could be encoded as 0xb5, indicating that hello is an 8-bit (zero page) address. Alternately, it might be encoded as 0xbd, indicating that hello is a 16-bit address.
In order to determine which encoding to use, the assembler must either (1) calculate the final value of the hello symbol, or (2) receive a hint as to which address space that hello should exist in.
LLVM's assembler has a feature called relaxation, in which individual instructions may be replaced with larger instructions, depending on whether the target address can be encoded in the smaller instruction or not. However, the exact encoding of an instruction depends on the value that the symbol resolves to. And the value of that symbol might not be resolved until link time. By the time that the linker is running, the relaxation step of the assembler is well over.
This chicken-and-egg problem has multiple solutions. It might be possible to create different pseudo opcodes, such as lda8 and lda16, that map directly to one specific encoding. But this solution is incompatible with all existing 6502 code. It also might be possible to modify the LLVM linker to rerun the assembly relaxation step once all memory addresses are finalized during linking. The gcc toolchain has some support for this. But as of this writing, this idea is still novel for the LLVM toolchain.
The solutions I've gone with, provide multiple ways to tell the assembler and the linker that you want to put a symbol in zero page.
A symbol will resolve to an 8-bit address, and instruction encoding will occur under that assumption, if at least one of the following are true:
1. the value of the symbol resolves, at assembly time, to an 8-bit non-zero constant; or
2. the symbol is previously defined in a section with one of the following names: .zp, .zeropage, and .directpage; or
3. the symbol is defined in a section marked with the special z flag.
If none of these conditions apply, then the symbol will refer to a 16-bit address, and instruction encoding will take place under that assumption.
So, one way to force a zero page access is fairly straightforward:
Code:
low_addr = 55 + 2 * 4
lda low_addr,x
Just define the address as a constant expression, and the assembler will deduce that a zero-page opcode is required. This is the classic solution, and most 8-bit programmers will be comfortable with it. The downside to this method, is that you have to do all memory management yourself, when ELF and the linker already have the information they need to assign those 8-bit addresses for you.
Another way to force a zero page access, is to tell the assembler your intention, by placing the symbol in one of the specially named zero-page sections:
Code:
.section zp
low_short:
.byte 0x00 0x00
.section text
high_short:
.byte 0x01 0x00
lda low_short,x
A third way to force a zero page access is to mark the section with the special z flag:
Code:
.section .lowmemory,"z",@nobits
adrlowmemory: .ds.b 1
Here, the assembler will understand that the adrlowmemory symbol will eventually be located in zero page. Therefore all subsequent references to it will require one byte.
This solution lets the linker figure out the exact location in zero-page memory where the symbol can go. It's up to the linker script to choose a reasonable zero-page location, on a per-target basis, for symbols marked as described above. Meanwhile, the assembler gets the hint it needs to make the lda instruction reference zero page, not 16-bit memory.
If you know nothing about this feature, then by default all symbols end up in 16-bit memory. This is the safest, but most memory hungry, option. This should work fine for most applications. But people who want to mess with zero page directly, have a choice of weapons in their arsenal to get there.
I should also mention that all this information gets serialized into ELF objects and executables, so that future tools can do other transforms or analysis on 6502 binary code directly.
Comments or thoughts or improvements?