Saturday, 25 April 2026

Recovering Sinclair QL App DataSpaces

Recently I've been playing with a QL emulator again, because I found a printed copy of my third year Computer Science dissertation, and wanted to retype it in the default word processor, QUILL.

However, I couldn't run QUILL, because when the program was copied to my Mac's file system it didn't copy the header, which contains the data and stack space allocated to the program. And that's part of how the QL works, executable files contain meta-data providing this information and it gets lost on modern operating systems including Linux and Windows.

In theory, fixing it is as easy as reserving memory for the program (progCode=RESPR(sizeOfFile)), then loading the code (lbytes fileName,progCode), then re-saving it under a different name: (sexec_w newFileName,progCode,dataSpace). But this means finding out how much data space has been allocated for static data and the stack. And... this information isn't generally available, even though many QL owners have had to face the problem.

The nearest I got was a web site which contained a BASIC program which could tweak QUILL for a few features I didn't care about (see the section called QUILL Mod). But at the end it did sexec_w QUILL with an actual data space which worked. I was then able to use QUILL to type in the first couple of pages of my dissertation, which was fun.

This wasn't a solution for all the other programs I could run on my QL emulator, e.g. Forth79! Amazingly though I found an intriguing program on one of my QL directories on my Mac called: HeadRead_bas. This turned out to be a machine code program and hexloader for it, which would then save the machine code in a file. Could this be it? Here's the program:

100 CLS:RESTORE:READ space:start=RESPR(space)
140 PRINT 'loading hex..':endAddr=hex_load(start)
150 INPUT 'save to file';f$
160 SBYTES f$,start,endAddr-start
170 STOP
180 DEFine FuNction dec(h$):RETurn h$(1) INSTR "0123456789ABCDEF"-1:END DEFine
190 DEFine FuNction hex_load(start)
195 LOCal sum,addr
200 PRINT 'Data entered at:',start
220 sum=0:addr=start
230 REPeat load_hex_digits
240 READ h$:IF LEN(h$)<>2*INT(LEN(h$)/2) THEN PRINT "Odd Hex digit Count";h$:STOP
300 FOR b=0 TO LEN(h$) STEP 2
360 byte=16*dec(h$(b+1))+dec(h$(b+2)):POKE addr,byte
370 sum=sum+byte
380 addr=addr+1
390 END FOR b
400 END REPeat load_hex_digits
410 READ check
420 IF check=sum then print "Sum OK":else print "Bad Sum"
430 RETurn addr
490 END DEFine
500 DATA 144
510 DATA '43FA000A34790000','01104ED20002001E'
520 DATA '0747657448454144','0010075365744845'
530 DATA '4144000000000000','784660027847BBCB'
540 DATA '675A2A0D4BEB0008','3479000001124E92'
550 DATA '664C3031E80054AE','0058264D2A45C0FC'
560 DATA '0028D0AE0030B0AE','00346C2C2A360800'
570 DATA '6B26347900000118','4E9266225343661C'
580 DATA '2031E80008000000','6612204522407440'
590 DATA '766420044E434E75','70FA4E7570F14E75'
600 DATA '*',10007

The program doesn't read QL program headers, it just creates the machine code file you can then use in another program to read headers. And that program was elsewhere in the same directory too:

10 hdrMod=RESPR(144):LBYTES mdv1_GetSetHead_bin,hdrMod:CALL hdrMod
100 BUFFER=RESPR(64)
120 INPUT 'ENTER DEVICE:';F$
130 OPEN #3,F$
140 GetHEAD #3,BUFFER
150 PRINT F$;', ';PEEK_L(BUFFER);' BYTES'
160 PRINT 'LAST ALTERED ';DATE$(PEEK_L(BUFFER+52))
170 PRINT 'CURRENT DATA SPACE ';PEEK_L(BUFFER+6)
210 CLOSE #3

It loads in the machine code first. It turns out the machine code adds the command GetHEAD to BASIC. GetHEAD reads the header from a file at the given channel and stores it in an allocated buffer. Then we can look at offsets in the file for the actual size of the executable and the data space.

This solves part of the problem: I now had a program which could read the headers. However, all the reported dataspace values were reported as 0. Fortunately, I still have my real Sinclair QL and a floppy disk system which is still largely reliable! I could either look for the same BASIC programs on a floppy disk, or type it out by hand again. Indeed, the programs were on floppy disk too!

