All right, I was curious what actually happened when you tried to execute that code. Because it crashes, obviously something is wrong, but what exactly is wrong.
After some digging, I finally found a machine on PCjs.org which both boots to a usable command line, and has sufficient hardware to be able to run DEBUG with good results. Start at the Windows 95 build 499 machine, and let it boot to a command prompt. By default, it will boot the disk image "PC-DOS 3.30 (Disk 1)". It will ask you for the current date and time; just press Enter at both prompts. You are now in IBM PC-DOS 3.30.
Then, load the "PC-DOS 3.30 (Disk 2)" disk image into drive A:. That one contains, among many other things,
DEBUG.COM. You can now start DEBUG and provide your source code (in my case, DEBUG is using segment 114C; yours may be different). I'm also doing a CPU register dump at the beginning, so that we know what we have to work with:
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=114C ES=114C SS=114C CS=114C IP=0100 NV UP EI PL NZ NA PO NC
114C:0100 255300 AND AX,0053
114C:0100 mov ah,09
114C:0102 mov dx,107
114C:0105 int 21
114C:0107 db 'Hello World', 0a, 0d, '$'
114C:0115 jmp 114c:0100
OK, so let's see what that turns into:
-u 100 117
114C:0100 B409 MOV AH,09
114C:0102 BA0701 MOV DX,0107
114C:0105 CD21 INT 21
114C:0107 48 DEC AX
114C:0108 65 DB 65
114C:0109 6C DB 6C
114C:010A 6C DB 6C
114C:010B 6F DB 6F
114C:010C 20576F AND [BX+6F],DL
114C:010F 726C JB 017D
114C:0111 64 DB 64
114C:0112 0A0D OR CL,[DI]
114C:0114 24EB AND AL,EB
114C:0116 E9376A JMP 6B50
Not quite what we intended, is it? The first few instructions are fine, but by CS:0107H
DEBUG goes off on a tangent, and never recovers. Notice that it doesn't think there is any instruction beginning on offset 0115, which is where our jump to the beginning of the program was supposed to be. But sure enough, if we look specifically for it...
-u 115 117
114C:0115 EBE9 JMP 0100
114C:0117 37 AAA
...there's our jump, in plain sight.
I'm assuming here that DEBUG's understanding of 8086 machine code is a decent approximation of the 8086/8088 CPU's Execution Unit, which is likely a reasonable assumption to make. (Its purpose, after all, was as a simple debugger and machine code monitor.)
This code will execute just fine up through offset 0107, moving past the supposed
DEC AX. Says Ralph Brown's Interrupt List, upon return from INT 21H AH=09H, AL=24H (probably mostly an accident of the implementation, and not a deliberate choice), so at that point, decrementing AX is a perfectly valid operation even not accounting for wraparound. However...
There is no x86 instruction that starts out with a 65H byte. (See for yourself.) That's also why DEBUG would display it as a plain data byte (
DB) until it again finds something that it recognizes as a valid instruction. In this case those valid instructions are nonsensical, but that's just because DEBUG is trying to interpret as code what in reality is data.
When the CPU encounters this illegal code byte (remember, the CPU is still in the middle of executing code; it has no knowledge of the fact that what's coming up was data intended for human consumption, and happily does the best it can), it triggers interrupt vector 06H,
#UD (undefined opcode or sometimes invalid opcode -- this one seems appropriately schizofrenic). Exactly what happens at that point depends on your environment, but you can have a look at its address to see where the CPU will jump. Interrupt vectors are stored as 32-bit segment:offset pairs starting at address 0000H:0000H for vector 00H. Vector 06H is thus at bytes 0000H:0018H through 0000H:001BH. Let's look at what's there:
-u 0000:0018 001B
0000:0018 A09500 MOV AL,
0000:001B F0 LOCK
0000:001C D01B RCR BYTE PTR [BP+DI],1
Ignore the disassembly; that's meaningless here. The important part is the data, which in this case turns out to be
A09500F0. Let's look at memory address F000H:95A0H (remember that x86 CPUs store integers in reverse-byte order, and that the segment offset is stored at a lower offset than the segment, so we need to effectively read the address bytewise backwards):
F000:A095 2E CS:
F000:A096 8B5404 MOV DX,[SI+04]
F000:A099 89560A MOV [BP+0A],DX
F000:A09C 8166160100 AND WORD PTR [BP+16],0001
F000:A0A1 33C0 XOR AX,AX
F000:A0A3 EB14 JMP A0B9
[remainder snipped for brevity]
At F000H:A0B9H, there is more meaningful-looking code. You can inspect it yourself if you want to (it's just a
-u f000:a0b9 away) but that doesn't really matter, because what PCjs does with an invalid opcode may or may not be representative of what actual hardware did with it. I'm guessing that, on real hardware, if you were to perform the same investigation, you'd find that the interrupt vector points toward a
HLT instruction, or possibly a jump-to-self, with the intent of freezing the computer from the user's point of view. Simply, by the time you get that far, something has gone horribly wrong, and in the simple days of DOS systems with no memory protection or anything similar, there wasn't a lot that could be done at that point to meaningfully recover. Stopping the system to make sure that no further harm is done, then, is a reasonable course of action.
And that is why your program stops working.
Easy as pie, no?