Skip to navigation

Preparing source code from the binaries

3 September to 5 September 2025

Here's my disassembly diary for this part of my project to document The Sentinel on the BBC Micro. You can click on the following links to jump to a specific day in the diary:

  • 3 September 2025 - Get game discs, extract binaries, create repository, create skeleton build process, create simple py8dis disassembly, start identifying code blocks
  • 4 September 2025 - Implement loader unpacking with COPYBLOCK, finish identifying code blocks, copy py8dis output into main source code file, switch to using text editor, add headers to sections (source start, configuration variable, workspaces, code, saving)
  • 5 September 2025 - Apply house style to OS calls, identify subroutine entry points that aren't in the sub_* format (e.g. JSR destinations, labels after RTS), convert all other labels to five characters (letter + hex), insert subroutine headers before all sub_ labels, create an empty progress spreadsheet, start at 0% progress

Please note that this diary is a dump of my thoughts as I disassembled and documented The Sentinel, and as such it contains lots of mistakes and dead ends and misinterpretations. This diary is all about the journey, rather than the destination; for the latter, see the finished product.

3 September 2025
================

See all the GitHub commits and diffs for 3 September 2025.

The very first step - hunt down tape/disc images.

Found the following:

Stardot links are taken from here.

Extract files using Disc Image Manager or JSBeeb (latter can be useful for HFE).

This doesn't get around any copy protection.

Analyse extracts with crc32 into a spreadsheet to pick out suitable binaries.

See the checksum spreadsheet in Google Sheets.

We have found a suitable binary!

Play It Again Sam 6 doesn't have any real disc protection, so extraction is possible, like this:

  • Start with HFE of Play It Again Sam 6.
  • Use JSBeeb to convert HFE to SSD.
  • Extract game binary from SSD in Disc Image Manager.

This doesn't work with original Firebird disc, but does work with PIAS 6 version.

Now to check whether this version differs from the others.

Load it into HexEdit and search for game binary, comparing it to binary from BBC Micro Games Archive on bbcmicro.co.uk, so we know we have the genuine game binary from the PIAS 6 disc.

Create empty git repository and add standard .git files.

See the empty repository.

Add skeleton build process that simply adds game binaries to a disc image to create a working game disc.

See the repository with the skeleton build.

Create a basic py8dis setup within the repo.

This uses py8dis to disassemble the game binary and then reassembles the disassembly output from py8dis to recreate the game binary.

It shows two crc32 scores that match the py8dis output with the game binary so we can see if any changes we make break the build.

We can then start to work on the py8dis output to get it to a point where we can take it and work on it manually.

Start by simply loading the binary at its load address of &1900 and setting an entry point of &6D00 (which we take from the file attributes).

See the basic py8dis setup in the repository.

Look at Entry routine in py8dis output.

This shows the first routine at &6D00 moves the whole binary down to &0400 and jumps to &3F04, so we can update py8dis to load the file at &0400 and set an entry point at &3F04.

And suddenly we have quite a good disassembly that we can start to pick apart.

See the disassembled source in the repository.

Next, go through the disassembly and try to identify which parts of the code are just data blocks, and which are code that hasn't been disassembled.

e.g. the contents of &0400 through to &0CFF is clearly data or noise, so we change this to EQUBs using:

  byte(0x0400, 0x0800, cols=8)

This pumps out a bunch of labels like L0A40, so we check these in the code to make sure they look like they are variables (which they are).

In a number of places we suddenly fall from code into data, like this:

  LSR L008B                                                            ; 0E70
  ROR L008A                                                            ; 0E72
 .return_1
  RTS                                                                  ; 0E74
 
  EQUB &8D, &0C, &0C, &8E, &3B, &0F, &20, &3E, &0F, &8D, &53, &0C, &A5 ; 0E75
  EQUB &75, &8D, &54, &0C, &A2,   1, &86, &60, &A2,   0, &2C, &0C, &0C ; 0E82
  EQUB &50,   3, &E8, &C6, &60, &C9, &7A, &90,   9, &B0, &31, &AD, &53 ; 0E8F
  EQUB &0C, &C9, &F0, &B0, &2A, &A9, &AB, &20,   3, &0D, &20,   3, &0D ; 0E9C