Now I was able to list all the data spaces for the executable files I had. It turns out that all the PSION programs for version 2.3 (though Easel is version 2.0) had a data space of 1280 bytes. So, then I could get all of them to work! Mostly I used QUILL and the Spreadsheet, ABACUS. I used the ARCHIVE database a bit and EASEL very little.

The rest can be summarised in this scrappy table:

Program                Size DataSpace
Computer One Assembler: ASSEMB 18094 256
Computer One Editor: EDITOR 12714 256
Computer One Linker:LINKER 4278 256
And Linker_A (??): LINKER_A 8616 4800
Debugger: debug_exc 2272 500
eda         13653 256
eye_q_dp         31476 43008
forth79         12616 57528

You might like to know what the assembly code for Header read is? I disassembled it using the Alan Giles disassembler written in BASIC. It's slow, about 1 or 2 lines per second but good enough for this.

3FF00 43FA000A               LEA     $000A(PC)=$3FF0C,A1
3FF04 347900000110           MOVE.W  $00000110,A2
3FF0A 4ED2                   JMP     (A2)
3FF0C 0002001E               OR.B    #$1E,D2
3FF10 0747                   BCHG    D3,D7
3FF12 6574                   BCS.S   $74(PC)=$3FF88
3FF14 4845                   SWAP    D5
3FF16 4144                   DC.B    'A','D'
3FF18 0010                   DC.B    0,16
3FF1A 0753                   BCHG    D3,(A3)
3FF1C 6574                   BCS.S   $74(PC)=$3FF92
3FF1E 4845                   SWAP    D5
3FF20 4144                   DC.B    'A','D'
3FF22 00000000               OR.B    #$00,D0
3FF26 0000                   DC.B    0,0
3FF28 7846                   MOVEQ   #$46,D4
3FF2A 6002                   BRA.S   $02(PC)=$3FF2E
3FF2C 7847                   MOVEQ   #$47,D4
3FF2E BBCB                   CMP.L   A3,A5
3FF30 675A                   BEQ.S   $5A(PC)=$3FF8C
3FF32 2A0D                   MOVE.L  A5,D5
3FF34 4BEB0008               LEA     $0008(A3),A5
3FF38 347900000112           MOVE.W  $00000112,A2
3FF3E 4E92                   JSR     (A2)
3FF40 664C                   BNE.S   $4C(PC)=$3FF8E
3FF42 3031E800               MOVE.W  $00(A1,A6.L),D0
3FF46 54AE0058               ADDQ.L  #2,$0058(A6)
3FF4A 264D                   MOVE.L  A5,A3
3FF4C 2A45                   MOVE.L  D5,A5
3FF4E C0FC0028               MULU    #$0028,D0
3FF52 D0AE0030               ADD.L   $0030(A6),D0
3FF56 B0AE0034               CMP.L   $0034(A6),D0
3FF5A 6C2C                   BGE.S   $2C(PC)=$3FF88
3FF5C 2A360800               MOVE.L  $00(A6,D0.L),D5
3FF60 6B26                   BMI.S   $26(PC)=$3FF88
3FF62 347900000118           MOVE.W  $00000118,A2
3FF68 4E92                   JSR     (A2)
3FF6A 6622                   BNE.S   $22(PC)=$3FF8E
3FF6C 5343                   SUBQ.W  #1,D3
3FF6E 661C                   BNE.S   $1C(PC)=$3FF8C
3FF70 2031E800               MOVE.L  $00(A1,A6.L),D0
3FF74 08000000               BTST    #$00,D0
3FF78 6612                   BNE.S   $12(PC)=$3FF8C
3FF7A 2045                   MOVE.L  D5,A0
3FF7C 2240                   MOVE.L  D0,A1
3FF7E 7440                   MOVEQ   #$40,D2
3FF80 7664                   MOVEQ   #$64,D3
3FF82 2004                   MOVE.L  D4,D0
3FF84 4E43                   TRAP    #$3
3FF86 4E75                   RTS
3FF88 70FA                   MOVEQ   #$FA,D0
3FF8A 4E75                   RTS
3FF8C 70F1                   MOVEQ   #$F1,D0
3FF8E 4E75                   RTS
3FF90 0000                   DC.B    0,0

The section hilighted in yellow is actually the information passed to SuperBASIC for the new command names and their syntax. In a future edit I hope to annotate it better.

Anyway, armed with this information you too, can go back to your old, actual QL and work out the data spaces for all the executables you couldn't otherwise run on your QL. Feel free to add them in comments!


No comments: