Possible bug in H89LDR2.ASM NOP sled

42 views
Skip to first unread message

B 9

unread,
Jan 2, 2026, 5:18:43 AM (7 days ago) Jan 2
to se...@googlegroups.com

Howdy Heathfolk,

I was examining the source code for H89LDR2.ASM (version 9) and was wondering about a couple oddities I found in the ALGNR2 loop, which shifts the program if it gets loaded into a slightly incorrect memory address.

        ORG 2329H       ; MATCHES WITH BOOTLDR
        NOP
        NOP
        NOP
        NOP
        DI
        LXI SP, SPINT
        LXI H, MAINPAD-1
        LXI B, BUFFER-MAININT+104H
        NOP
ALGNR1:
        NOP
        NOP
        NOP
        INX H
        MOV A, M
        ORA A
        JZ ALGNR1       ; FIND FIRST CODE
        LXI D, BUFFER-MAININT  ; COUNT TO MOVE
        DAD D   ; H POINTS TO END OF SHIFTED CODE
        LXI D, BUFFER   ; END OF CODE
        NOP
ALGNR2:
        NOP
        NOP
        NOP
        MOV A, M
        STAX D  ; MOVE IT UP
        DCX D
        DCX H
        DCR C
        JNZ ALGNR2      ; 256 BYTES?
        DCR B           ; CAUTION DOESN'T WORK RIGHT FOR
                        ;  SIZES OF EVEN 100H
        JNZ ALGNR2      ; ALL DONE?
        JMP MAININT
MAINPAD:
        NOP
        NOP
        NOP
        NOP
        NOP
        NOP
MAININT:

The first oddity is simple: When the loop is decrementing BC there’s a comment that says it doesn’t work for sizes of even 100H. Why doesn’t the loop use the standard 16-bit counter idiom that is seen elsewhere in the same program? For example,

        DCX B
        MOV A,B
        ORA C 
        JNZ ALGNR2

The second is more perplexing: BC is the number of bytes to copy as it relocates the program. From my naive view, it looks like it ought to be simply

        LXI B, BUFFER-MAININT         ; BC = Total bytes to move. BUFFER is the end of this program.

but instead the code adds 104H (260) more bytes:

        LXI B, BUFFER-MAININT+104H

The problem I see with that (and please correct me if I’m wrong) is that adding those extra bytes means that if the program actually is offset in memory, instead of correcting itself and continuing, it will overwrite its own loop, causing it to crash. For example, if it is off by 1 byte, instead of JNZ to ALGNR2 at 2349H, it will jump to 4949H.

So a few questions:

  1. Am I reading this correctly?
  2. Why does this program even attempt to move itself into the right place? The only possibility I can imagine would be that, if someone ran BOOTSTRP.OCL before connecting the serial port, they might cause a few spurious characters when they plugged it in. Did line noise used to be a more serious problem back in the 1980s?
  3. Why was that mysterious 104H constant added? How long has it been there?
  4. If I'm right and it wasn’t working anyway, would it be reasonable to remove all of the self-shifting code? I was thinking about extending H89LDR2, but would want the BIN file to still be 818 bytes so BOOTSTRP.OCL and H89TRANS.SEQ do not need to change.

—b9

glenn.f...@gmail.com

unread,
Jan 2, 2026, 9:35:35 AM (7 days ago) Jan 2
to se...@googlegroups.com

Dwight still subscribes to this list so he’s probably the best one to answer these questions. I can tell you that the code is well proven and definitely works.  Because he has logically connected two sets of code (the boot loader and the larger H89LDR) there are some hard wired numbers that make the code hard to understand and quite fragile if any changes are made to either piece.

 

Have you not been able to make the code work?  can you give more details?

 

  • Glenn

--
You received this message because you are subscribed to the Google Groups "SEBHC" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sebhc+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/sebhc/CAG1tPes6w4be_L_owEMrkYEkgrJyA9YUf7PVA-mXjoDKDkzQxg%40mail.gmail.com.

dwight

unread,
Jan 3, 2026, 3:14:00 PM (6 days ago) Jan 3
to se...@googlegroups.com
I'm not sure about the 100H issue but as I recall, the program was intended to over write itself in order to terminate the move operation and move into the next operation. Clearly not normal code but that was the intended operation. If you follow the byte by byte operation you will see that the order should work. 
Dwight


