4

I have written the following program in debug.exe:

1165:0100  mov ah, 09
1165:0102  mov dx, 107
1165:0105  int 21
1165:0107  db 'Hello World', 0A, 0D, '$'
1165:0115  jmp 1165:0100

However, when I execute it, it stops working after printing "Hello World" once. What I want it to do is to keep printing "Hello World" repeatedly, in a loop.

I also tried jmp 0100 but it also didn't work.

10
  • 1
    If you want to explore debug, I strongly recommend getting a hold of Rex Last's MS-DOS Revealed — it’s a pretty good introduction to assembly-language programming using debug. Alternatively, check out any of the free assemblers; NASM is pretty good and has good documentation. Commented Jun 7, 2017 at 11:30
  • @stephen I've actually been looking for a good reference for debug.com, and I hadn't heard of the book you suggested, so I was going to purchase a copy. Unfortunately, used copies on Amazon.com seem to go for $268. Do you know of another source? Or is the text online somewhere, by chance? Commented Jun 7, 2017 at 12:47
  • 1
    Not sure though what's going on here... you are writing a long jump (by specifying the segment, explicitly even which would require the program binary being loaded at some particular physical address), but it's looking like DEBUG turns it into not just a near jump by discarding the segment reference, but also a near relative jump based on the fact that only two bytes are used to encode the instruction. (That would be one byte for the near relative jump instruction itself, and one byte for the offset.) A near jump would IIRC require three bytes: one instruction byte plus 16 bits address.
    – user
    Commented Jun 9, 2017 at 11:59
  • 1
    Just a heads-up to let you know that the standard line terminator in MS-DOS (and later Windows) is CR LF (0x0D followed by 0x0A). Your screenshot shows them being swapped. It will work (sort-of), but you will confuse some text editors if you output this to e.g. a file.
    – 9Rune5
    Commented Jun 18, 2017 at 10:38
  • 3
    Also pay heed to Michael's comments on short jumps. You cannot presume which segment DOS loads your little program into. Next run it could be somewhere completely different. Far jumps (jumps to other segments) requires more forethought. As Michael says, it looks like debug turned your jump into a short jump, and I too am a bit surprised it did so using only two bytes (my guess is that the x86 instruction set has a convenient jmp instructions for jumps that are less than 128 bytes away, a little disassembly and google will reveal the answer)
    – 9Rune5
    Commented Jun 18, 2017 at 10:38

3 Answers 3

36

Your code runs through the data ("Hello World" string) interpreting it as (nonsense) machine code - and very probably crashes before it even reaches the jmp instruction. You need to move the jmp instruction to before the string.

4
  • 7
    What actually happens is that the processor tries to execute the data, as if it were code. You surely already know this, but your answer could be improved by making that more explicit. Note that @user5161 can see what his code disassembles to in debug.com by typing -u 100. You'll see that the "string" data is turned into a bunch of nonsense instructions. Commented Jun 7, 2017 at 12:57
  • 3
    More specifically, DOS programs don't exactly 'crash' in the sense that the OS has no provisions for terminating ill-behaving programs. What actually happens is that there is JMP 0119 instruction in the resulting code.
    – void_ptr
    Commented Jun 7, 2017 at 18:31
  • 6
    @void_ptr Well, they typically crash in a much more spectacular way than on modern OSs. "Segmentation Fault. Core dumped" is really a boring show compared to a slight fizzling from the monitor that tells you a program gone astray has just readjusted your video frequencies ;)
    – tofro
    Commented Jun 7, 2017 at 21:35
  • I've seen print-screen functions on some other systems (e.g. the Commodore 128 kernel) which use code equivalent to pop si / cld / loop: lodsb / cmp al,endOfString / jz done / [output byte somehow] / jmp loop / done: jmp [si]. Functions designed in that way should have string data "in-line" with code. The DOS function in this question, however, is not designed that way.
    – supercat
    Commented Jun 3 at 15:17
18

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:

A>DEBUG.COM
-r
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
-a 100
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
114c:0117
-

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,[0095]
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):

-u F000:A095
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?

