41 comments

[ 2.2 ms ] story [ 77.7 ms ] thread
What baffles me a bit is that, even in this day and digital age, there is apparently no employee left from MOS Technology that still has the old dusty manual - for internal use only of course - with all the opcodes documented and explained. Digital archeology is really a thing.
I bet there wasn't one. The undocumented instructions are just unintended side effects of how the hardware works, not a ploy to give some programming teams an advantage via secret knowledge ;)
Yeah, it was my understanding that the opcodes directly encoded what we would nowadays call microcode, that is, bits which selected which CPU component acted on which data[1], so these illegal instructions are just unused bit patterns that select various operations just by having those bits set, not something that was ever intended to be actually used. So it would make sense that they're not documented, since they're entirely incidental.

[1] https://www.youtube.com/watch?v=dXdoim96v5A

There is a story about how BIT sets bit 6 into the overflow flag, and this was documented on a piece of parchment paper in pencil!

http://www.6502.org/tutorials/vflag.html

Either way, every transistor in the processor has been broken down completely, and even recreated in a visual simulation:

http://visual6502.org/

The overflow flag and its intrinsics was quite a mystery for a while.

(Related fun fact: There is no such thing as a break flag, it just gets inserted, whenever the the status register is pushed to the stack by an instruction, i.e. by software, as opposed to a hardware interrupt. This is also, why it is commonly shown as 1, because this is the value it would show up with, if there was a PHP or BRK instruction at the given time, which is also: always. But this is really just a virtual state and there is no such flag in the CPU.)

>(Here, for the “illegal” opcodes, we use the mnemonics used by the DASM and ACME assemblers, with the exception of “USBC” for instruction code $EB, where these use plain “SBC”.)

6502 assembly code is more sleek and beautiful and perfectly justified than most other assembly code, because all the opcode mnemonics are exactly three letters.

In comparison, assembly code in other CPU instruction sets looks so rough and messy, like the code was torn off along the right edge.

Kudos to the authors of that guide for respecting the lovely three letter convention for the illegal opcode mnemonics.

I actually considered inventing a new three-character one for "USBC" (maybe "SNC" for "SBC+NOP"?), but thought it would be better to not to add to the existing confusion regarding the names of these undocumented instructions. (Some of them have a long history, not all of them are perfectly chosen, but they are what they are.) The same applies to "ANC" and "ANC2": is the distinction of these externally similar instructions really worth ruining the simplicity and beauty of the three-letter convention?
I started as a teenager coding 6502 assembly on a Vic20. The three letter opcodes definitely made my code look beautiful.

Z80 assembly a few years later was a real disappointment. No doubt the instruction set was more powerful. But the code always looked ugly to me.

I've never forgotten my love for 6502 assembly. My mobile number ends with the 4 digits '6502'. <3

That's a cool mobile number! Was it intentional?
Yep! I was buying a special mobile number for my partner, and spotted the 6502 one. What makes my number extra special (to me) is it is "06 <year of birth> 6502".
Exactly! Z80 code always looked "rough" to me, like its opcodes generated more friction while sliding through the CPU.

I don't have Obsessive Compulsive Disorder. I just think of it as Obsessive Compulsive Order.

Even though all its opcodes weren't 3 characters, the PowerPC really appeals to me as a clean, powerful, orthogonal, crystalline clear instruction set. I always visualized the PowerPC as Superman's Fortress of Solitude, the ice crystal palace with all the crisp 90 degree angles.

https://en.wikipedia.org/wiki/Fortress_of_Solitude

I started with the C64, and moved to an Amiga, and it felt effortless to move from the 6502 to a 68k. When I years later for the first time sat down with x86 asm it immediately motivated me to stop programming assembler.

Despite being incredibly dissimilar, the relative regularity of the 6502 and 68k felt familiar - a small set of rules and patterns flowed naturally into implementing quite complex code in a way it didn't on the x86. It's remarkable how much those things matters.