From: se...@googlegroups.com <se...@googlegroups.com> on behalf of glenn.f...@gmail.com <glenn.f...@gmail.com>
Sent: Friday, January 2, 2026 6:35 AM
To: se...@googlegroups.com <se...@googlegroups.com>
Subject: RE: [sebhc] Possible bug in H89LDR2.ASM NOP sled
 

B9

unread,
Jan 3, 2026, 6:00:56 PM (6 days ago) Jan 3
to se...@googlegroups.com
There are two self-modifying parts. I should have clarified, the code works fine as long as BOOTSTRP loads the next stage into the right place in memory  -- H89LDR2 correctly overwrites the last byte of BOOTSTRP, causing execution to flow into it. (A very nifty trick!)

The second self-modifying part is in ALGNR2, where it realigns the MAININT code if BOOTSTRP received spurious characters. That's where I'm seeing possible problems.

ALGNR1 calculates the offset correctly. ALGNR2 is supposed to move the program at MAININT-offset to MAININT. It starts the copy at the end of the program, copying from BUFFER-offset (HL) to BUFFER (DE), both of which are set correctly.

However the number of bytes copied (BC) is not the same as the length of the program (BUFFER - MAININT), as one would expect. Instead that value is increased by 260, which would cause the loop in ALGNR2 to overwrite itself and jump to a random location in memory... But only if there is any misalignment.

If there is no spurious character, the code is not aligned and it just copies itself in place. It's basically a NOP, which is why the program  works.

Or at least that's what I think. Am I  misunderstanding something?

--B






--
You received this message because you are subscribed to the Google Groups "SEBHC" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sebhc+un...@googlegroups.com.

B 9

unread,
Jan 4, 2026, 3:07:46 AM (5 days ago) Jan 4
to se...@googlegroups.com

A-ha! I figured it out. Remember how I said there were two oddities? Turns out they cancel each other out!

Because ALGNR2 is decrementing BC as two separate 8-bit counters, B and C, and then checking against zero for each, it had to increase B by 1. It did that by adding 104H to BC, which is the same as B=B+1, C=C+4. So, the alignment program does work.

But now I have other questions.

  1. Why is it increasing C by 4? I can see that there is a 4 byte NOP sled at the beginning of the program (2329H) which gives the program room to be offset by 5 bytes and still work. But increasing C by 4 shouldn’t help with that, right? It only makes 4 extra NOPs be copied before MAININT (2361H) starts. The comment about needing to not be divisible by 100H also doesn’t seem to be relevant since the size from MAININT to BUFFER is 2FAH.

  2. Why doesn’t ALGNR2 decrement BC using DCX and then check if it is zero by ORing B and C? That technique is used elsewhere in the H89LDR2 program.

—b9

dwight

unread,
Jan 4, 2026, 1:12:32 PM (5 days ago) Jan 4
to se...@googlegroups.com
Your new questions are not in my memory. I realize there are often several alternate ways to get to the same result. As I recall, I was targeting 8080 instructions, rather than Z80 instructions. Mostly because of my lack of experience with Z80 instruction set.
 The clearing of possible bytes in the serial chip's buffer was not done by the init if the serial chip. I wanted to minimize the number of bytes that had to be entered by hand. I think I tried to remove anything that wasn't absolutely needed. As I recall, one of the serial chips could buffer more than a single byte. I think the number 4 was chosen to account for that buffer. I could be wrong about this but I think that was what. I could have ofset by any larger number so maybe it was just an orbitrary amount ( brain fade ). I do think it may have been an aritrary amount. 
Do remember, the hole concept of bootstraping a system is tricky. I think I'd have done it differently if I better understood the parts of the disk system, better.
It was a long time ago and at 77 years old, remembering specifically why I did this or that has faded some.
Dwight

From: se...@googlegroups.com <se...@googlegroups.com> on behalf of B 9 <hack...@gmail.com>
Sent: Sunday, January 4, 2026 12:07 AM
To: se...@googlegroups.com <se...@googlegroups.com>
Subject: Re: [sebhc] Possible bug in H89LDR2.ASM NOP sled
 

glenn.f...@gmail.com

unread,
Jan 4, 2026, 1:20:03 PM (5 days ago) Jan 4
to se...@googlegroups.com

So I think you’ve got it mostly figured out. BC gets loaded with 003.376 (0x03FE) and the copy loop pre-decrements B so it goes through three iterations. The first one copies 254 bytes, the next one copies 256 bytes and the third one copies 256 bytes for a total of 766 (0x02FE or 002.376) bytes. This is four more than BUFFER-MAININT (002.372) but the four extra bytes are just null (NOP). I’m still a little bit confused about the purpose of the NOPs but they’re there for a reason.  E.g. on my system when I run the program the code from START through MAININT-1 is mislocated by 2 bytes, but the NOPs save the day.

 