6
  • 6
    "There is no x86 instruction that starts out with a 65H byte". 65h is the prefix to indicate that any address operand in the instruction is relative to the GS segment register (386 and above only). DEBUG doesn't decode it because it doesn't understand any extensions to the instruction set beyond the original 8087 additions (there are extended versions of DEBUG out there that do understand this, including the version that comes with FreeDOS, however). 65 6C decodes to GS INSB which is a rather meaningless instruction, but would be understood by the processor to be just the same as INSB.
    – Jules
    Commented Sep 6, 2017 at 1:16
  • 1
    Disassembling with ndisasm rather than debug gives the following: mov ah,0x9 / mov dx,0x107 / int 0x21 / dec ax / gs insb / insb / outsw / and [bx+0x6f],dl / jc 0x7d / or cl,[fs:di] / and al,0xeb / jmp word 0x6a50
    – Jules
    Commented Sep 6, 2017 at 1:24
  • 1
    @Jules Interesting, thank you. Without analyzing the code in detail, it still does look rather nonsensical (as would be expected for data being treated as code). I have plans to update this answer later, and will see what I can do about incorporating your information then.
    – user
    Commented Sep 6, 2017 at 5:39
  • 1
    Since this is an 8088-tagged question, all the talk about gs registers and illegal opcode interrupts would seem to be moot. The 8088 had no such beasts and would have either treated them as nop or done something weird. Even treating them as nop wouldn't help since there's a perfectly legal jb 017d and jmp 6b50 in the "pipeline". So it almost certainly jumped to a code location that sent it "off with the fairies", as we used to say.
    – paxdiablo
    Commented Feb 26, 2022 at 9:43
  • "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." ===[ Would that be on purpose, even in a primitive computer? It just halted or hung the user's computer on purpose? Wouldn't it have a default "general error" interrupt vector at FF or somewhere that at least wrote to the screen like a BIOS code? That's terrible! Commented Jun 3 at 13:20
2

My approach: Throw more debuggers at it

As already answered, having the data in the path of control flow will interpret the data as nonsense code causing a crash.

The core reason for my answer is that I want to point out how your far jump was assembled into a short jump. This is a long-standing bug in MS-DOS Debug, which I believe affects all versions of MS-DOS Debug's line assembler. I fixed this bug in my fork called MSDebug, which is based on the MS-DOS v2 free software release of 2018. The 2024-04-16 changeset is titled "fix, assemble CALL/JMP ssss:oooo as far even if CS == ssss".

This is the latest build of MSDebug with the bug: https://pushbx.org/ecm/download/old/msdebug/20240110.zip

This is the oldest build with the bug fixed: https://pushbx.org/ecm/download/old/msdebug/20240416.zip

Here's how they behave with your input. First, the revision with the bug:

E:\testms>20240110.COM
MSDebug release 1 by ecm
-a
07B2:0100  mov ah, 09
07B2:0102  mov dx, 107
07B2:0105  int 21
07B2:0107  db 'Hello World', 0A, 0D, '$'
07B2:0115  jmp 07B2:0100
07B2:0117

Note that the length of the assembled instruction is already visible from the line assembler's prompts before and after entering the jmp instruction. The only jmp instruction that takes up two bytes is a short jump, encoded with the machine opcode 0EBh plus a rel8 byte. The rel8 means it is a signed 8-bit relative address, encoding a jump destination in the -128 to +127 range relative to the offset after the instruction.

Disassembling the generated "code" in MSDebug yields the same result as others have pointed out, the debugger's disassembler encounters 186+ and 386+ opcodes that it does not recognise:

-u 100 l 17
07B2:0100 B409          MOV     AH,09
07B2:0102 BA0701        MOV     DX,0107
07B2:0105 CD21          INT     21
07B2:0107 48            DEC     AX
07B2:0108 65            DB      65
07B2:0109 6C            DB      6C
07B2:010A 6C            DB      6C
07B2:010B 6F            DB      6F
07B2:010C 20576F        AND     [BX+6F],DL
07B2:010F 726C          JB      017D
07B2:0111 64            DB      64
07B2:0112 0A0D          OR      CL,[DI]
07B2:0114 24EB          AND     AL,EB
07B2:0116 E90410        JMP     111D

As before, an instruction starting at 0115h is not found because of how the instruction boundaries appear to the disassembler.

More interestingly we can ask the debugger to disassemble just the jump instruction that we assembled last. This reveals that (as for all official MS-DOS Debug) the jmp imm:imm instruction was actually assembled into a short jump:

-u 115 l 1
07B2:0115 EBE9          JMP     0100
-q

Now the same input to the fixed revision of the debugger:

E:\testms>20240416.COM
MSDebug release 1 by ecm
-a
07B3:0100  mov ah, 09
07B3:0102  mov dx, 107
07B3:0105  int 21
07B3:0107  db 'Hello World', 0A, 0D, '$'
07B3:0115  jmp 07B3:0100
07B3:011A
-u 100 l 1A
07B3:0100 B409          MOV     AH,09
07B3:0102 BA0701        MOV     DX,0107
07B3:0105 CD21          INT     21
07B3:0107 48            DEC     AX
07B3:0108 65            DB      65
07B3:0109 6C            DB      6C
07B3:010A 6C            DB      6C
07B3:010B 6F            DB      6F
07B3:010C 20576F        AND     [BX+6F],DL
07B3:010F 726C          JB      017D
07B3:0111 64            DB      64
07B3:0112 0A0D          OR      CL,[DI]
07B3:0114 24EA          AND     AL,EA
07B3:0116 0001          ADD     [BX+DI],AL
07B3:0118 B307          MOV     BL,07
-u 115 l 1
07B3:0115 EA0001B307    JMP     07B3:0100
-q

Now we actually assembled a far jump as intended!


For a "jump" type branch the change from a jmp imm:imm to jmp rel8 arguably could be "intended" as an optimisation. If you never run this instruction from a different segment than what it was assembled to, and you do not patch or inspect the instruction, then it will act the same way as the expected far jump. However, this falls flat very much for "call" type branches. call ssss:oooo should be a far call, pushing both cs and ip, unlike call oooo which should only push ip.

Further, I have a concept called roundtrip which means that the disassembler output should, if fed to the assembler, produce the same machine code as was originally disassembled. With the bug, far immediate jumps are disassembled in the expected way but entering the same text as input to the assembler may fail to assemble to the same machine code.

The bugfix involves checking explicitly for the ssss: form rather than just comparing the obtained address to the current assembly segment to detect a far immediate branch.


To throw yet another debugger at it, here is the most recent revision of my FreeDOS Debug/X fork, lDebug. The FreeDOS Debug/X descendants do all support 186 and 386 level disassembly, so the output may be more useful:

E:\testms>ldebug
lDebug (2024-06-04)
-a
2B03:0100  mov ah, 09
2B03:0102  mov dx, 107
2B03:0105  int 21
2B03:0107  db 'Hello World', 0A, 0D, '$'
2B03:0115  jmp 2B03:0100
2B03:011A
-u 100 l 1A
2B03:0100 B409              mov     ah, 09
2B03:0102 BA0701            mov     dx, 0107
2B03:0105 CD21              int     21
2B03:0107 48                dec     ax
2B03:0108 65                seg     gs (unused)
2B03:0109 6C                insb
2B03:010A 6C                insb
2B03:010B 6F                outsw
2B03:010C 20576F            and     [bx+6F], dl
2B03:010F 726C              jb      017D
2B03:0111 640A0D            or      cl, [fs:di]
2B03:0114 24EA              and     al, EA
2B03:0116 0001              add     [bx+di], al
2B03:0118 032B              add     bp, [bp+di]
-u 115 l 1
2B03:0115 EA0001032B        jmp     2B03:0100
-

As is evident lDebug shares the correct interpretation of jmp 2B03:0100 as a far immediate jump.

And once more with the AFORMAT Extension for lDebug loaded:

E:\testms>ldebug
lDebug (2024-06-04)
-ext aformat.eld install
AFORMAT installed.
-a
2B03:0100  mov ah, 09
          B409
2B03:0102  mov dx, 107
          BA0701
2B03:0105  int 21
          CD21
2B03:0107  db 'Hello World', 0A, 0D, '$'
          48656C6C6F20576F726C640A0D24
2B03:0115  jmp 2B03:0100
          EA0001032B
2B03:011A  .
-

(The larger segment for the debuggee process is because lDebug uses much more memory than MSDebug.)

2
  • I was not aware of this peculiar behavior of MS-DOS DEBUG.EXE. The 'auto-optimization' for jmp seg:off and call seg:off that DEBUG does only happens when seg is equal to the current segment. For the far jump the replacement should always be fine, even if the program later is loaded to another segment. But for the far call, the replacement is very wrong as somewhere down the line the programmer will have written a retf that desperately needs two words on the stack.
    – Sep Roland
    Commented Jun 4 at 21:34
  • Yes, that is precisely what I meant about the possible intention. However, I disagree with your "even if the program later is loaded to another segment". If you relocate a far immediate jump 0EAh it will still point to the same far address. If you relocate a near or short jump it will not point to the same far address as before. So even then there is unexpected breakage. I'm fairly confident this behaviour is a bug.
    – ecm
    Commented Jun 4 at 23:19

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .