7

I'm curious if there are any extant examples of commercial games for classic consoles (let's say, pre-32-bit era here) which make uses of self-modifying code. As far as I know, all the systems I've listed are capable of executing from RAM. I know this for a fact, in the case of the Gameboy, and I can't conceive of anything which would limit the others from doing so.

  • This isn't intentional, but there's an exploit in Mario that you can use to execute code in memory which sends you to the end credits: hackaday.com/2015/01/22/… – llama Nov 29 at 19:42
  • 1
    the consoles are able to execute code from RAM, but they usually have not a lot of RAM (I'm talking about the consoles with cartridges), so I think it's VERY rare if someone uses the small RAM used for data to store code just to perform SMC (maybe to gain performance in some situations). On computers on the other hand it happened all the time – Jean-François Fabre Nov 29 at 20:31
  • 1
    "as far as I know, all the systems I've listed are capable of executing from RAM": you have to be more specific when you're saying that Atari is a system. Which one is it? VCS 2600, XL, ST, Jaguar ? – Jean-François Fabre Nov 29 at 20:38
  • I’ll bet there’s at least one example for the Atari Lynx: that machine treats its cartridges strictly as serial secondary store, being able to execute only from its 64kb of RAM. – Tommy Nov 30 at 10:31
  • @llama Yeah, that exploit (or an analogue on Gameboy, I think) was what gave me the idea for this question – Kevin Keith Nov 30 at 12:34
6

I know about one Atari 8bit game called Robbo. There are source codes published where in file R1.ASM you can see:

     LDA LMVT,X
     STA JMP1+1
     LDA LMVT+4,X
     STA JMP1+2
JMP1 JMP *

LMVT DTA L(UP_)
     DTA L(DW_)
     DTA L(LT_)
     DTA L(RT_)

     DTA H(UP_)
     DTA H(DW_)
     DTA H(LT_)
     DTA H(RT_)

LDA gets address from table indexed by X register, STA stores this address in JMP instruction (after OPCODE, so JMP1+1). JMP jumps to different locations depending on value in X register. I think this is quite typical situation for self modifying code - modify only parameters of instruction not whole instruction.

  • 1
    Maybe I'm wrong but Atari 8 bit is a computer (400XL, 800XL) not a console right? Self-modifying code is very common in computer games, to avoid indirect calls/reads for instance. I know a ton of examples on Amiga (68000): Ex: Dune, Red Zone... and Oric (6502) but those aren't consoles. – Jean-François Fabre Nov 29 at 20:24
  • @Jean-FrançoisFabre it is a console too; see the XEGS, which comes from the Commodore C64GS / Amstrad GX4000 school of thought. – Tommy Nov 30 at 10:23
  • 1
    Any reason not to use the RTS trick here? It would save 4 bytes and not take more time. – Wilson Nov 30 at 11:15
  • 1
    @Wilson Or store the address in zero-page RAM and JMP (addr). – fadden Nov 30 at 17:13
  • 1
    @Wilson: Better yet, if one can spare a couple byte of RAM "full time", place jump targets so they're all in the same page, and have the upper byte of a pair hold the MSB of that page address, so only the lower byte will need to be updated. Even if there's only enough room in that page for a JMP, that would still be faster than having to use an extra load/store for the upper page address. – supercat Dec 4 at 21:18
6

RAM and ROM are reachable by the CPU for execution, so most consoles can execute code from RAM.

The consoles using cartridges usually don't have a lot of RAM, so I think it's VERY rare if someone uses the small RAM used for data to store code just to perform self-modifying code.

On computers on the other hand it happened all the time, just because

  1. it's sometimes easier than creating a table
  2. it saves cycles and an indirection.

A CD console like the Amiga CD32 is a console but cannot run the code from CD obviously, so it has to load it into its 2MB RAM.

Same can be said for the Commodore CDTV, an earlier version, less powerful, which inherited a lot of Amiga games too.

Since those 2 consoles inherited a lot of Amiga games with very little adaptation, and some amiga games use self-modifying code, you can say that those consoles can use self-modifying code.

The CD32 console can also run CDTV titles. The fact that the processor is a 680EC20 could cause issues, so when detecting a CDTV title, the CD32 console turns the caches off, to handle CPU-dependent issues such as CPU-busy loops and cache issues (i.e. self-modifying code).

It's difficult to find an example of a real game that used self-modifying code on an official CD32 game but I found one: Zool CD32

Disassembling the executable, we found several occurrences of this instruction in the code, (setting a label plus 2 is fishy enough and often a sign of self-modifying code):

MOVE.L  #LAB_0411,LAB_0404+2    ;00cefe: 23fc0000b2fa0000abd6

now at LAB_0404 we find:

LAB_0404:
    JSR LAB_0868        ;00abd4: 4eb90001538c
    CLR.L   LAB_002F+2      ;00abda: 42b9000004ea

So the code changes the jump address depending on the situation (without flushing the caches afterwards). The 68020 instruction cache isn't big enough for this to be a (visible) issue.

  • The consoles using cartridges usually don't not a lot of RAM, so I think it's VERY rare if someone uses the small RAM [...] to perform self-modifying code. Doesn't have to be VERY rare. In many cases, just a very small routine that can be speeded up significantly with self-modifying code could be used. And since the RAm is here anyway - even when the game doesn't need all of it for it's internal state, it might as well be used for other uses. – Bregalad Dec 6 at 14:40
3

I rarely had occasion to use self-modifying code. Doing that had a lot of overhead both in ROM space and RAM space, and I rarelysaw an occasion where it would be worth it. I did do it on the IBM PC, where I had bunches of block moves to do that were not contiguous, and doing so eliminated looping overhead during a critical time, at the expense of some RAM.

I did, however, use a variant for copy protection. We ROM programmers were always in an arms race with people who cracked games. Usually, copy protection was bolted on after-the-fact by adding code that had no purpose but copy protection... typically a loop that wrote garbage into the ROM area (having no effect on ROM but destroying a RAM copy). It was easy enough for the crackers to disassemble the code and unbolt them.

On the other hand, games have many tables of gameplay parameters. Some were binary: The robots do not shoot on level 1, but do on level 3. If there were 30 levels, that was 30 bytes per parameter unless you wanted to do a binary unpack. ROM space was tight.

So I would store 3 binary tables in the same byte array: in bits 0, 6 and 7, and other values in bits 1-5. To get to bit 0, I did

 ROR ($TABLE_REF),Y

And the value would pop into the carry flag. (Y has the level number, loaded earlier. table_ref is a zero page location preloaded with the table location). For bits 6 and 7, I would ASL - bit 7 drops into carry and bit 6 drops into sign.

This operation writes the shifted value back to RAM. That has no effect on ROM. But in a RAM copy, it corrupts the gameplay table. Playability is ruined: The robots would never shoot.

By using only a 2-byte instruction, the cracker couldn't just replace it with a 3-byte JSR to a replacement subroutine. It required rewriting that section of the code.

2

(This is slightly off-topic, but related.)

The 65816 CPU has a pair of block move instructions, MVN and MVP, which allow you to copy up to 64KiB from one 24-bit address to another. The instructions work like this:

ldx #{16-bit source address}
ldy #{16-bit destination address}
lda #{16-bit count - 1}
mvn src_bank, dest_bank

(The "bank" is the high 8 bits of the 24-bit address.)

The tricky part is the instruction itself, because part of the address is embedded in it. If you happen to know the bank addresses at assembly time (e.g. you know source and destination are both in bank 0) you're fine, but in the general case you're required to use self-modifying code.

If you want to use these instructions from ROM code, you have to set up a move instruction somewhere in RAM and call it. If your code is reentrant (due to interrupts) you can't simply use a fixed location. IIRC the Apple IIgs ROM pushed the instruction onto the stack and called it there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.