There's probably at least one 6502 assembler out there that dissassembled $0A/$4A as "ASLA"/"LSRA" instead of "ASL A"/"LSR A" or just "ASL"/"LSR".
If anybody is wondering why those undocumented instructions exist:

https://www.pagetable.com/?p=39

...it's basically a tolerated side effect of the very simple table-based instruction decoding on the 6502. The main design goal of the 6502 was to create a very cheap yet practical CPU (compared to more complex and more expensive CPUs like the Z80), and one way to achieve this was to simplify the instruction decoding with a simple "switch board" even if this means that there's nonsensical "cross-talk" for some opcode bit patterns.

Here's also a very nice blog post by Ken Shirriff about the instruction decoding differences on ARM1, 6502 and Z80:

https://www.righto.com/2016/02/reverse-engineering-arm1-inst...

That was my first thought when seeing what these opcodes do. Just a result of a random mix of various parts of cpu in action. Thanks for confirming it!
This is also, what this article is actually about. It's just from a more "hermeneutic" perspective, rather than a bottom-up approach based on an analysis of the hardware.
It was my understanding that this sort of combinatorial logic is used by every CPU's control unit - is this outdated? What do modern chips use now?
I don't know :) But for instance on the Z80, even though there's a clear opcode bit pattern (see http://www.z80.info/decoding.htm), any holes in the instruction set that make no sense have been "plugged" with official special-case instructions.

For instance the opcode 0x76 should be "LD (HL),(HL)", but instead this opcode has been reused for the HALT instruction.

The Z80 has undocumented instructions too (for instance in the prefix-ranges for the IX and IY indexed instructions), but those behave the same as documented instructions, except that they cost more cycles. This seems to indicate that the instruction decoding on the Z80 works quite a bit different than on the 6502.

I'm not a semiconductor engineer, but it is my understanding that most CPUs today either use a PROM, which is programmed at the factory the correct version/variant for that part, or (a sometimes compacted?) variant of mask ROM which is produced automatically by some EDA tool(s) from the final production version of the ROM.

Mask ROM usually is just used as a big lookup table which replaces the combinatorial logic by just explicitly matching each input bitstring to the correct bitstring to control the ALU, etc. (along with some sort of state-machine for VLIW ISAs).

Moving to a ROM-style architecture makes development a little more straightforward because modifying instruction decoder only requires burning a different bitstring into a PROM rather than redesigning a combinatorial logic circuit and then manufacturing an entirely new chip.

A ROM-based architecture probably makes microcode updates easier to design into a CPU as well, because you can basically just overlay the microcode over the existing ROM and patch, at runtime, essentially any part of the instruction decode (or whatever else you've designed in the ROM) on an unmodified production IC.

I imagine that the combinatorial logic approach isn't really used anywhere today (for new designs) because of how "cheap" gates are considered to be in a digital IC design, but I could be entirely wrong about this (I'm not in the industry).

I think you're confusing instruction ROMs and instruction decode/sequencing ROMs.

What's being discussed is at the same level as microcode, whereas most of the distinctions you're making apply to the normal code storage ROMs. For instance modern microcode is nearly never a PROM.

Additionally combinatorial logic is generally made out of cheap gates, the two concepts aren't in conflict, but instead orthogonal.

I was specifically referring to the process of instruction decode, but I may be using the incorrect terminology.

My understanding (as explained to me by someone who did work on some CPU-like ASICs) was that they were able to produce the early versions of ICs with some form of "PROM" (possibly not the correct term) which could then be one-time programmed so that they didn't have to pay for a new set of masks if they wanted to change any of their instruction decode (or whatever they had wired into the "PROM") logic.

All they had to do was burn a new version into a blank chip and test/use it from there.

The company in question was producing low-volume, high cost products for a narrow niche and so I don't know if they had to throw away some die space to have this "PROM-like" feature and if that is still how things are done today.

It's not just die space, but much slower timing that's the main problem in most cases. At that point it's a lot better to iterate via hardware emulation.
Oh right. I didn't think about the extra time that signal propagation through a big ugly ROM would take.

I my comment earlier about EDA tools being able to "simplify" the ROM into the die was probably a paraphrasing of what I was told about a tool. The tool (or workflow) allowed a designer to go from a working prototype microcode ROM to a hardcoded gate circuit (presumably by the same sort of process that one can go through to simplify a logic statement that is in disjunctive-normal form).

This was information passed on to me second hand from quite a few years back now so I probably should've tried to look up some primary sources before writing my comment! Oh well.

Normally you want to reserve the "unused" opcodes in order to assign them new semantics (new instructions) later in an updated CPU model. To discourage programs from using those yet unassigned opcodes, CPUs usually trap if they encounter them (i.e. raise an "illegal instruction" interrupt).

The 6502 approach of just executing some random (but deterministic) logic leads to a situation where the "illegal" opcodes cannot be expected to be unused (programs actually use them), and where assigning them new semantics effectively breaks compatibility with existing programs. That’s why CPUs usually invest in the extra logic necessary to give those opcodes undesirable behavior (i.e. traps).

> compared to more complex and more expensive CPUs like the Z80

It preceded the Z80 by a year or two.

Yes, but that wasn't really the point. The 65xx series was a result of Chuck Peddle initially proposing a drastically cost-cut design similar to the 68xx series from Motorola. When Motorola didn't want to do it, Peddle and other Motorola staff left for MOS Technologies to realise the design. So the simplicity of the 65xx had less to do with when it was released than with an explicit design goal to be extremely simple and cheap.
Is there more on this proposal (e.g., a preserved document)?

Something I found especially interesting is the decision to not implement "STX abs,Y" and "STY abs,X", while the decoding matrix would have easily provided for this. Was this to keep the instruction set arbitrarily simple (as in explicitly not orthogonal)? Or was there a more serious architectural conflict? Or was this just a legacy from the 6800, which had just a single index register, thus lacking a similar option?

(I added a note on this to the article.)

Most of what I know about that comes from Brian Bagnall's books (e.g. Commodore: A company on the edge). It's covered in the first chapter. I don't know of any preserved documents - if they exist they'd belong to Motorola, and I'd expect the leavers would have preferred to ditch any such documents (though part of the reason for the settlement with Motorola was that one of the engineers stupidly taking 6800 documentation with him).

This [1] summarizes it. Here [2] is an interview by Computer History Museum of Chuck Peddle. It matches up with the first chapters in Bagnalls book very closely. From about 50 minutes in he talks about his meetings w/Motorola customers that told him the 6800 was too expensive. From about 58m in they talk about basically setting a size metric for the chip to meet a low enough price point. So the starting point was what they could fit in small enough space.

You might also like the Computer History Museum interview w/Bill Mensch [3]

[1] http://www.cpushack.com/2013/08/03/mos-technology-mcs6501-pr...

[2] https://www.youtube.com/watch?v=enHF9lMseP8

[3] https://www.youtube.com/watch?v=Ne1ApyqSvm0

Thanks! I'll have a closer look at these. – Admittedly, I haven't looked deep into Chuck Peddle's history (besides anything related to the PET), yet.
So you found similarities between the instructions, arranged them in a pattern that preserved it and made predictions from there. A bit like what Mendeleev did with natural elements to create the periodic table. Cool work, thanks for sharing!
It's just another (hopefully somewhat systematic) perspective on a known phenomenon, though. (OP here.)

P.S.: What might be of minor interest is that this may help decide, whether it's "ROL A" (as in older assemblers) or just "ROL", etc. (as in some of the more modern ones). We may see that there is a slot for "ROL A" and another one for "ROL implied", which is a NOP. As is the case with all the other accumulator instructions. – However, brevity is still a thing. :-)

I like it as an example of inferential process; here's what we can see the chip do in response to inputs, here's what we think that must mean for its design. With the ability to go look at the hardware and many explanations of it to go look at later and see how and why the observed effects happen.
Does anybody know if the mnemonics can be derived from the opcode? (like lda). I remember I was looking for a table of a dissambler, I couldn't find one, and was always wondering how this is implemented.
Not sure what you mean, but all 6502 disassemblers I've seen use a table to get the mnemonic (and the addressing mode) from the opcode.

Mnemonics are arbitrary, there is no other way.

You can convert from an opcode to an index into a lookup table of mnemonics quite efficiently, and vice versa. You can also pack a single three character mnemonic into two bytes, so the lookup table may be hard to spot. Assemblers and disassemblers running on 6502 used to do this because space was at a premium.
Now I want to golf a 6502 opcode -> mnemonic decoder...

Not that I have any use for one.

These undocumented 6502 opcodes were very useful during the 90s, when developing Game Genie codes for the Nintendo Entertainment System.

GG codes were essentially[1] single-byte ROM patches. For example, the code AAAAA would patch address 0x8000 to 0x00 (I have a web-based decoder (and Node.js module) for those interested [2])

The Galoob Game Genie device only let you enter 3 codes at a time (unless you chained together multiple Game Genies), so minimizing the number of bytes you have to change to make the code work was a worthwhile goal. If your effect requires 3 codes, a typical player will not be able to use any other code. Modifying only one byte is ideal.

How do unofficial opcodes help here? Well, the official opcode list is quite limited. Undocumented/illegal opcodes help in at least two ways:

Multi-byte no-operations: want to remove an instruction completely? You can NOP the first byte, the opcode, but this leaves the operands (if there are any), desynchronizing the instruction stream (sometimes you can get away with this and it produces interesting effects, however). Fine, so you can add one or two more codes NOP'ing out the operands, but then your effect is 2 or 3 codes long!

Undocumented codes to the rescue. DOP and TOP, double-nop and triple-nop, turn this 2 or 3 code effect into only 1 code. Although to be fair, there is another workaround: patch to a legitimate opcode, which has an operand of the same size (same or similar "addressing mode"), but an effect which does not meaningfully impact the program operation. Frequently this was possible, with some effort, but DOP and TOP is nonetheless a cleaner solution.

Another benefit is combining multiple operations into one, also for a more compact code. Many of the undefined opcodes exist, as I understand it, due to "don't cares" in minimizing the instruction decoding logic, hence they combine multiple operations, sometimes usefully so (sometimes not):

ASR = AND + LSR

ANC = AND + ASL

XXA = AND + AND

ARR = AND + ROR

DCP = DEC + CMP

ISC = ISB + INS

LAS = LDA + TSX

LAX = LDA + LDX

LXA = STX + AND

RLA = ROL + AND

RRA = ROR + ADC

SAX = AND + AND

SBX = CMP + DEX

SHA = AND + AND

SHX = AND + AND

SHY = AND + AND

SLO = ASL + ORA

SRE = LSR + EOR

TAS = AND + AND

etc

The multiple-store (Sxx) and load (Lxx) operations most useful for this purpose, from my recollection. I think the most I used was LAX, which would load the operand from memory into both registers A and X, in one opcode. The combined store and arithmetic opcodes in theory could be useful though in practice many were unstable.

[1] Technically, GG codes patch the lower 15 bits of _CPU_ addresses, which can map to multiple ROM addresses depending on the mapper hardware in the cartridge. But this effect is difficult to usefully harness (though it is possible) to patch multiple ROM addresses you want because they have to line up exactly, in practice on these games the coder will use an 8-letter code to restrict the patching to one ROM bank (example: the famous SLXPLOVS = 1123?de:bd, meaning "if CPU address 0x9123 sends data 0xde, replace with 0xbd - replacing the DEC opcode with LDA, disabling decrementing the player life count variable when they die, resulting in the "Infinite Lives" effect on Super Mario Bros. 3. Galoob codemasters didn't use undocumented opcodes in their official GG codes as far as I know, but they were useful in homemade amateur codes.)

[2] https://satoshinm.github.io/nescode/