Dirty Assembly Tricks in NES Assembly (2013)
andrewkelley.me
andrewkelley.me
There are some interesting, digestible bits in the serial interrupt handler[1]
[0]: https://github.com/benogle/obd0vtec_dev/blob/master/src/stoc... [1]: https://github.com/benogle/obd0vtec_dev/blob/master/src/stoc...
The graphics chip has two "missile" objects that act like simple sprites that are only one bit wide. They are each enabled or disabled by writing 1 or 0 to a certain bit of a certain hardware register of the graphics chip.
That register is laid out in hardware with that certain bit at bit 1. Why there, why not the low or high bit? Because that corresponds to the location of the zero flag in the flags register of the 6502 CPU.
So the missile objects can be quickly enabled or disabled by setting the stack pointer to the address of that register, doing a compare on the object coordinates, and pushing the flags register. Both missiles can be done in sequence because the registers are adjacent and the push instruction decrements the stack pointer to set up for the next one.
All to save two instructions on rendering the missiles each scanline. With a nice bonus that the code is time-invariant without branching.
- The parallel port chip has direction bits that control whether a line is an input or an output. This register is read/write, so you can use it as temporary memory when you don't care about reading or writing I/O (which is most of the time).
- Lots of code doesn't need a stack; it doesn't call anything, and it can just jump to where it needs to be at the end. Now we can use the S register for something else.
Many, many other tricks. I used some of them a few years ago on an embedded system that needed to fit into 1920 bytes. I enlisted a cow-orker into the effort and we were both cackling away. [Later, the hardware guys offered us a different chip with twice the memory, but at that point we were too close to shipping to make any changes, and besides, it wouldn't have been as fun... :-) ]
The key word, though, is 'potential' - as the author mentions, one of the issues you run into with disassembly is that you sometimes run into code that jumps into garbage (dynamic jumps with insufficiently constrained inputs, or code that should've been unreachable), and sometimes you just hit an infinite loop waiting for an interrupt. It really needs some input/interactivity to determine likely entry points and dead ends/unreachable code.
One nice thing you can do while you have the call graph, though, is propagate constraints (basically type inference/data flow analysis) to get an idea of where dynamic jumps may go, which ones might be under-constrained, and which branch targets are likely dead ends. Haven't gotten as far as implementing it yet, but there's a paper on it here: http://www.cs.rhul.ac.uk/home/kinder/papers/phdthesis.pdf
And this isn't just for developers; this bijection between source and binary enables things like Dropbox hooking the OSX Finder without the Finder explicitly supporting a plug-in system, or Windows Error Reporting sending you reports when your shipped app crashes in production on some client's system.
And either way, I'd much rather my code had proven-safe JIT tricks done to it by the runtime system it's loaded on, than that those tricks be burned into it by the compiler (where they will then become gradually more outdated.) Or, worse yet, that I burn them in there myself by hand-editing the resulting binary, and then have to do the same thing every time I recompile.
I'll bet that many high-end routers pay very close attention to instruction clocks on their main datapaths.
Optimization is not dead. Pointless optimization should be dead, but isn't.
Love that story.
Reminds me of the not so extreme but still considered alternatively beautiful and horrendous Duff's Device in C, where one entangles a switch/case in a while loop:
I vaguely remember it being discussed in an obscure demoscene magazine article (20+ years ago) about size-optimisation techniques, where it was called a "bridge". I also had some experience working with a disassembler/decompiler that could detect such tricks and output a multicolumn disassembly.
The amount of skill and their results demonstrated by many groups in the demoscene continues to amaze and inspire me, particularly those on platforms like the NES and C64 - a tiny system orders of magnitude smaller and slower than PCs today, yet these people can make them do things that most people would think require far more powerful hardware.
There was also this trick of using the last byte of RAM as the entry to the interrupt handler, so as to use the "free" array of 0xFF that sat in ROM as the handler table. The last byte was a relative jump because luckily the first byte of ROM was negative in U2, so the handler jumped back into RAM after wrapping around into ROM.
Bam, you're in the game.
Thanks for sharing. I will have to read this again carefully after I learn some Go myself.