or, Losing your mind with PROMAL
Learning how things work
In a recent experiment in learning to work with PROMAL, I needed a method for moving pieces of data around in memory to split strings. The MOVSTR library procedure seemed ideal, but consistently missed the mark and corrupted memory.
As it turns out, I had an addressing issue as well as a misunderstanding of what the procedure will do for me.
For our reference, I’ll quote the MOVSTR proc’s documentation from the Library Manual.
PROC MOVSTR COPY OR JOIN STRINGS OR EXACT SUBSTRING
USAGE: MOVSTR FromString, ToString [,Limit]
MOVSTR is a procedure which is used to copy strings, to concatenate strings, or extract substrings (i.e., replaces the LEFT$, MID$, and RIGHT$ functions found in BASIC). FromString is the address of the string to copy. ToString is the address of the destination. Limit is an optional argument specifying the maximum number of characters to copy.
This brings up some useful syntax in PROMAL: Specifying the address of a string. In my project, I needed to extract the middle of a string and deposit into another variable. My first attempt used this method:
movstr buf, name, 16
A person would think then, movstr would copy from buf to buf into name, but this was not the case. After some deep debugging of the PROMAL library routine itself, I learned that I was in fact telling PROMAL to use the address at buf and buf as the source for the string to put into name. This is an inconsistency in addressing that was learned: When I specify ‘movstr buf,name,16’ it will use the location of buf, but if I use ‘movstr buf,name,16’ it instead uses a vector placed at buf. To fix this issue, use the # operator to specify ‘the address of…’:
movstr #buf, name, 16
The alternate format tells the compiler to use the address of buf instead of a vector at the same location.
Learning inner workings through an assembly debugger
Disclaimer: Most 8-bit fans will balk at using an emulator to develop programs for their beloved 8-bit systems. I do heavily prefer to develop on the machine itself, but there’s little that beats a debugging system that will stop the system cold: video refresh, hardware timers, everything gets paused. As I was having trouble doing local development, I moved my data to the Vice emulator and got to work.
The MOVSTR function
In the library, the definition for MOVSTR is ‘EXT ASM PROC MOVSTR AT $F33’. This tells the compiler that MOVSTR is a procedure that can be called directly in memory at location $0f33. In my particular installation of Vice, Alt-H opens the debugger, pausing the emulation. A quick look at f33 will show that it’s part of a jump table:
(C:$f33e) d f33 .C:0f33 4C 38 22 JMP $2238 .C:0f36 4C 18 22 JMP $2218 .C:0f39 4C DF 26 JMP $26DF
Of course, the only real interest is the first instruction: jmp $2238. Let’s have a look there.
Exploratory surgery (or, finding out how PROMAL thinks)
At $2238 is a fairly straightforward routine. For reference, the code below is being called by my test program after things are in working order as that’s the only debug log I saved. There’s still a lot to learn!
Here’s the processor registers when the call is made: a=3 x=3 y=3 sp=f7
First, promal saves the calling address in a scratch space so it can return to the caller:
2238 68 PLA 2239 85 3A STA $3A 223b 68 PLA 223c 85 3B STA $3B
Now, we can examine the stack and prepare something ahead of time:
223e A9 FF LDA #$FF 2240 C0 03 CPY #$03 2242 D0 03 BNE $2247
At this point, I recognize the #03: Movstr can have two or three parameters, so apparently the Y register holds the number of parameters for the function. I specified three in my application, so this falls through to the next instruction:
2244 68 PLA 2245 68 PLA 2246 88 DEY 2247 85 38 STA $38
At first, this confused me greatly. Why would you pull two bytes from the stack without saving the first? As it turns out, the third parameter is only supposed to be a byte, rather than a word. However, the compiler apparently always pushes words to the stack. The first PLA simply pulls the unused high byte of the word and discards it. DEY is a setup for the next compare below:
2249 C0 02 CPY #$02 224b F0 03 BEQ $2250 224d 4C 80 10 JMP $1080
Here’s the second check. Remember, movstr can have two or three parameters. Here, Y is checked to see if it’s 2. If it is, the jmp is skipped. For reference, $1080 is a runtime error routine. I checked by entering ‘go 1080’ in the promal executive. PROMAL replied with this:
*** RUNTIME ERROR: ILLEGAL # ARGS, LIB. CALL AT $C3F3 *** PROGRAM ABORTED.
Continuing to $2250, the routine then gathers more information:
2250 68 PLA 2251 85 35 STA $35 2253 68 PLA 2254 85 34 STA $34 2256 68 PLA 2257 85 2D STA $2D 2259 68 PLA 225a 85 2C STA $2C
At this point, the MOVSTR routine has everything set up for the routine below. The [limit] was processed early on, and now the [tostring] and [fromstring] parameters are stored in zero-page as well. Tearing apart the actual copy routine is beyond the scope of this post, but I’ll include it for reference.
225c A5 34 LDA $34 225e 38 SEC 225f E5 2C SBC $2C 2261 AA TAX 2262 A5 35 LDA $35 2264 E5 2D SBC $2D 2266 D0 1F BNE $2287 2268 8A TXA 2269 C5 38 CMP $38 226b B0 1A BCS $2287 226d A0 00 LDY #$00 226f B1 2C LDA ($2C),Y 2271 F0 0A BEQ $227D 2273 C8 INY 2274 C4 38 CPY $38 2276 90 F7 BCC $226F 2278 A9 00 LDA #$00 227a F0 03 BEQ $227F 227c 88 DEY 227d B1 2C LDA ($2C),Y 227f 91 34 STA ($34),Y 2281 C0 00 CPY #$00 2283 D0 F7 BNE $227C 2285 F0 15 BEQ $229C 2287 A0 00 LDY #$00 2289 A5 38 LDA $38 228b F0 0D BEQ $229A 228d B1 2C LDA ($2C),Y 228f 91 34 STA ($34),Y 2291 F0 09 BEQ $229C 2293 C8 INY 2294 C4 38 CPY $38 2296 90 F5 BCC $228D 2298 A9 00 LDA #$00 229a 91 34 STA ($34),Y
Remember how we started? We stored the return address at $3a so we could examine the parameters on the stack. To return, an internal routine is then run, which does the work of putting the calling routine back on the stack and returning:
229c 4C 69 20 JMP $2069 [at $2069] 2069 A5 3B LDA $3B 206b 48 PHA 206c A5 3A LDA $3A 206e 48 PHA 206f 60 RTS
Lesson learned? The [limit] parameter for MOVSTR has a maximum value of 255, and one has to be very careful about how the parameters are specified. We don’t have the luxury of a memory protection unit that modern systems have, so an incorrectly specified parameter can cause the whole environment to be overwritten at random.
Also, if you looked carefully at the entire routine, you’ll notice that the copy will stop on the first null ($00) byte it finds. As it’s a ‘string’ move rather than a block move, it makes sense considering PROMAL uses ‘ascii-z’ strings.
I also got a good chance to see how exactly the PROMAL compiler passes its data to procedures via the stack. What I learned is confirmed in the promal language indexes, specifically the section on calling external assembly functions and procedures.