&0E75 looks like code, though, so we try adding an entry point like this:

  entry(0x0E75, "sub_C0E75")

and now &0E75 onwards is code again:

  LSR L008B                                                            ; 0E70
  ROR L008A                                                            ; 0E72
 .return_1
  RTS                                                                  ; 0E74
 
 .sub_C0E75
  STA L0C0C                                                            ; 0E75
  STX L0F3B                                                            ; 0E78
  JSR sub_C0F3E                                                        ; 0E7B
  STA L0C53                                                            ; 0E7E
  LDA L0075                                                            ; 0E81
  STA L0C54                                                            ; 0E83

Same for lots of points where py8dis thinks this is data:

  entry(0x0E75, "sub_C0E75")
  entry(0x0F70, "sub_C0F70")
  entry(0x0F9E, "sub_C0F9E")
  entry(0x10B7, "sub_C10B7")
  entry(0x118B, "sub_C118B")
  entry(0x1224, "sub_C1224")

The last one is interesting, as &1222 has two zeroes, which would be BRK instructions.

This is unlikely, so these are probably variables, with the code starting at &1224, which is indeed the case.

Sometimes we try to convert data into code, and it just doesn't look right.

Example is at &137D and &138C - these are almost certainly lookup tables of some kind, so we can make these EQUBs.

4 September 2025
================

See all the GitHub commits and diffs for 4 September 2025.

Started looking through Begin, and it moves a block of memory from &4100 to &5800, so we need to capture this in the disassembly somehow.

Started by trying move() commands in py8dis, which works but because an instruction is split across a move boundary, it's complex, so instead I went for manually splitting the binary, as with Revs.

See the split files in the repository.

And then added COPYBLOCK commands to move the binary around, and document with comments in the py8dis source as I find COPYBLOCK totally confusing!

See the py8dis source in the repository.

Tidied up data blocks from &4A00 to &5560 by applying byte() to them.

For some, like this:

 .L5500
  EQUB &01                                                             ; 5500
 .L5501
  EQUB &01, &02, &02, &03, &03, &04, &04, &05, &05, &06, &06, &07, &07 ; 5501
  EQUB &08, &08, &09, &09, &0A, &0A, &0B, &0B, &0C, &0C, &0C, &0D, &0D ; 550E
  EQUB &0E, &0E, &0F, &0F, &0F                                         ; 551B

It seems as if the table might contain two-byte entries, as the only time L5501 appears is here:

  LDA #&FF                                                             ; 278C
  STA L0B00,Y                                                          ; 278E
  STA L0B01,Y                                                          ; 2791
  STA L5520,Y                                                          ; 2794
  STA L5500,Y                                                          ; 2797
  LDA #&14                                                             ; 279A
  STA L5521,Y                                                          ; 279C
  STA L5501,Y                                                          ; 279F

So the chances are that this will end up being changed to L5500+1, so we just set one byte block at L5500 with this py8dis line:

  byte(0x5500, 0x5520 - 0x5500, cols=8)

so we get a block that incorporates L5501, like this:

 .L5500
  L5501 = L5500+1
  EQUB &01, &01, &02, &02, &03, &03, &04, &04                          ; 5500
  EQUB &05, &05, &06, &06, &07, &07, &08, &08                          ; 5508
  EQUB &09, &09, &0A, &0A, &0B, &0B, &0C, &0C                          ; 5510
  EQUB &0C, &0D, &0D, &0E, &0E, &0F, &0F, &0F                          ; 5518

There is no doubt a way to do this in py8dis, but I'm a lazy hacker and just want to get into the text editor stage as quickly as possible.

