Note: Much of the information in this blog post comes from Oliver Schmidt. I’m simply passing along my experiences with this topic in hopes of documenting it thoroughly for other people to enjoy.
CC65, the cross-compiler system for 6502/65816 computers, is very powerful. One of the most powerful features is the ld65 linker which allows you to create executables that are extremely flexible. One use case allowed by the linker is the ability to create overlays, or code fragments that reside at the same memory location and are loaded into RAM dynamically as needed. This is an extremely useful feature for writing large applications that will not fit into the typically tight memory configurations of 6502-based computers.
Creating an application with overlays does require some planning ahead of time. You must make some decisions about how much RAM to allocate to the main program and how much to allocate to the overlays. The results of these decision is documented in the linker configuration file. You must define some SYMBOLS that the linker will use as variable. The SYMBOL that most affects us is __OVERLAYSIZE__. This SYMBOL specifies the size of the overlay (note your overlay does not have to consume all of this memory, but it cannot consume more than this amount of memory). The __OVERLAYSIZE__ SYMBOL is used in the MEMORY definitions to allocate the memory of the overlay. Overlay memory should be defined by starting at the top of RAM available for cc65 and work down. In the case of the C65, we define the top of RAM to be $D000 (you can make it higher by banking out I/O and Kernal ROM, but that’s beyond the scope of this article). So the start of RAM for the overlays is $D000 – __OVERLAYSIZE__. The linker config file also specifies the name of the overlay file to be written. We will use a naming convention where the name of the main executable is used, followed by an index for the overlay. Your SYMBOLS and MEMORY sections should look like this:
Code Snippet
- SYMBOLS {
- __LOADADDR__: type = import;
- __EXEHDR__: type = import;
- __STACKSIZE__: type = weak, value = $0800; # 2k stack
- __OVERLAYSIZE__: type = weak, value = $1000;
- }
- MEMORY {
- ZP: file = "", define = yes, start = $0002, size = $001A;
- LOADADDR: file = %O, start = $07FF, size = $0002;
- HEADER: file = %O, start = $0801, size = $000C;
- RAM: file = %O, define = yes, start = $080D, size = $C7F3 - __STACKSIZE__ - __OVERLAYSIZE__;
- OVL1: file = "%O.1", start = $D000 - __OVERLAYSIZE__, size = __OVERLAYSIZE__;
- OVL2: file = "%O.2", start = $D000 - __OVERLAYSIZE__, size = __OVERLAYSIZE__;
- }
Note also the RAM memory definition. This takes the normal amount of RAM allocated for our program, which is $C7F3 bytes, and reduces it by the size of the overlay. This is where our main program and variables go.
In addition to defining the MEMORY section of the configuration file, we must now use those MEMORY definitions to declare memory SEGMENTS which are used to link together the application. These segments are used by the linker to determine things such as the beginning RAM for each block of 6502 assembly code that is generated by the compiler or specified by the programmer. Here is the SEGMENTS section of our configuration file:
Code Snippet
- SEGMENTS {
- LOADADDR: load = LOADADDR, type = ro;
- EXEHDR: load = HEADER, type = ro;
- STARTUP: load = RAM, type = ro;
- LOWCODE: load = RAM, type = ro, optional = yes;
- INIT: load = RAM, type = ro, define = yes, optional = yes;
- CODE: load = RAM, type = ro;
- OVL1CODE: load = OVL1, type = ro, define = yes;
- OVL2CODE: load = OVL2, type = ro, define = yes;
- RODATA: load = RAM, type = ro;
- DATA: load = RAM, type = rw;
- ZPSAVE: load = RAM, type = bss;
- BSS: load = RAM, type = bss, define = yes;
- ZEROPAGE: load = ZP, type = zp;
- }
Of note to our overlay use case are the OVL1CODE and OVL2CODE segments. What these SEGMENTS do is define memory allocations for TWO overlays. If you will need more overlays you will need to define more SEGMENTS. You should also name these segments such that they are easy to identify which will make your code easier to read when you generate the overlay specific code in your C or assembler sources.
The rest of the linker configuration file is not pertinent to this discussion. I will post an archive in a later article that includes the complete linker configuration file, but this should be enough to get you started.
Now that we have our linker configuration done, we can turn our attention to the code necessary to implement overlays. There are four topics that need our attention:
- Specifying the code that is included in an overlay.
- Specifying the literals that are included in an overlay.
- Linker defined macros that specify values for the load address and size of our individual overlays.
- Loading the overlay to RAM.
Specifying the code in an overlay
To create overlays, the linker needs to know what compiled code modules need to be located in the overlay memory segment. This is done through the use of the code-name pragma (please read the link for a full description of this pragma). In our example, that pragma points to either OVL1CODE or OVL2CODE.
Specifying the literals in an overlay
One side effect of creating overlays and using the code-name pragma is that your string literals define in the overlay code do not reside in the overlay by default, but rather will go into the main string table of the application. If your application is tight on RAM this is not the optimal use of that RAM and you will want your string literals for the overlay to reside in the overlay. To accomplish this we use the rodata-name pragma (please see the link for a full description of the pragma) to define where in memory these string literals are placed.
Although not strictly necessary, I highly recommend putting the code for each Overlay into a separate source code file. This is just a good idea from a code management perspective and allows you to not use the compiler stack to define the scope of the pragmas. Here is a sample overlay:
Code Snippet
- #include <stdlib.h>
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #pragma code-name ( "OVL2CODE");
- #pragma rodata-name ( "OVL2CODE");
-
- int overlay2()
- {
- printf("This is overlay 2.\n");
- return 1;
- }
Getting the size and location of an overlay
Overlays are incredibly easy to use, but they are not magical. Your main program still has to have some knowledge of the size and location of an overlay if it is to load the overlay into RAM. To facilitate this, the linker can populate a pair of macros that provide this information to you code. These macros are used like constants in your application and follow a naming convention so you know what to put in your code. That naming convention is _{MEMORY SEGMENT}_LOAD__ for the load address of the overlay and _{MEMORY SEGMENT}_SIZE__ which is used to get the compiled size of the overlay (not the value of the __OVERLAYSIZE__ symbol). Note that these constants are pointers and must be treated as such. Here is an example of using these macros:
Code Snippet
- extern void _OVL1CODE_LOAD__, _OVL1CODE_SIZE__;
- extern void _OVL2CODE_LOAD__, _OVL2CODE_SIZE__;
Loading the overlay to RAM
Finally, we must load our overlay to RAM. This uses the constants defined above to provide the information necessary to load the overlay. Loading a binary file to RAM is pretty simple, but one thing you need to know is that the overlay files do NOT include a load address, so you may not use cbm_load, which is something I learned the hard way. :)
Here is a loader that takes the information about the overlays and loads them on demand.
Code Snippet
- bool loadOverlay(char *overlayName, void *startAddress, void *size)
- {
- static char filename[21];
-
- unsigned int r = 0;
-
- sprintf(filename, "%s,p,r", overlayName);
- r = cbm_open(2, _curunit, 2, filename);
-
- if(r == 0)
- {
- r = cbm_read(2, startAddress, (unsigned)size);
-
- cbm_close(2);
-
- if(r == (unsigned)size)
- {
- sprintf(messageBuffer, "Read %s (%u bytes) from %d.", overlayName, r, _curunit);
- log(messageBuffer);
- }
- else
- {
- sprintf(messageBuffer, "Error on %d reading %s, %d", _curunit, overlayName, _oserror);
- log(messageBuffer);
- }
-
- return r > 0;
- }
- else
- {
- sprintf(messageBuffer, "Error on %d opening %s, %d", _curunit, overlayName, _oserror);
- log(messageBuffer);
- }
-
- return false;
- }
**please note that log() is a helper method defined elsewhere in my framework that I’ll be posting later.
This method is called in the following manner:
Code Snippet
- if(loadOverlay("overlaytest.c64.2", &_OVL2CODE_LOAD__, &_OVL2CODE_SIZE__))
- {
- overlay2();
- }
**Your code should always test that the overlay was successfully loaded before attempting to call the methods of that overlay!
And that’s it! You are now ready to use overlays in your own application! Feel free to post questions or comments below!
PS. The configuration and other elements of this article are based on the newest nightly snapshot of CC65 at the time of the writing of this article. If you are using older or newer versions of CC65 you may have to adjust your linker configuration file as appropriate.