SourceGen: Advanced Topics

Back to index

Platform Symbol Files (.sym65)

Platform symbol files contain lists of symbols, each of which has a label and a value. SourceGen comes with a collection of symbols for popular systems, but you can create your own. This can be handy if a few different projects are coded against a common library.

If two symbols have the same value, the older symbol is replaced by the newer one. This is why the order in which symbol files are loaded matters.

Platform symbol files consist of comments, commands, and symbols. Blank lines, and lines that begin with a semicolon (';'), are ignored. Lines that begin with an asterisk ('*') are commands. Three are currently defined:

Tags can be used by extension scripts to identify a subset of symbols. The symbols are still part of the global set; the tag just provides a way to extract a subset. Tags should be comprised of non-whitespace ASCII characters. Tags are global, so use a long, descriptive string. If *TAG is not followed by a string, the symbols that follow are treated as untagged.

All other lines are symbols, which have the form:

  LABEL {=|@|<|>} VALUE [WIDTH] [;COMMENT]

The LABEL must be at least two characters long, begin with a letter or underscore, and consist entirely of alphanumeric ASCII characters (A-Z, a-z, 0-9) and the underscore ('_'). (This is the same format required for line labels in SourceGen.)

The next token can be one of:

If an instruction references an address, and that address is outside the bounds of the file, the list of address symbols (i.e. everything that's not a constant) will be scanned for a match. If found, the symbol is applied automatically. You normally want to use '@', but can use '<' and '>' for memory-mapped I/O locations that have different behavior depending on whether they are read or written.

The VALUE is a number in decimal, hexadecimal (with a leading '$'), or binary (with a leading '%'). The numeric base will be recorded and used when formatting the symbol in generated output, so use whichever form is most appropriate. Values are unsigned 24-bit numbers. The special value "erase" may be used for an address to erase a symbol defined in an earlier platform file.

The WIDTH is optional, and ignored for constants. It must be a decimal or hexadecimal value between 1 and 65536, inclusive. If omitted, the default width is 1.

The COMMENT is optional. If present, it will be saved and used as the end-of-line comment on the .EQ directive if the symbol is used.

Using MULTI_MASK

The multi-address mask is used for systems like the Atari 2600, where RAM, ROM, and I/O registers appear at multiple addresses. The hardware looks for certain address lines to be set or clear, and if the pattern matches, another set of bits is examined to determine which register or RAM address is being accessed.

This is expressed in symbol files with the MULTI_MASK statement. Address symbol declarations that follow have the mask set applied. Symbols whose addresses don't fit the pattern cause a warning and will be ignored. Constants are not affected.

The mask set is best explained with an example. Suppose the address pattern for a set of registers is ???0 ??1? 1??x xxxx (where '?' can be any value, 0/1 must be that value, and 'x' means the bit is used to determine the register). So any address between $0280-029F matches, as does $23C0-23DF, but $0480 and $1280 don't. The register number is found in the low five bits.

The corresponding MULTI_MASK line, with values specifed in binary, would be:

  *MULTI_MASK %0001001010000000 %0000001010000000 %0000000000011111

The values are CompareMask, CompareValue, and AddressMask. To determine if an address is in the register set, we check to see if (address & CompareMask) == CompareValue. If so, we can extract the register number with (address & AddressMask).

We don't want to have a huge collection of equates at the top of the generated source file, so whatever value is used in the symbol declaration is considered the "canonical" value. All other matching values are output with an offset.

All mask values must fall between 0 and $00FFFFFF. The set bits in CompareMask and AddressMask must not overlap, and CompareValue must not have any bits set that aren't also set in CompareMask.

If an address can be mapped to a masked value and an unmasked value, the unmasked value takes precedence for exact matches. In the example above, if you declare REG1 @ $0281 outside the MULTI_MASK declaration, the disassembler will use REG1 for all operands that reference $0281. If other code accesses the same register as $23C1, the symbol established for the masked value will be used instead.

If there are multiple masked values for a given address, the precedence is undefined.

To disable the MULTI_MASK and resume normal declarations, write the tag without arguments:

  *MULTI_MASK

Creating a Project-Specific Symbol File

To create a platform symbol file for your project, just create a new text file, named with a ".sym65" extension. (If your text editor of choice doesn't like that, you can put a ".txt" on the end while you're editing.) Make sure you create it in the same directory where your project file (the file that ends with ".dis65") lives. Add a *SYNOPSIS, then add the desired symbols.

Finally, add it to your project. Select Edit > Project Properties, switch to the Symbol Files tab, click Add Symbol Files from Project, and select your symbol file. It should appear in the list with a "PROJ:" prefix.

If an example helps, the A2-Amper-fdraw project in the Examples directory has a project-local symbol file, called "fdraw-exports". (fdraw-exports is a list of exported symbols from the fdraw library, for which Amper-fdraw provides an Applesoft BASIC interface.)

NOTE: in the current version of SourceGen, changes to .sym65 files are not detected automatically. Use File > Reload External Files to import the changes.

Extension Scripts

Extension scripts, also called "plugins", are C# programs with access to the full .NET Standard 2.0 APIs. They're compiled at run time by SourceGen and executed in a sandbox with security restrictions.

The current interfaces can be used to generate visualizations, to identify inline data that follows JSR, JSL, or BRK instructions, and to format operands. The latter can be used to format code and data, e.g. replacing immediate load operands with symbolic constants.

Scripts may be loaded from the RuntimeData directory, or from the directory where the project file lives. Attempts to load them from other locations will fail.

A project may load multiple scripts. The order in which functions are invoked is not defined.

Built-In Scripts

A number of scripts are distributed with SourceGen, and may be used freely by projects. Most are tailored for a specific platform, e.g. Apple II ProDOS calls or Atari 2600 graphics.

The StdInline.cs script in the RuntimeData/Common directory has some general-purpose inline data formatting functions. To use them, add the script to the project, then add an appropriate label to the subroutine that handles the inline data. For example, suppose the code looks like this:

$1000  START        JSR     L1234
$1003               .STR    "hello, world!"
$1010               .DD1    $00
$1011               .DD1    $a9
$1012               .DD1    $55
[...]
$1234  L1234        PLA
[...]

The code won't analyze correctly because it will try to follow the code into the string data. If you include the script, and set the label at L1234 to InAZ_PrintString, the code will then format correctly:

$1000  START        JSR     InAZ_PrintString
$1003               .ZSTR   "hello, world!"
$1011               LDA     #$55
[...]
$1234  InAZ_PrintString PLA
[...]

The label prefixes currently defined by the script are:

Anything more complicated will require a custom script.

Script Development

SourceGen defines an interface that plugins must implement, and an interface that plugins can use to interact with SourceGen. See Interfaces.cs in the PluginCommon directory.

The easiest way to develop extension scripts is inside the 6502bench solution in Visual Studio. This way you have the interfaces available for IntelliSense completion, and get all the usual syntax and compile checking in the editor. (This is why there's a RuntimeData project for Visual Studio.)

If you have the solution configured for debug builds, SourceGen will pass IncludeDebugInformation=true to the script compiler. This causes a .PDB file to be created. While this can help with debugging, it can sometimes get in the way: if you edit the script source code and reload the project without restarting the app, SourceGen will recompile the script, but the old .PDB file will still be open by VisualStudio and you'll see some failure messages. Exiting and restarting SourceGen will allow regeneration of the PDB files.

Some commonly useful functions are defined in the PluginCommon.Util class, which is available to plugins. These call into the CommonUtil library, which is shared with SourceGen. While plugins could technically use CommonUtil directly, they should avoid doing so. The APIs there are not guaranteed to be stable, so plugins that rely on them may break in a subsequent release of SourceGen.

Known Issues and Limitations

Scripts are currently limited to C# version 5, because the compiler built into .NET only handles that. C# 6 and later require installing an additional package ("Roslyn"), so SourceGen does not support this.

When a project is opened, any errors encountered by the script compiler are reported to the user. If the project is already open, and a script is added to the project through the Project Properties editor, compiler messages are silently discarded. (This also applies if you undo/redo across the property edit.) Use File > Reload External Files to see the compiler messages.

PluginDllCache Directory

Extension scripts are compiled into .DLLs, and saved in the PluginDllCache directory, which lives next to the application executable and RuntimeData. If the extension script is the same age or older than the DLL, SourceGen will continue to use the existing DLL.

The DLL names are a combination of the script filename and script location. The compiled name for "MyPlatform/MyScript.cs" in the RuntimeData directory will be "RT_MyPlatform_MyScript.dll". For a project-specific script, it would look like "PROJ_MyProject_MyScript.dll".

The PluginCommon and CommonUtil DLLs will be copied into the directory, so that code in the sandbox has access to them.

The contents of the directory are generated as needed, and can be deleted entirely whenever SourceGen isn't running.

Sandboxing

Extension scripts are executed in an App Domain sandbox. App domains are a .NET feature that creates a partition inside the virtual machine, isolating code. It still runs in the same address space, on the same threads, so the isolation is only effective for "partially trusted" code that has been declared safe by the bytecode verifier.

SourceGen disallows most actions, notably file access. An exception is made for reading files from the directory where the plugin DLLs live, but scripts are otherwise unable to read or write from the filesystem. (A future version of SourceGen may provide an API that allows limited access to data files.)

App domain security is not absolute. I don't really expect SourceGen to be used as a malware vector, so there's no value in forcing scripts to execute in an isolated server process, or to jump through the other hoops required to really lock things down. I do believe there's value in defining the API in such a way that we could implement full security if circumstances change, so I'm using app domains as a way to keep the API honest.

Working With Multiple Binaries

Sometimes a program is split into multiple files on disk. They may be all loaded at once, or some may be loaded into the same place at different times. In such situations it's not uncommon for one file to provide a set of interfaces that other files use. It's useful to have symbols for these interfaces be available to all projects.

There are two ways to do this: (1) define a common platform symbol file with the relevant addresses, and keep it up to date as you work; or (2) declare the labels as global and exported, and import them as project symbols into the other projects.

Support for this is currently somewhat weak, requiring a manual symbol-import step in every interested project. This step must be repeated whenever the labels are updated.

A different but related problem is typified by arcade ROM sets, where files are split apart because each file must be burned into a separate PROM. All files are expected to be present in memory at once, so there's no reason to treat them as separate projects. Currently, the best way to deal with this is to concatenate the files into a single file, and operate on that.

Overlapping Address Spaces

Some programs use memory overlays, where multiple parts of the code run in the same address in RAM. Others use bank switching to access parts of the program that reside in separate physical RAM or ROM, but appear at the same address. Nested address regions allow for a variety of configurations, which can make address resolution complicated.

The general goal is to have references to an address resolve to the "nearest" match. For example, consider a simple overlay:

         .ADDRS  $1000
         JMP     L1100

         .ADDRS  $1100
L1100    BIT     L1100
L1103    LDA     #$11
         BRA     L1103
         .ADREND

         .ADDRS  $1100
L1100_0  BIT     L1100_0
L1103_0  LDA     #$22
         JMP     L1103_0
         .ADREND

         .ADREND

Both sections start at $1100, and have branches to $1103. The branch in the first section resolves to the label in the first version of that address chunk, while the branch in the second section resolves to the label in the second chunk. When branches originate outside the current address chunk, the first chunk that includes that address is used, as it is with the JMP $1000 at the start of the file.

The full address-to-offset algorithm is as follows. There are two inputs: the file offset of the instruction or data item that has the reference (e.g. the JMP or LDA), and the address it is referring to.

This searches all children and all siblings before checking the parent. If we hit the top of the tree without finding a match, we conclude that the reference is to an external address.

OMF Relocation Dictionaries

This feature is considered experimental. Some features, like cross-reference tracking, may not work correctly with it.

65816 code can be tricky to disassemble for a number of reasons. 24-bit addresses are formed from 16-bit data-access operands by combining with the Data Bank Register (DBR), which often requires a bit of manual intervention. But the problems go beyond that. Consider the following bit of source code for the Apple IIgs:

rsrcmsg    pea   rsrcmsg2|-16
           pea   rsrcmsg2
           _WriteCString
           lda   #buffer
           sta   pArcRead+$04
           lda   #buffer|-16
           sta   pArcRead+$06

In both cases we're referencing a 24-bit address as two 16-bit values. Without context, the disassembler will treat the PEA instruction as two independent 16-bit addresses, and the immediate values as constants:

                               .dbank  $02
02/317c: f4 02 00     L2317C   pea     L20002 & $ffff
02/317f: f4 54 32              pea     L23254 & $ffff
02/3182: a2 0c 20              ldx     #WriteCString
02/3185: 22 00 00 e1           jsl     Toolbox
02/3189: a9 00 00     L23189   lda     #$0000
02/318c: 8d 78 3f              sta     L23F78 & $ffff
02/318f: a9 03 00              lda     #$0003
02/3192: 8d 7a 3f              sta     L23F78 & $ffff +2

Worse yet, those STA instruction operands would have been shown as hex values or incorrect labels if the DBR had been set incorrectly. However, if we have the relocation data, we know the full address from which the addresses were formed, and we can tell when immediate values are addresses rather than constants. And we can do this even without setting the DBR.

02/317c: f4 02 00     L2317C   pea     L23254 >> 16
02/317f: f4 54 32              pea     L23254 & $ffff
02/3182: a2 0c 20              ldx     #WriteCString
02/3185: 22 00 00 e1           jsl     Toolbox
02/3189: a9 00 00     L23189   lda     #L30000 & $ffff
02/318c: 8d 78 3f              sta     L23F78 & $ffff
02/318f: a9 03 00              lda     #L30000 >> 16
02/3192: 8d 7a 3f              sta     L23F78 & $ffff +2

The absence of relocation data can be a useful signal as well. For example, when pushing arguments for a toolbox call, the disassembler can tell the difference between addresses and constants without needing emulation or pattern-matching, because only the addresses get relocated. Consider this bit of source code:

           lda   <total_records
           pha
           pea   linebuf|-16
           pea   linebuf+65
           pea   $0005
           pea   $0000
           _Int2Dec

Without relocation data, it becomes:

02/0aa8: a5 42                 lda     $42
02/0aaa: 48                    pha
02/0aab: f4 02 00              pea     L20002 & $ffff
02/0aae: f4 03 31              pea     L23103 & $ffff
02/0ab1: f4 05 00              pea     L20005 & $ffff
02/0ab4: f4 00 00              pea     L20000 & $ffff
02/0ab7: a2 0b 26              ldx     #Int2Dec
02/0aba: 22 00 00 e1           jsl     Toolbox

If we treat the non-relocated operands as constants:

02/0aa8: a5 42                 lda     $42
02/0aaa: 48                    pha
02/0aab: f4 02 00              pea     L230C2 >> 16
02/0aae: f4 03 31              pea     L23103 & $ffff
02/0ab1: f4 05 00              pea     $0005
02/0ab4: f4 00 00              pea     $0000
02/0ab7: a2 0b 26              ldx     #Int2Dec
02/0aba: 22 00 00 e1           jsl     Toolbox

Debug Menu Options

The DEBUG menu is hidden by default in release builds, but can be exposed by checking the "enable DEBUG menu" box in the application settings. These features are used for debugging SourceGen. They will not help you debug 6502 projects.

Features: