In the era of C64, Apple][ GS, and SNES, did the games use hardcoded memory locations, or did they let the assembler help them (like modern assemblers)? If yes, how did they manage the memory?
On the C64, even hobbyist assemblers like HypraAss supported symbols, so no, I don't think anyone used hardcoded addresses if there was a way to use a symbol instead.
But you had to be careful how you arrange your data in memory. For example, on the C64, it wasn't possible to put video or sprite data into RAM locations 0x1000..0x1fff and 0x9000..0x9fff because the VIC chip had a fixed mapping to read the character generator ROM when using these addresses. So programmers preferred these locations for program code.
There had been other restrictions on the locations of video data, too. For example, the VIC could only address 16k of memory and the C64 had a bank-switching mechanism to make it access all the memory. But it had another bank-switching mechanism for the CPU, so memory addressing was quite a hassle, especially in the 0xd000..0xdfff area, where the I/O ports and the color RAM were mapped along the main RAM. So, these areas had to be spared from program code mostly.
I general, you needed a lot of knowledge about the address layout of the machine, and let the assembler only help you to move code snippets around as needed without having to change all addresses by hand. It was very common to have a lot of .origin statements in one's code.
The 6502 (the processor in many of the computers mentioned) had a special addressing mode for the 'zero page' (see Wikipedia article by the same name). The zero page was faster to access and use. A three cycle
LDA $0000 could instead be written as a two cycle
LDA $00 accessing the same location in memory. Thus, the zero page was a location where significant amounts of convention was stored. One could possibly work with an assembler that had labels and go through and label each of those 256 memory locations, but it was safer to work with them as hard coded values to make sure you were using the
LDA $00 form of the instruction which translated to
A5 00 rather than
AD 00 00.
The zero page map for the Apple ][+ can be seen at Jon Relay's Apple II Info Archives - Zero Page Addresses. If you wanted to get the horizontal location of the cursor, this was stored at
$24. Thus, hard coded addresses were certainly used for accessing locations of things on the zero page.
Furthermore, page 1 on the 6502 was the stack.
$01FF was the stack - no way around that. The instructions
PLP were working on the stack. Along with
JSR for a subroutine which would push the return address on to the stack.
Hard coded locations in ROM were especially useful if there was a card necessary. The code on the card didn't know what slot it was loaded into (each of the slots had a different memory range), just that it was invoked. So jumping to a known
RTS memory instruction in ROM would push the return address onto the stack, and then pop it and return. Then an examination of the stack pointer register and accessing one byte (little endian) just past the top of the stack would tell you the high byte of the memory you came from - and that would tell you what slot your code ran in.
Memory management on the Apple ][+ within Applesoft, was largely handled by the zero page location
$6F which had the top of free memory and the ROM routine at
$E452 (GETSPACE) which would do essentially a malloc of the accumulator bytes and store the location of that memory in the X and Y registers. There was also a garbage collection routine at
$E484 and a memory reorganization routine at
$E5E2. And while there were symbolic names for these routines, and a sufficiently advanced assembler could use them (not the one at
$F666), they were well known and unchanging addresses.
While a large download, the pdf Inside the Apple //e has a significant amount of information about the structure of the processor and programs that ran.
More than hardcoded addresses, I'd say hardcoded memory regions. For example, in the ZX Spectrum series, a developer should avoid the memory region between
$7FFF if he wants to put time-sensitive code or data, as this region is under contention between the CPU and the ULA.
OTOH, there are routines that benefits from putting data on certain boundaries, such as LUTs. A 256 entry LUT can provide an output in only a few machine cycles if the LUT base address is on a 256 byte boundary. That is, if its base address is in the form
$XX00. That way, the translation (Z80 code) is as fast as:
LD H,$XX ;high byte of base address LD L,A ;A is the value we want to translate from LD A,(HL) ;A gets the value translated from the LUT
IM 2 vector tables (for the Z80 CPU also) are also subject of address hardcoding. There are machines which may put a random value in the data bus during an INTA bus cycle, so the called vector is unknown. In these cases, the complete IM 2 vector table must be filled with the same value
$XY so the address of the IM 2 routine will be
It really depended on which developer was creating the games. Atari had a very sophisticated system that their developers used and they wrote proper assembler and compiled it on mainframes. Independent developers often did not have such resources available to them and would hack away in a monitor, which is typically how hard-coded addresses wind up in games.
For the 6502 CPU, writing a machine code program to be relocatable and still work when loaded at different memory addresses is difficult because dynamic branching can't go up or down more than 128 bytes from the current program pointer position (BNE, BEQ, BMI, BPL).
It is possible to do an indirect jump using a lookup table JMP ($4000) but calls to subroutines are fixed JSR $300
Making a program relocatable is possible, but requires keeping track of all the direct JMPs and JSRs, and doing a one-time initialization that figures out where the program was loaded into memory, and then go through the program code editing all the JMPs and JSRs with the offset from the default loading address, so they will work in the new location.
Beyond this, many program's read and write locations would also have to be tracked and manually adjusted, as it was common for programs to be self-modifying.
Rather than using a pointer or zero page location to read or write data, the program would directly modify its own code. A typical simple memory copier that modifies itself:
300: A9 04 LDA #$04 ; Source page $#04 302: 8D 0E 03 STA $030E ; Modify read address 305: A9 08 LDA #$08 ; Destination page $#08 307: 8D 11 03 STA $0311 ; Modify write address 30A: A2 00 LDX #$00 ; Init X register at 0 30C: BD 00 04 LDA $0400,X ; Read byte from page 30F: 9D 00 08 STA $0800,X ; Write byte to page 312: E8 INX ; increment X register 313: E0 00 CPX #$00 ; Check if it rolled over 315: D0 F5 BNE $030C ; No? Do it some more 317: EE 0E 03 INC $030E ; Add 1 to page number 31A: EE 11 03 INC $0311 ; Add 1 to page number 31D: AD 0E 03 LDA $030E ; Get source page number 320: C9 08 CMP #$08 ; Is it at the ending value? 322: D0 E8 BNE $030C ; No? Do it some more 324: 60 RTS ; Exit
Although there are no JMP or JSR here, the code modifies locations $30E and $311, and to be relocatable it is necessary to change the code so it modifies itself correctly, for the opcodes at $302, $307, $317, $31A, and $31D.
So relocatable code can be done but it is fiddly, and nothing in the hardware enables this directly, in a manner that the programmer doesn't have to directly and intimately be involved with making it work.
The Apple IIgs running GS/OS did indeed support a more modern memory management model.
Applications (.S16, .EXE, CDA, NDA, etc.) were stored in Object Module Format. All of the major Apple IIgs development tools supported the creation of OMF files. OMF files included a relocation information which Loaders could use to relocate the object code into a specific memory location.
On Elektronika BK, using hardwired memory locations in ROM to access useful subroutines was quite popular among game developers.
We certainly always used labels. There was interesting things done to reuse start up code as data. We were not above stealing buffers and things from the supposed OS but they still got labels.
People also put variables into code which nobody seems to even know about any more.
If you take Z80 then it's faster to do
LD HL, (var)
So you can save two bytes and some clocks by making the fastest load of the variable a constant load and we'd do this
_var: LD HL,constant var equ _var + 1
and everywhere else use (var)
All code I did on the 8bits started at the beginning of usable RAM (after OS data, etc); but the tools let us select where each block of code or data starts, so it was up to the user, knowing that some zones had OS uses and some may be mirrored on machines with less ram (16kb vs 64kb for example).
Honestly I suspect most folks used an assembler with labels (rather than a monitor type program). It's possible some Apple ][ folks wrote stuff using the mini assembler but even I as a penny less school kid wrote a wrapper around it that added labels and org statements. I suspect a lot of the early Z80 developers were cross compiling from CP/M to target systems.
However I wrote a load of games in assembler without ever using a linker and memory management of locations was pretty manual using .org statements. I stopped using my home brew assembler once I got the ProDOS development kit.
At least several answers (and comments) seem to interpret the question with an implied idea that one can have an assembler that does not resolve symbolic addresses. I think that this interpretation is misguided. In theory, yes, a program like this can be developed (there are some examples of such programs in the comments). However, in practice, starting with the very first real assembler written for IBM 701, the whole point of having the assembler was not so much resolving the mneumonics into specific binary commands, but the resolution of symbolic addresses (see Rochester, N. (1953) Symbolic programming, Transactions of the IRE Professional Group on Electronic Computers, (1), 10-15).
Just to make my point even clearer, this is the abstract of the cited N. Rochester's paper:
Automatic calculators can be programmed to interpret programs which have been written with symbolic instead of actual addresses. This method allows the calculator to assume much of the clerical burden which must otherwise be borne by the programmer.
At the same time, when programming micros, especially when developing natively, it was often impossible to pass the whole burden of memory management to the assembler. For example, in my youth I developed a simple Battle City clone for ZX Spectrum using a ZX Spectrum 48K with a tape drive. For writing my code I used Zeus, a native assembler, which of course supported symbolic labels. However, having the assembler in memory meant that it was not possible to have my whole game in memory at the same time. Therefore, I did my development in batches: a group of routines would be developed and debugged using assembly, then built into a binary and saved onto the tape. I had a special notebook with addresses of key routines needed externally, so if I needed to refer to any of these routines later, I could call them directly by their address. Thus, low- to mid-level building and linking was done in assembler. However, all higher-level linking was done effectively by hand. I am guessing that a lot of homebrew developers at the time must have used similar approaches.
Many Amiga games used hard coded addresses, for example to detect if extra memory had been installed by looking for it at specific locations.
The introduction of floppy disks for storage also made multi-loading more practical. Most tape games tried to avoid it because of very slow load times, but with floppy disks it was common to load between levels.
Using fixed addresses had two benefits for multi-load games. First it meant that any code loaded did not need to be relocatable. Jumps could be absolute, and there was no need for jump tables of trampolines or complex communication between modules. Many programmers were used to doing things that way on 8 bit systems.
The other benefit was preventing memory management issues like fragmentation. Repeatedly allocating and freeing chunks of memory can cause memory allocation to fail eventually.
Finally, one other big benefit of using fixed addresses is that it makes debugging easier. Popular debuggers like the Action Replay allowed the programmer to inspect memory, so knowing exactly where things are located really helps. Of course the compiler will produce a map file but for speed and ease of use many people preferred not to have to look up addresses every time.