Sometimes it looks as if a label should be inserted, like this:

  EQUB &87, &87, &87, &87, &87, &87, &0F, &0F                          ; 58F8
  EQUB &10, &00, &01, &00                                              ; 5900
 .L5904
  EQUB &07, &00, &05, &00, &11, &00, &00, &00                          ; 5904

The data abruptly changes style at &5900, and looks more like it's related to the L5904 data than the &58xx data, so we insert a label with:

  label(0x5900, "L5900")

If it's wrong, it doesn't matter.

If we make our byte() blocks too big, then we get output like this:

 .L5900
  L5904 = L5900+4
  L590C = L5900+12
  L5910 = L5900+16
  L5912 = L5900+18
  L5914 = L5900+20
  L591C = L5900+28
  L5928 = L5900+40
  L5951 = L5900+81
  L595F = L5900+95
  EQUB &10, &00, &01, &00, &07, &00, &05, &00                          ; 5900
  EQUB &11, &00, &00, &00, &78, &00, &0A, &00                          ; 5908
  EQUB &12, &00, &03, &00, &22, &00, &14, &00                          ; 5910
  EQUB &13, &00, &04, &00, &90, &00, &14, &00                          ; 5918
  EQUB &10, &00, &02, &00, &04, &00, &28, &00                          ; 5920
  EQUB &01, &02, &00, &00, &00, &00, &00, &00                          ; 5928
  EQUB &14, &00, &EC, &EC, &78, &78, &02, &04                          ; 5930
  EQUB &00, &00, &00, &00, &00, &00, &02, &FF                          ; 5938
  EQUB &00, &00, &78, &78, &04, &02, &01, &FF                          ; 5940
  EQUB &00, &01, &01, &08, &78, &FF, &00, &FF                          ; 5948
  EQUB &78, &08, &03, &01, &06, &FA, &00, &01                          ; 5950
  EQUB &01, &00, &01, &FF, &00, &00, &78, &08                          ; 5958
  EQUB &04, &82, &01, &FF, &00, &02, &01, &07                          ; 5960
  EQUB &78, &FA, &FE, &FE, &78, &00, &01, &01                          ; 5968
  EQUB &00, &00, &00, &00, &00, &00, &78, &88                          ; 5970
  EQUB &FF, &FF, &78, &00, &00, &00, &00, &00                          ; 5978

Is this one long lookup table that is accessed in chunks, or is it a sequence of lookup tables? Probably the latter, but I can't see any obvious blocks to split it into, so we can work this out as we work through the disassembly later.

Meanwhile, at &5980 we have this:

  EQUB &78, &FA, &FE, &FE, &78, &00, &01, &01                          ; 5968
  EQUB &00, &00, &00, &00, &00, &00, &78, &88                          ; 5970
  EQUB &FF, &FF, &78, &00, &00, &00, &00, &00                          ; 5978
  EQUB &00, &03, &06, &09, &0D, &10, &13, &16                          ; 5980
  EQUB &19, &1C, &1F, &22, &26, &29, &2C, &2F                          ; 5988
  EQUB &32, &35, &38, &3B, &3E, &41, &44, &47                          ; 5990
  EQUB &4A, &4D, &50, &53, &56, &59, &5C, &5F                          ; 5998

This looks like a change from data into lookup table - from &5980 onwards the values steadily increase, whereas before that point they are either very high or very low (which could be a lookup table, maybe a high byte, but it feels like a different batch of data). So we add a label at &5980, because why not.

And another at &5260:

  EQUB &FF, &FF, &FF, &FF, &FF, &7F, &FF, &FF                          ; 5240
  EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF                          ; 5248
  EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF                          ; 5250
  EQUB &FF, &FF, &FF, &FF, &FF, &FF, &FF, &FF                          ; 5258
  EQUB &54, &50, &51, &55, &54, &51, &4E, &55                          ; 5260
  EQUB &51, &54, &4F, &50, &54, &4F, &53, &52                          ; 5268
  EQUB &4E, &4F, &43, &47, &46, &42, &43, &40                          ; 5270
  EQUB &44, &47, &43, &40, &42, &46, &45, &41                          ; 5278
  EQUB &42, &41, &45, &44, &40, &41, &47, &4C                          ; 5280