Relocatable code is generally an issue with the 8080 as it doesn’t have relative jumps like most other processors do. Dwight has done some clever coding to make this all work, and the NOP padding is a little insurance against minor misalignments (though I still haven’t figured out why that occurs).  For truly relocatable code (e.g. HDOS itself or device drivers) HDOS attaches a patch table to the code, listing each address that has to be patched when the code is loaded.  But Dwight’s code is designed as a stand-alone utility so it can’t take advantage of this.

 

Not sure why 16 bit decrement (DCX B) wasn’t used but you could experiment with that and might learn.

 

Have you got the code working? Were you able to save it to a bootable disk?

B 9

unread,
Jan 8, 2026, 1:18:25 AM (yesterday) Jan 8
to se...@googlegroups.com

Thank you, Glenn, those numbers match mine. The NOPs allow for misalignment caused by any characters received by BOOTSTRP before H89LDR2 is sent over.

At first I thought the spurious characters must have been from line noise, but now I suspect it was part of the design of the H89TRANS program. Looking at the Forth code in H89TRANS.SEQ, the PC sends 1 byte handshakes to H89LDR2 running on the H89. If you try to use a command (such as S[ave]) before H89LDR2 is actually loaded, you’ll send a byte to BOOTSTRP, causing the offset problem which the NOP sled could compensate for. I don’t expect either line noise or people accidentally hitting the wrong key was a common enough issue for such a heavy duty solution. Perhaps the intention was to be able to check if H89LDR2 was running and automatically load it if necessary?

On the other hand, as I think about it, you wouldn’t need self-relocating code to do that as H89TRANS already does the reverse. When the user tries to send H89LDR2 to an H89, in a touch of genius H89TRANS doesn’t send a byte to probe for H89LDR2 as that would cause an offset if BOOTSTRP is running. Instead, it sends the H89LDR2 file and checks after every byte if the H89 replied with a ‘?’, indicating that H89LDR2 is already loaded. In a similar way, you could check if H89LDR2 is running and automatically load it if it isn’t there: Just send the last byte of H89LDR2 (a NULL) to the H89. If the H89 responds with ‘?’, then H89LDR2 is alive. If not, it’s safe to send over the rest of H89LDR2 (in reverse order, as BOOTSTRP requires).

So, in short, I’m still confused about why it was necessary to have H89LDR2 reposition itself in memory. It’s very cool as I’ve never come across code like that before. Was that just the way things were done back then?

Oh, and I have a new question: Where can I read details about how HDOS loads itself into memory? It seems to be doing some tricky relocation, similar to H89LDR2. The serial port .ABS loader program I wrote seems to work correctly, but even the simplest programs make syscalls to HDOS for output. I’m curious if I could create .ABS files which load up the necessary bits and pieces of HDOS over the serial port. I’m running into a four issues:

  1. Most ABS files load up fine, but when I load up HDOS.SYS using my ABS loader, it seems to load into memory okay, but it corrupts something and my program crashes back to the H: prompt as soon as it re-enables interrupts. It looks like MTR-90 is using a chunk of memory (around 2032H) for its stack space, but that’s right in the middle of where HDOS.SYS’s file header says it should load up (1C04H to 356AH). I’m not even sure how HDOS.SYS is supposed to be executed as the “ENTRY ADDRESS” (where the PC should be set after loading) points to a memory location (15C3H) which is outside of that range.

  2. I can see that having HDOS routines in memory is not enough as one also has to set up interrupt vectors. I tried simply writing to 38H (70A) with my own JMP, but the memory didn’t change even after I unprotected the lower 8K using OUT 7FH, 80H. What do I google for to find out how to configure those?

  3. Where can I read about the format of “patch tables”? I might need to be able to parse them in order to load .ABS files directly over serial. I’ve noticed that in the .ABS files I’ve extracted (using diu) there is often extra data after the nominal end given by the length in the .ABS header. Is that the “patch table”? [Coincidentally, earlier today I read an article by Glenn from the early 1980s about how to squeeze HDOS down to the bare minimum by deleting the “seldom used patch tables,” among other techniques.]

  4. I was looking for a place in memory that I could stash my ABS loader program that would be sure to not get trampled by any .ABS file it was loading. Right now I’m using 1400H to 17FFH, which is supposed to be for the floppy drive. It works fine there on my machine, but I wonder if that is the case for all machines. Is that RAM actually on the H17 card, so a machine without that card wouldn’t be able to write to that memory region? If that location isn’t consistently available on other machines, what would be a good alternate location?

