Or, decrypting the unencrypted cryptic code
I decided to spend some more time looking at how PROMAL works internally. The next routine I decided to examine is BLKMOV, as its function is similar to the MOVSTR function examined earlier.
Let’s have a look at the jump table to find BLKMOV:
EXT ASM PROC BLKMOV AT $F30
0f30 4C A9 21 JMP $21A9
Easy enough! Let’s have a look to see how BLKMOV initializes itself, and to see if it accepts a 16-bit length.
21a9 20 15 18 JSR $1815
21ac 33 E0 RLA ($E0),Y
21ae 38 SEC
21af 34 2C NOOP $2C,X
21b1 A5 34 LDA $34
21b3 38 SEC
21b4 E5 2C SBC $2C
21b6 A8 TAY
21b7 A5 35 LDA $35
21b9 E5 2D SBC $2D
21bb AA TAX
21bc 98 TYA
21bd C5 38 CMP $38
21bf 8A TXA
21c0 E5 39 SBC $39
Wait. WHAT? (Almost) no professional programmer would use illegal opcodes for a final product. The RLA and NOOP $2C,X are invalid opcodes. Also, I’ve tested PROMAL and found that it works well on the CMD SuperCPU accelerator, which will behave differently with illegal opcodes and cause PROMAL to crash.
Caller relative addressing
Let’s see what exactly is going on with 1815. I’ve studied the routine ahead of time and added some helpful comments and names to the disassembly. Because this routine is used by many library routines, I will refer to the caller as “the parent”. After analyzing the routine I’ll provide BLKMOVs results to help clarify the routine’s work.
; we are always a grandchild!
; parameter = value from p-code interp
; argument = value from caller's table
1815: 68 getprm PLA ; get parents address
1816: 85 3A STA Parent ;
1818: 68 PLA ;
1819: 85 3B STA Parent+1 ; and save for our use
181B: 68 PLA ; get grandparent's address
181C: 85 6D STA GParent ;
181E: 68 PLA ;
181F: 85 6E STA GParent+1 ; and save for later restoration
1821: 84 6A STY numparms ; save number of parameters
1823: A0 01 LDY #$01 ; initialize index
1825: B1 3A LDA (Parent),Y; get min/max parameters
I knew something was odd. This is an uncommon but handy trick: The data after the JSR in the parent is never executed. If you examine 1816, you see that it stores the parent’s address from the stack as MOVSTR did. It then stores the parent’s parent’s return address (‘GParent’) so that it can get to the parameters on the stack later. After that’s all set up, the number of parameters from PROMAL is saved to numparms and the Y index is initialized to 1.
Why all this work? The method is used when resources are tight: We have parameters on the stack that get processed by many routines in the library. It’s best for code space efficiency if a single routine handles these parameters. However, not all library routines use the same number or even type of parameters. That’s where this routine comes in. The arguments for the ‘getprm’ routine are stored after the JSR from the library routine calling it. This way each library routine will be able to specify what type of information it expects to find on the stack.
On arguments and parameters
In this post I need to distinguish between two things: The data used by the getprm routine, and the data the parent needs from the stack. In this case, ‘argument’ refers to data used by getprm, and ‘parameter’ refers to any data passed on the stack by PROMAL. This is done in consistency with the MOVSTR post.
Let’s have a good look at this routine to understand what it does.
Setting things up
We already have the calling routines’ addresses safely shuffled away, and we have our first argument retrieved from the parent.
1827: 29 0F AND #$0F ; Mask max #parms off
1829: 85 69 STA maxparms ; and save
182B: C5 6A CMP numparms ; compare with paremeter count
182D: 90 10 BCC getperr ; too many, runtime error
In the segment above, the argument loaded from the parent is masked off and saved. Studying the routine ahead of time helped me understand that the low half of the first argument is the ‘minimum’ number of arguments the parent requires. If the number of arguments provided by PROMAL (‘CMP numparms’) is larger than the maximum, the routine branches off to a fatal runtime error (‘BCC getperr’).
182F: B1 3A LDA (Parent),Y; get min/max parameters
1831: F0 40 BEQ getpfin ; 0/0 parms? exit.
The argument is reloaded since it was mangled when setting up maxparms. While it’s loaded and un-mangled, the routine checks to see if there are no parameters to be loaded. If this is the case, the routine exits. It would seem to make no sense to call this routine if you don’t want any parameters. I’d agree, but there must be a good reason to call it in this fashion as a few library routines do just that.
1833: 4A LSR ;
1834: 4A LSR ;
1835: 4A LSR ;
1836: 4A LSR ;
1837: 85 6B STA minparms ; save min #parms
1839: C5 6A CMP numparms ; compare with parameter count
183B: F0 05 BEQ getpok ; same? ok.
183D: 90 03 BCC getpok ; more than min parms? ok.
183F: 4C 80 10 getperr JMP syserr ; fail out via system error
Now, the upper half of the first argument is shifted down and store in ‘minparms’. It’s again compared to the numparms value, this time to determine if there are at least the correct number of parameters (beq: bcc). If not, the routine fails through to a jump to PROMAL’s fatal runtime error routine.
1842: C8 getpok INY ; Increment index
1843: B1 3A LDA (Parent),Y; Get mask bits
There will be a lot of INY : LDA (Parent),y as the routine works its way through the argument table.
1845: 85 6C STA getpmsk ; Save in mask byte
The second argument is stored to getpmsk, short for ‘getprm mask.’ This byte is actually eight flags, each indicating the type of parameter to get from the stack. There are two types of data and one way to work with each. As a quick reminder, PROMAL always pushes parameters as words, even when they’re bytes.
Bit = 0 Parameter is a byte
The next argument byte is a zero-page address and a default value.
* Store this byte where specified at the address
* Load and discard high byte from stack if applicable
* Load and store low byte from stack at the address if applicable
Bit = 1 Parameter is a word
This argument is one zero-page address.
* Load and store the high byte from stack at the address+1
* Load and store the low byte from the stack at the address
The routine appears to not have any facilities for handling a default 16-bit value. It’ll be up to the parent to detect a missing 16-bit parameter and set up a default value in its place.
Processing arguments and setting up parameters
At this point, the routine is initialized and ready to load parameters as specified by the parent until it’s out of arguments.
1847: C6 69 getpl DEC maxparms ; Decrement parameter count
1849: 30 28 BMI getpfin ; Out of parms? exit.
Maxparms is now used as a count-down value to determine when the routine’s out of arguments. The name of the location is a bit of a misnomer, I apologize.
184B: C8 INY ;
184C: B1 3A LDA (Parent),Y; get zp address
184E: AA TAX ; and save
The arguments now always start with a zero page address. This is read from the argument table and saved in the X register to be used as an index. This allows the code to run without modifying itself and is a good example of advanced indexing when used in this situation.
184F: A5 69 LDA maxparms ; check max parms
1851: C5 6B CMP minparms ; are we out of required parms?
1853: 90 0F BCC gpfprm ; No, go pull it off the stack.
In this section of the loop, maxparms is compared with minparms to determine whether or not we’re out of required parameters.
1855: 24 6C BIT getpmsk ; is current parm a word?
1857: 30 05 BMI gpisw ; yep, skip
Remember the paramter type mask? This is one of the two checks against the flags in the loop. The BIT instruction does a handful of things, but of interest to the routine is the way it copies bit 7 of getpmsk to the negative flag without modifying any other registers. In this case, if a parameter is a word the negative flag gets set and the BMI (branch if minus) routes the cpu to gpisw (short for getparm is word), below.
1859: C8 INY ;
185A: B1 3A LDA (Parent),Y; get default value or low byte
185C: 95 00 STA 0,X ; store at zp address
185E: A5 69 gpisw LDA maxparms ; is current argument
1860: C5 6A CMP numparms ; greater than parameter count?
1862: B0 0A BCS gpdefl ; Yes, process default value
The next argument byte is loaded if it’s a ‘byte’ type. It’s stored at the zero page location pointed to by X, which was read in earlier. Then it follows through to ‘gpisw’, which checks the current argument against the number of parameters provided to the parent by PROMAL. If we’re out of parameters, we skip off to gpdefl, which is short for ‘getparm default’.
1864: 68 gpfprm PLA ; Get parameter from stack
1865: 24 6C BIT getpmsk ; current parm = word?
1867: 10 02 BPL gpis8 ; no, skip high byte store
1869: 95 01 STA 1,X ; * store high byte if word
186B: 68 gpis8 PLA ; get low byte or default value
186C: 95 00 STA 0,X ; store where requested
In a moment whose reason eludes me, I called this branch point gpfprm. What this section does is first pull the high byte of the next parameter from PROMAL off the stack and then check the parameter type mask to see if it’s a byte type. If so (BPL, as bit 7 would be a zero its plus or positive), it skips to gpis8, discarding the byte. If it’s a word, it gets stored to X+1 by using a base of 1 instead of 0.
Gpis8 always pulls and stores the byte to the location indicated by X.
This method is clever: The routine first loads the default value from the argument block into memory, and then only loads a value if there’s one available on the stack. It’s a good way of ensuring a default is in place if it’s not specified by PROMAL.
186E: 06 6C gpdefl ASL getpmsk ; shift parameter bit mask
1870: 4C 47 18 JMP getpl ; loop
The argument mask is shifted one to the left to ensure it stays in sync with the argument index in Y. Then, the loop is restarted.
1873: 98 getpfin TYA ; transfer index to A
1874: 18 CLC ; pre for math
1875: 65 3A ADC Parent ; Add our parent's return address
1877: AA TAX ;
1878: A5 3B LDA Parent+1 ;
187A: 69 00 ADC #$00 ;
187C: 48 PHA ; and put on stack
187D: 8A TXA ;
187E: 48 PHA ; for rts.
As we don’t want to return into a data block, we’ll add our current value for Y to the parent’s calling address and put it on the stack. This ensures we safely RTS into the byte following the argument table.
187F: A5 6D LDA GParent ; get grandparent's address
1881: 85 3A STA Parent ; place where p-code expects parent's
1883: A5 6E LDA GParent+1 ;
1885: 85 3B STA Parent+1 ;
1887: 60 RTS ; and return to arg table+1
And as a last bit of cleanup, the grandparent’s address is placed where our parent would expect it to be, leaving the runtime in a good state and keeping the stack clear.
How BLKMOV used this routine
BLKMOV used this routine to set up all of its zero page vectors. Once getprm is done, the routine looks largely like MOVSTR, so I’ll (probably) cover it later.
Here’s what getprm did for BLKMOV. I’ll include the first part of BLKMOV again, with a bit better formatting since we know what the data following the JSR is for.
21a9 20 15 18 JSR $1815 ; jsr to getprm
.byte $33 ; min/max number of parameters
.byte $e0 ; %1110 0000 - all three parameters are words
.byte $38 ; first word stores at $38 and $39
.byte $34 ; second word stores at $34 and $35
.byte $2c ; third word stores at $2c and $2d
When getprm runs for BLKMOV, it performs these actions:
* Writes the last parameter (Count) to $38 and $39
* Writes the second parameter (From) to $34 and $35
* Writes the first parameter (To) to $2c and $2d
* Cleans house and returns to the byte following the argument table at 21b1
This might look a little familiar. MOVSTR uses the same vectors.
The routine is very handy in that you can easily specify what you need loaded, as well as quickly specifying how many parameters you require and how many you can take. The limit for the number of parameters is sensibly eight, given the mask argument is a byte providing 8 flags for parameter types.
The getprm routine is used by many routines in the PROMAL system, including (but not limited to) GETC, GETL, BLKMOV, OPEN, CLOSE, CHKSUM, and EDLINE.