Ditto at &4F40, &5080 and &51C0.

Then there are remnants of source code in the binary, which I've labelled as Noise1, Noise2 and Noise3 (for "workspace noise").

  label(0x400A, "Noise1")
  label(0x5A00, "Noise2")
  label(0x5B00, "Noise3")

It looks like the latter is used as variable space, as there are lots of pointers into the noise, but we'll see.

Now, look at the labels py8dis has put in there. They seem OK... but what about this one?

  L83A9                = &83A9

That doesn't feel right. It appears just once, here:

 .sub_C35A3
  LDA L83A9,Y                                                          ; 35A3
  JSR sub_C5E2C                                                        ; 35A6

A9 83 is an instruction on its own, so is this subroutine at the wrong address - should it be at &35A4 instead? &35A4 doesn't appear in the disassembly, but this subroutine could be reached via a jump table. In any event, LDA L83A9,Y sounds wrong, so change:

  entry(0x35A3, "sub_C35A3")

to:

  entry(0x35A4, "sub_C35A4")

to give:

  EQUB &B9                                                             ; 35A3
 
 .sub_C35A4
  LDA #&83                                                             ; 35A4
  JSR sub_C5E2C                                                        ; 35A6

which at least gets rid of the (surely) incorrect L83A9 label.

So now we are done and we can switch from py8dis to text editor.

Add config.set_hex_dump(False) to py8dis to remove hex dump, but leave hex dump output in the py8dis part of the repo so we can refer back to it if required.

Copy and paste into main source file, and:

  • Split into sections (configuration variables, zero page etc.)
  • Strip comments with a regex
  • Add blank line around all .labels with a regex

Now to start editing in a text editor (I use VSCode and BBEdit), checking the build after each change to make sure a matching binary is still being built.

5 September 2025
================

See all the GitHub commits and diffs for 5 September 2025.

Minor tidying of EQUB blocks:

  • 0x5FFF to 0x6100 (do in py8dis)
  • Manually wrap blocks of zeroes in 0x0C00 workspace

Tidy up sub_ labels, ready for adding headers.

Search for "B.. sub_" and check whether the destination labels also have JSRs.

sub_C3763, sub_C5560 have no JSRs only branches, so remove sub_ from these.

Also search for "JMP sub_" and check whether the destination labels also have JSRs.

(All matched JMPs also have JSRs, so nothing to change here.)

Convert OSBYTE calls, SHEILA pokes and OS variable names to house style.

Convert vector addresses to variables (e.g. IRQ1V).

Move L49xx labels into correct location within main game code, so config and workspace variable blocks contain the correct declarations.

Look for labels that are JSR'd but aren't in the format sub_C* by looking for "JSR C" and changing the destination label to sub_C, so that's sub_C39D9, sub_C125A, sub_C3194, sub_C1200 and so on.

Look for RTS followed by a code label that isn't a sub_, judging each on their own merit (convert if it looks like a self-contained bit of code rather than a part of a multi-part routine).

Do this by searching for grep:

  RTS\n\n\.[^sl]

e.g. C1017 -> sub_C1017, C1AEC -> sub_C1AEC.

But C2136 is OK as it is as it is the destination for the BCC above:

 .sub_C2127
 
  LDY L0A40,X
  LDA L0C0A
  BCC C2136
  SBC L2140,Y
  BCS C2139
  SEC
  RTS
 
 .C2136
 
  ADC L2140,Y
  ...

Convert return_xx to CREXX so all in-code labels are 5 chars and start with C.

Convert loop_C1234 to P1234 for the same reason.

Now that we have added as many correct sub_ links as possible, we add subroutine headers:

Search for grep:

  ^\.(sub_.+)$

Replace with:

