I can't seem to stop wanting to improve my KENBAK-2/5 project, and anything that gets me "into the weeds" of the instruction set is especially appealing. To that end I have created a stand alone command line Disassembler. I have added the Python script to my GitHub repository and created a release.
Here is the online help:
usage: Disassemble a binary or delimited text file to KENBAK-1 source [-h] [-s] [-d] [-a] -f FILE optional arguments: -h, --help show this help message and exit -s, --save save the disassembly to a .asm file -d, --dump dump the disassembly to standard out -a, --address add the instruction address to each line -f FILE, --file FILE file to disassemble
Writing a disassembler is an interesting exercise, one that I had never undertaken before. Faced with an array of 256 raw bytes and trying to coax some meaningful structure out of it was pretty daunting.
The first thing I did was to determine which of the bytes represented the program code vs program data. Fortunately the first byte of code can be ascertained by looking at the fourth byte in memory which holds the PC (program counter/instruction pointer - defaults to 4). From that point, consecutive instructions can be readily resolved, that is until a "jump" instruction is encountered. By carefully following all of the jumps to other blocks of code you can create a map of all the code in the program.
All of the non-code bytes are then assumed to be data. But which of those bytes represent constants vs data bytes that will be manipulated by the program. Since all of the "registers" on a KENBAK-1 (A, B, X, PC, OUTPUT, AOC, BOC, XOC, and INPUT) are at fixed positions in memory that is a good place to start marking them as data bytes using a DB directive. The other bytes that will be written to can be determined by looking at all of the instructions that store to memory: STORE, SET, and JMK. Everything else is then treated as a constant.
Finally I did a pass to convert all of the memory references in the code to labels to make reading the emitted code a bit easier.
To test the disassembler I input the 256 byte "Day of the Week.bin" file created by the IDE assembler and compared the assembly output to the original code that I wrote. Furthermore I ran the disassembled code inside of the IDE to ensure that it indeed worked as expected.
Here is the original code:
; Program to calculate the day of the week for any date. To start this program you will
; have to input the date in four parts: Century, Year, Month, and Day. Each of the parts
; is entered as a two digit Binary Coded Decimal number (ie. the first digit will occupy
; bits 7-4 as a binary number, and the second digit bits 3-0) using the front panel data
; buttons. The steps to run this program are:
;
; 1) Set the PC register (at address 3) to 4.
; 2) Clear the input data then enter the date Century.
; 3) Press Start.
; 4) Clear the input data then enter the date Year.
; 5) Press Start.
; 6) Clear the input data then enter the date Month.
; 7) Press Start.
; 8) Clear the input data then enter the date Day.
; 9) Press Start.
;
; The day of the week will be returned via the data lamps using the following encoding:
;
; 7-Sunday 6-Monday 5-Tuesday 4-Wednesday 3-Thursday 2-Friday 1-Saturday
;
; All lamps turned on means the last item entered was invalid and you have to restart.
;
;
; Get the date we want the day for.
;
load A,INPUT ; Get the century.
jmk bcd2bin
store A,century
halt
load A,INPUT ; Get the year.
jmk bcd2bin
store A,year
halt
load A,INPUT ; Get the month.
jmk bcd2bin
sub A,1 ; Convert from 1 based to 0 based.
store A,month
halt
load A,INPUT ; Get the day.
jmk bcd2bin
store A,day
load A,0b10000000 ; Setup the rotation pattern.
store A,rotate
;
; All the inputs should be in place. Start the conversion.
;
load A,year ; Get the year.
sft A,R,2 ; Divide by 4.
store A,B ; Save to B the working result.
add B,day ; Add the day of the month.
load X,month ; Use X as index into the month keys.
add B,monkeys+X ; Add the month key.
jmk leapyr ; Returns a leap year offset in A if applicable.
jmk working ; Working...
sub B,A ; Subtract the leap year offset.
jmk cencode ; Returns a century code in A if applicable.
jmk working ; Working...
add B,A ; Add the century code.
add B,Year ; Add the year input to the working result.
chkrem
load A,B ; Find the remainder when B is divided by 7.
and A,0b11111000 ; Is B > 7?
jmp A,EQ,isseven ; No then B is 7 or less.
sub B,7 ; Yes then reduce B by 7.
jmk working ; Working...
jmp chkrem ; Check again for remainder.
isseven
load A,B ; Is B = 7?
sub A,7 ; Subtract 7 from B value.
jmp A,LT,gotday ; No B is less than 7.
load B,0 ; Set B to zero because evenly divisible.
gotday
load X,B ; B holds the resulting day number. Use as index.
load A,sat+X ; Convert to a day lamp.
store A,OUTPUT
halt
error
load A,0xff ; Exit with error
store A,OUTPUT ; All lamps lit.
halt
;
; Store inputs.
;
century db
year db
month db
day db
;
; Static table to hold month keys.
;
monkeys 1
4
4
0
2
5
0
3
6
1
4
6
;
; Need to preserve A while performing some steps.
;
saveA db
;
; Subroutine to blink the lamps to indicate working.
;
rotate db ; Pattern to rotate.
working db ; Save space for return adderess.
store A,saveA ; Remember the value in A.
load A,rotate ; Get the rotate pattern.
store A,OUTPUT ; Show the rotated pattern.
rot A,R,1 ; Rotate the pattern.
store A,rotate ; Save the new rotation.
load A,saveA ; Restore the value of A.
jmp (working) ; Return to caller.
org 133 ; Skip over registers.
;
; Subroutine takes a BCD nuber in A as input and returns the equivalent binary number
; also in A.
;
bcd2bin db ; Save space for return address.
store A,X ; Save A.
sft A,R,4 ; Get the 10's digit.
jmk chkdig ; Make sure digit is 0 - 9.
store A,B ; B will hold the 10's digit x 10 result
add B,B ; B now X 2
sft A,L,3 ; A is now 10's digit X 8
add B,A ; B now 10's digit X 10
store X,A ; Retrieve original value of A
and A,0b00001111 ; Get the 1's digit value in binary.
jmk chkdig ; Make sure digit is 0 - 9.
add A,B ; Add the 10's digit value in binary.
jmp (bcd2bin) ; A now has the converted BCD value.
;
; Subroutine determines if the date is a leap year in January or February and returns
; an offset of 1 if it is, and 0 otherwise.
;
leapyr db ; Save space for return address.
load A,month ; Check to see if month is January or February.
and A,0b11111110 ; Are any bits other than bit 0 set?
jmp A,NE,notlpyr ; Yes then not January or February. Return 0.
load A,year ; Is this an even century?
jmp A,NE,chkyear ; No then have to check the year.
load A,century ; Yes so see if century evenly divisible by 4.
and A,0b00000011 ; Are bits 1 or 0 set?
jmp A,EQ,islpyr ; Yes evenly divisible by 4 and is a leap year.
jmp notlpyr ; No this is not a leap year.
chkyear
load A,year ; See if rear evenly divisible by 4.
and A,0b00000011 ; Are bits 1 or 0 set?
jmp A,NE,notlpyr ; Yes so not evenly divisible by 4 and not a leap year.
islpyr
load A,1 ; Offset 1.
jmp (leapyr) ; Return offset.
notlpyr
load A,0 ; Offset 0.
jmp (leapyr) ; Return offset.
;
; Subroutine determines if a century code needs to be applied to the calculation.
;
cencode db ; Save space for return address.
load A,century ; Century must be between 17 - 20.
chkmin
sub A,17 ; Is century less than 17?
jmp A,GE,chkmax ; Yes so century >= 17. Check max boundry.
load A,century ; Increase century by 4.
add A,4
store A,century
jmp chkmin
chkmax
load A,century ; Century must be between 17 - 20.
sub A,20 ; Is century greater than 20?
jmp A,LT,retcode ; No so calculate century code.
jmp A,EQ,retcode
load A,century ; Decrease century by 4.
sub A,4
store A,century
jmp chkmax+2
retcode
load X,century ; Calculate the century code
sub X,17 ; Create an index into the century codes.
load A,ctcodes+X ; Get the appropriate century code.
jmp (cencode) ; Return century code.
;
; Subroutine that checks if the digit passed in A is in range 0 - 9.
;
chkdig db ; Save space for return adderess.
store A,saveA ; Remember value in A.
load A,9
sub A,saveA ; Subtract value passed from 9.
and A,0b10000000 ; Is negative bit set?
jmp A,NE,error ; Yes so value in A not in range 0 - 9.
load A,saveA ; No so A value in range.
jmp (chkdig) ; Return to caller.
;
; Static table to hold the output pattern for the day of the week.
;
sat 0b00000010
sun 0b10000000
mon 0b01000000
tues 0b00100000
wed 0b00010000
thur 0b00001000
fri 0b00000100
;
; Static table to hold century codes.
;
ctcodes 4
2
0
6
And here is the disassembled code:
;
; Disassembly for [Day of the Week.bin]
;
org 0
A db
B db
X db
PC db
load A,INPUT
jmk LAB133
store A,LAB094
halt
load A,INPUT
jmk LAB133
store A,LAB095
halt
load A,INPUT
jmk LAB133
sub A,1
store A,LAB096
halt
load A,INPUT
jmk LAB133
store A,LAB097
load A,128
store A,LAB111
load A,LAB095
sft A,R,2
store A,B
add B,LAB097
load X,LAB096
add B,LAB098+X
jmk LAB156
jmk LAB112
sub B,A
jmk LAB189
jmk LAB112
add B,A
add B,LAB095
LAB062 load A,B
and A,248
jmp A,EQ,LAB074
sub B,7
jmk LAB112
jmp LAB062
LAB074 load A,B
sub A,7
jmp A,LT,LAB082
load B,0
LAB082 load X,B
load A,LAB243+X
store A,OUTPUT
halt
LAB089 load A,255
store A,OUTPUT
halt
LAB094 db
LAB095 db
LAB096 db
LAB097 db
LAB098 1
4
4
0
2
5
0
3
6
1
4
6
LAB110 db
LAB111 db
LAB112 db
store A,LAB110
load A,LAB111
store A,OUTPUT
rot A,R,1
store A,LAB111
load A,LAB110
jmp (LAB112)
0
0
OUTPUT db
OCA db
OCB db
OCX db
0
LAB133 db
store A,X
sft A,R,4
jmk LAB228
store A,B
add B,B
sft A,L,3
add B,A
store X,A
and A,15
jmk LAB228
add A,B
jmp (LAB133)
LAB156 db
load A,LAB096
and A,254
jmp A,NE,LAB185
load A,LAB095
jmp A,NE,LAB175
load A,LAB094
and A,3
jmp A,EQ,LAB181
jmp LAB185
LAB175 load A,LAB095
and A,3
jmp A,NE,LAB185
LAB181 load A,1
jmp (LAB156)
LAB185 load A,0
jmp (LAB156)
LAB189 db
load A,LAB094
LAB192 sub A,17
jmp A,GE,LAB204
load A,LAB094
add A,4
store A,LAB094
jmp LAB192
LAB204 load A,LAB094
LAB206 sub A,20
jmp A,LT,LAB220
jmp A,EQ,LAB220
load A,LAB094
sub A,4
store A,LAB094
jmp LAB206
LAB220 load X,LAB094
sub X,17
load A,LAB250+X
jmp (LAB189)
LAB228 db
store A,LAB110
load A,9
sub A,LAB110
and A,128
jmp A,NE,LAB089
load A,LAB110
jmp (LAB228)
LAB243 2
128
64
32
16
8
4
LAB250 4
2
0
6
0
INPUT db
As mentioned the disassembler will accept 256 byte raw binary files as input (files with a .bin extension). It can also parse delimited text files (with a .txt extension). The delimiters can be any combination of white space characters (like spaces, tabs, newlines, etc.) plus periods, commas, colons, and semi-colons. The bytes themselves can be expressed as hexidecimal (starts with 0x), decimal (start with 1-9), octal (starts with 0), or binary (starts with 0b) , but must resolve to a number less than 256.
If the --save option is chosen a disassembly file will be generated. The file name will be the same as the input's with the extension replaced by ".asm".
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.