Thanks, and sorry for the long ramble. I’m having fun learning about my Heathkit!

—b9

glenn.f...@gmail.com

unread,
Jan 8, 2026, 9:10:08 AM (yesterday) Jan 8
to se...@googlegroups.com

You certainly have had a good “education” looking at this code (and the Forth companion piece). That’s great.  It’s an interesting application.

 

Here’s what I think is going on and why the code relocation is required. As we’ve seen there is some “slop” in the location of the code when it is first downloaded. It loads itself at 2329H so that execution will begin as soon as it overwrites the PCHL instruction in the boot program, however, given the slop, Dwight had to put in some NOP lines around any labeled location subject to a CALL or JMP (see START, ALGNR1, ALGNR2).  He could have done that for the whole H89LDR2 program, which would be tedious and messy. Instead, he did it only for the small portion that relocates the code. Once the code is relocated to a known correct location all the JMP and CALL statements in the program will thereafter be in good shape.

 

From your second question I wonder if you’re aware that the full source code for HDOS was published and is preserved on our sites?  See https://sebhc.github.io/sebhc/documentation.html#HEATHKIT_SOURCE_LISTINGS

 

Back in 2019 I worked with David Troendle to convert most (maybe we did all? Not sure) of these PDF listings into assemblable source code. That work resides in BitBucket:

https://bitbucket.org/HeathkitGuy/sebhcsoftware/src/master/HDOS/HDOS2.0/

 

If you look at the “BootCode” directory you’ll see the relocatable portions of HDOS. Look at the end of the listing for HDOS.SYS (starting at line 5869) and you’ll see the relocation table. This is a list of 2-byte addresses that need to be patched based on the load point for the code. The end of the list is denoted by a 000.000 entry. Remember these are stored in “little endian” format so reverse the bytes:

https://bitbucket.org/HeathkitGuy/sebhcsoftware/src/master/HDOS/HDOS2.0/Volume1/BootCode/HDOS.LST

 

so:

102.146  214 051 115 06149    END

            052 146 052

            200 052 206

            052 214 052

            222 052 241

            052 274 052

            277 052 307

 

Is actually:

051.214

052.115

052.146

Etc.

 

And if you look at the code listing you’ll see those are references to locations in memory. E.g. 051.214 is the location of SUNIT, 052.115 is the location of SYSCAL, etc.

 

So how does HDOS know if a file is relocatable?  The first byte of any absolute file must be 0xFF. For standard .ABS files the second byte will be zero. For a relocatable file the second byte will be 1. See PICDEF.ACM

https://bitbucket.org/HeathkitGuy/sebhcsoftware/src/master/HDOS/HDOS2.0/ACM/PICDEF.ACM

 

As for the code that implements this, you’ll have to scan through the HDOS source to find it.

 

With regard to Patch Tables: this was a well-intentioned exercise in overkill (IMHO). You had to have a patch “key” to implement any system patches. I don’t know too much more about them. Stan Webb developed a patch to PATCH so that you don’t need a magic key to use it:

https://sebhc.github.io/sebhc/documentation/software/supplemental/HDOS_2.0_System_Y2K_Date_Patch.pdf

 

as I said in my “losing weight” REMark article, the patch tables can generally be stripped off without any issue.

 

not sure how to answer your .ABS loader question. I’d have to better understand what you’re trying to accomplish. You’re correct that the memory at 1400H (024.000) is the 1K space that normally maps to the H17 RAM (the RAM chips are on the H17 controller board).  I’m not sure what breaks if you use that as general memory. Obviously if you tried to use the H17 things would get messy. A safer location is 020.000 (1000H), but there’s only RAM there if you have an ORG0 configuration (and the RAM is enabled).  If I understand correctly what you’re doing, the 1K H17 space seems like an OK place to locate your code, so long as it doesn’t call any of the H17 routines.

 

You may want to look at the Sector 0 boot code. This is the code that is on track zero of every initialized H17 disk and it is essentially the boot loader for HDOS.  The code is embedded in INIT (search for S0BOOT):

https://bitbucket.org/HeathkitGuy/sebhcsoftware/src/master/HDOS/HDOS2.0/Volume1/System/INIT.LST

Reply all
Reply to author
Forward
0 new messages