\ ******************************************************************************
\
\       Name: $1
\       Type: Subroutine
\   Category: ???
\    Summary: ???
\
\ ******************************************************************************

.$1

Now add variable headers. Search for grep (case sensitive to avoid matching loop_ labels):

  ^\.(L.+)$

Replace with:

\ ******************************************************************************
\
\       Name: $1
\       Type: Variable
\   Category: ???
\    Summary: ???
\
\ ******************************************************************************

.$1

Manually add headers to Begin, Entry and NoiseX.

Scroll through and look for code that needs a header, e.g. L5FFF and C5560 (latter also needs to be sub_C5560).

Convert L0C00 block to a workspace.

Remove headers for loops, e.g. loop_C1054 (because I forgot to make the variable header replace case sensitive - oops!).

Move parts of subroutines that come before main entry point into subroutine, e.g.

 .C1F78
 
  LDA #&00
  STA L0C6D
  STA L0C4D
  STA L0C1E
  RTS

\ ******************************************************************************
\
\       Name: sub_C1F84
\       Type: Subroutine
\   Category: ???
\    Summary: ???
\
\ ******************************************************************************

 .sub_C1F84
 
  JSR sub_C2096
  BCS C1F78
  LDA #&19

changes to:

\ ******************************************************************************
\
\       Name: sub_C1F84
\       Type: Subroutine
\   Category: ???
\    Summary: ???
\
\ ******************************************************************************

 .C1F78
 
  LDA #&00
  STA L0C6D
  STA L0C4D
  STA L0C1E
  RTS
 
 .sub_C1F84
 
  JSR sub_C2096
  BCS C1F78
  LDA #&19

Remove sub_ headers when they are obviously not subroutines, e.g. change:

\ ******************************************************************************
\
\       Name: sub_C23C6
\       Type: Subroutine
\   Category: ???
\    Summary: ???
\
\ ******************************************************************************

 .sub_C23C6

  L23C7 = sub_C23C6+1

to:

 .C23C6

  L23C7 = C23C6+1

Rename Noise1-3 back to Lxxxx for consistency, adding details about the noise to the header. So now all labels are in this format.

Rename Begin to sub_C3F04 as it probably needs a better name and this gives everything the same format name.

Now to create a progress spreadsheet.

Start by extracting names and line numbers from source using regex:

  1. Load source code into text editor (see the source at this point).
  2. Merge Name and Type lines into one (search for "\n\\ Type: " and replace with \t).
  3. Save file (see the source at this point).
  4. Run a grep, sending output to a file:
      grep -n "\t" source.txt > line_numbers.txt
    
    This outputs the line_numbers.txt file with each line as follows, where \t is a tab:
      1595:\       Name: sub_C0F1C\tSubroutine
    
    First figure is the line number, then the rest contains the data we need.
  5. Process this into a suitable format for a spreadsheet with a regex, replacing:
      ":\\       Name: "
    
    (without quotes), with:
      \t
    
    This creates a TSV with three columns: line number, name, type.
  6. Paste this into a blank spreadsheet and add a new column to the start that calculates the difference between each entry's line value and the next, to give a line count for that section (we have to add an extra line number entry for the very last calculation). So the first row's calculation is =B2-B1, for example, which we can copy to fill the whole of column 1. So the sheet now contains four columns: line count, line number, name, type.
  7. Duplicate a progress sheet from a previous project (Revs in this instance) as "The Sentinel progress".
  8. Copy the line count table from the spreadsheet into a text editor, copy out of the text editor, paste into a blank spreadsheet, delete the line number column (leaving just the line count column), and paste into the "Line numbers" tab of "The Sentinel progress". Tidy up the "Summary" tab by removing categories that aren't applicable, and we have a starting point for the project tracking.

See the result in the 0% progress sheet in Google Sheets. The divisions by zero are correct for now: we need to increase the "Done" figure to get rid of them.

So we now have a sheet for tracking progress: 13,716 lines to document, with 0% done.

Time to start documenting this source code!