61

In the Sierra On-Line game "Leisure Suit Larry 2" there's a part in the game where the main character (Larry) has to write a program in 8088 assembly language as part of his tribal initiation. Larry then jokes that he's written a multi-user operating system called "Eunuchs".

Animated screenshot showing the scene from the game

The code shown in the game is:

INT     21
JB      0103

MOV     AH,4D
INT     21
CS:

MOV     [0BEA],AX
JMP     02B2
JMP     1451

CS:
TEST    BYTE PTR [0C59],01
JZ      0150

CS:
TEST    BYTE PTR [0C59],02
JZ      014F
IRET

INT     21
JB      0103

TEST    BYTE PTR [0C59],04
JZ      0169
CMP     AH,01
JA      014F

I haven't run this code as I presume it probably isn't complete, but does anyone know what it might do?

8
  • 8
    It seems to be nonsense. Some of the int 21h calls don't specify the call, and AH = 4D requests the result of an exec call which hasn't been made. There's a spurious IRET in the middle of it. The jump and branch destinations are meaningless without more context. The protagonist can't have written a "multiuser OS" in 20 lines using MS-DOS calls. It's the code equivalent of a panel with fake leds and gauges. Commented Feb 24 at 23:42
  • 16
    Most people here may already know this, but “Eunuchs” is a pun on UNIX, which is an actual multi-user operating system (but it never ran natively on an 8088). And UNIX was, itself, a pun - it was envisioned as a simpler version of MULTICS. Commented Feb 25 at 1:40
  • 10
    @JacobKrall Unix was of course available for the 8088PC/XT. Even trice, first as V7 based Venix/86, then as System III based Xenix 3 Release 1 by SCO and last as System V based PX/ix by IBM (developed by Interactive Systems). All in the 1983/1984 time frame and each under genuine AT&T licence.
    – Raffzahn
    Commented Feb 25 at 4:11
  • 4
    @JacobKrall memory protection is a good thing, not just for multi user, even on single user/single tasking like DOS. Still, it's just a nice add on, not necessary for anything. Also, Unix does not need multiple users either.
    – Raffzahn
    Commented Feb 25 at 12:08
  • 13
    Being Leisure Suite Larry, I'm somewhat surprised it's not 80085 Assembly language!
    – Jon P
    Commented Feb 26 at 3:07

3 Answers 3

88

In a brief email conversation with Al Lowe (yes, the man himself!) he suggested that he probably used the COMMAND.COM file as the basis for this code as it would have been installed on every PC.

Booting up a vanilla copy of MS-DOS(R) Version 3.30 and using the debug tool on the COMMAND.COM file shows the following disassembled code:

-u 12d 160
1133:012D CD21          INT     21
1133:012F 72D2          JB      0103
1133:0131 B44D          MOV     AH,4D
1133:0133 CD21          INT     21
1133:0135 2E            CS:
1133:0136 A3EA0B        MOV     [0BEA],AX
1133:0139 E97601        JMP     02B2
1133:013C 2E            CS:
1133:013D F606590C01    TEST    BYTE PTR [0C59],01
1133:0142 740C          JZ      0150
1133:0144 2E            CS:
1133:0145 F606590C02    TEST    BYTE PTR [0C59],02
1133:014A 7403          JZ      014F
1133:014C E90213        JMP     1451
1133:014F CF            IRET
1133:0150 2E            CS:
1133:0151 F606590C04    TEST    BYTE PTR [0C59],04
1133:0156 7411          JZ      0169
1133:0158 80FC01        CMP     AH,01
1133:015B 72F2          JB      014F
1133:015D 80FC0C        CMP     AH,0C
1133:0160 77ED          JA      014F

Accepting some variation in the byte code due to differences between MS-DOS versions, I think for all practical purposes this is fairly close to what's seen in the game.

6
  • 8
    It's even go as far and say it's exactly that code. Making a lot more sense with the JMP 1451 being located before the IRET (at 014Ch). So I'd say that line got misplaced (or on purpose moved) when the DEBUG output was copied into the program. Likewise all jump addresses are going to meaningful locations. Not being present in DOS 1 or 2 makes sense considering that for DOS 3 large parts were rewritten. Lovely. Task well solved.
    – Raffzahn
    Commented Feb 27 at 23:07
  • 2
    So it's DOS. Not Eunuchs. How disappointing. :-| Commented Feb 28 at 4:19
  • 1
    As far as I can tell, this is partly code that collects the return code after launching an executable (well, obviously), followed by an initial fragment of the ISR for the Ctrl+C interrupt (0x23). Commented Feb 28 at 7:37
  • Why not pick your own answer as accepted? Seriously, you deserve it. Commented Feb 28 at 11:06
  • 1
    @user3840170 I think the answer that I accepted is still the best one, in the sense that I wanted to know what the code did rather than where it was from. Your answer was very relevant also because I used the byte searches to confirm the origin. Thank you for all your edits, btw. Commented Feb 28 at 11:18
49

It's most definitely not a program 'written' by a programmer, but rather a listing of some random program segment using the disassembly command of a debugger.

Have you tried to find that code within the program?

Let's See What We Got

Hard to say anything definitive with only this (incomplete) section. A few details visible are:

01: INT 21         ; An unknown (*1) call to DOS, function code would have been set prior to this line
02: JB 0103        ; Jump to address 0103h if DOS returns NO error
03: MOV AH,4D      ; Get return code from DOS (*1)
04: INT 21
05: CS:            ; (*2)
06: MOV [OBEA],AX  ; Save return information within the program code at offset 0BEAh
07: JMP 02B2       ; Continue at 02B2h
08: JMP 1451       ; Continue at 1451h (*3)
09: CS:            ; (*4)
10: TEST BYTE PTR [0C59],01 ; Test the lowest bit of the byte at 0C59h within code 
11: JZ 0150        ; Continue at 0150h if not set (*6)
12: CS:
13: TEST BYTE PTR [OC59],02 ; Test the second lowest bit 
14: JZ 014F        ; Continue at 014Fh if not set (*6)
15: IRET           ; Return from Interrupt to terminate the program(*5,*6)
16: INT 21         ; Again a an unknown DOS call (*7)
17: JB 0103        ; Continue at 0103h if no error reported (*8)
18: TEST BYTE PTR [0C59],04 ; Test the third lowest bit
19: JZ 0169        ; Continue at 0169h if not set
20: CMP AH,01      ; check AH for 01h (*7)
21: JA 014F        ; If AH was greater than 01h continue at 014Fh (*9)

*1 - Since the second INT 21h (L:04), made when an error is returned, asks DOS for a program's return code, the previous DOS call (L:01) was most likely an EXEC call to run a different program as child process. Using this function also tells that the program was written/compiled for MS-DOS 2.0 or later.

*2 - A hint that the debugger used was most likely the one supplied with MS-DOS.

*3 - Line 08 might be a target from a previous branch, one like the one on L:02. Such would be a typical compiler structure to continue from a branch to a destination more than 128 bytes away.

*4 - An entry point, like *2, except here operation continues

*5 - Using an IRET here is a strong indicator that the program may be a .COM program and this being used to terminate it.

DOS prepares the memory of a COM program much like CP/M, including a way that a jump at address 0000h will terminate it. This is done by placing it's original termination interrupt INT 20h at the start of the PSP - which is at CS:0 for COM programs - and pushing a word of 0000h onto the stack.

Now a simple RET NEAR can be used to terminate. This of course only works if CS doesn't get changed inbetween. A condition true for most simple programs, especially such converted from 8080 code. To still use that way of termination while possibly changing CS during program run it was common for COM programs to start by pushing CS and another word of 0000h onto the stack. Now a RETF could used, no matter what content CS had at the moment (and as long as all stack levels were cleaned up).

EXE programs added a little issue as their initial CS is not pointing to the PSP. But DS is loaded in both types with the PSP address, so pushing DS instead of CS would make the code usable for both types of program.

Since that 'trick' would leave a a single word on stack (the one provided by DOS to start with), it became common to use an IRET instead of a RET FAR, which does take that word as well off the stack. Not that it made a difference, but it felt better :))

*6 - It's extreme hard to do any guess without knowing the address that code is located. But having two jumps targeted only one byte apart means that either some (for x86) unusual program trick is used, or 014Fh containing a single byte instruction. Considering that an IRET/INT combination is rather unusual and that IRET is a single byte instruction, it could quite well be that it's address is 014Fh. But then again the previous JZ wont make much sense, as either way would lead to taking the IRET.

*7 - Checking AH after DOS functions is rare, as the standard return code is contained in AL (AX). Get Return code (see L:03/04) in turn does use AH and AL different. AL contains the child return code, while AH contains the termination reason. Here 00h/01h are regular termination reasons, while 02h is a critical error abort. Checking for above 01h would make sense if the previous INT 21h was as well an EXEC call.

*7 - Note this being he same target as used on line 02, which seems to be the default good exit.

*8 - Note this being he same target as used on line 14


Conclusions From Code

The code snipplet is most likely part of an exec function that starts a child program and checks if the execution went well and continues afterwards. It might thus be some menu system/framework starting overlay modules for different tasks (game sections??). It seems to be written in some High level language.

Also, it's most likely not from DOS or any OS level software, but user space, as it used INT 21h functions.

Using the IRET termination hints to some really old code with DOS 1.x origin, or a compiler using that trick to save code size if here are multiple termination points. It must be an older one, as MS discouraged use of returning via PSP:0 and INT 20h in general with DOS 3

Taking into account that the target for 'good' execution (L:02/L:17) points to a very low address within this code, this could be an endless loop starting around the loading and executing a child process. Together with the fact that it checks flags within its own code segment after returning from the child process to decide how to continue, I could imagine this being a loader for a larger program, whose only job is to reload that program whenever it got normally terminated, unless some flags have been set by the child before termination.

A program like this could be a simple solution to catch certain errors without adding lots of error checking code within the application. Any error detection would just exit the child, which then gets restarted from above code.

Note: These are all wild guesses, not necessary true. There is no way to tell where it's originated, unless one finds exactly that code in some program.

Now, What is It?

Nothing. I'd say they just started up their debugger, loaded some program, dumped a random section of 21 lines and copied them into the game text. Could be from the game or any other program.

4
  • Comments have been moved to chat; please do not continue the discussion here. Before posting a comment below this one, please review the purposes of comments. Comments that do not request clarification or suggest improvements usually belong as an answer, on Retrocomputing Meta, or in Retrocomputing Chat. Comments continuing discussion may be removed.
    – Chenmunka
    Commented Feb 27 at 21:52
  • 14
    @Chenmunka Moving these comments doesn't seem like a great move as they contained a lot of follow up information (Like Al Lowes own reply). Those actions seem to be quite random and not really targeting off topic comments. Might be better for all to keep a more hands off approach.
    – Raffzahn
    Commented Feb 27 at 22:41
  • 3
    It's tricky on this site as commeting happens more than on other SE sites and usually does have lots of fascinating (if not always directly relevant) info - comment threads more than 20 comments long get flagged by the system automatically and standard practice is to move them. I try and review each one individually, I've chosen to leave some in the past while migrating others. I didn't see these and maybe would have left them, but the system does make moving them the easiest and default option.
    – Matt Lacey
    Commented Feb 28 at 10:25
  • @MattLacey Yeah, process is a beast and tools supporting a certain beast makes it howl :) I can't judge about RC.SE being different from other sites, but it feels as if the community is quite self regulating with strong voices on all opinions. So in general less moderation might work out as well better as on other sites.
    – Raffzahn
    Commented Mar 4 at 12:28
18

The asker’s own answer almost definitively establishes where the code came from: it is an edited disassembly of the command interpreter of MS-DOS 3.30 obtained with MS-DOS’s bundled DEBUG debugger. (Which is more-or-less what I guessed initially before it was posted.) Opcode dumps and memory addresses were removed (probably to save screen space, and to make it look more like typed-in source code and not a disassembly listing), and some instructions were shuffled around and deleted (maybe to make the code more visually impressive, or maybe to disguise its origin and avoid copyright claims). No other edits were made: the characteristic code style choices of DEBUG were preserved. Even the number of spaces between the mnemonic and operands matches, which is especially visible if you extract raw string data from the game with the help of ScummVM.

original disassembly                  code shown in the game
═════════════════════════════════════════════════════════════════════════
INT     21     ──┬──────────┬───────▶ INT     21
JB      0103   ──┘ (⁰)      │         JB      0103
                            │
MOV     AH,4D               │         MOV     AH,4D
INT     21                  │         INT     21
CS:                         │         CS:
                            │
MOV     [0BEA],AX           │         MOV     [0BEA],AX
JMP     02B2                │         JMP     02B2
                            │  ┌────▶ JMP     1451
                            │  │
CS:                         │  │      CS:
TEST    BYTE PTR [0C59],01  │  │      TEST    BYTE PTR [0C59],01
JZ      0150                │  │      JZ      0150
                            │  │
CS:                         │  │      CS:
TEST    BYTE PTR [0C59],02  │  │      TEST    BYTE PTR [0C59],02
JZ      014F                │  │      JZ      014F
JMP     1451   ────────────────┘  ┌─▶ IRET
IRET           ─────────────│─────┘
                            └───────▶ INT     21
                                      JB      0103
CS:     ──────────────────────────┐
TEST    BYTE PTR [0C59],04        │   TEST    BYTE PTR [0C59],04
JZ      0169                      │   JZ      0169
CMP     AH,01    ┌────────────────┤   CMP     AH,01
JB      014F   ──┤             ┌────▶ JA      014F
CMP     AH,0C  ──┘             │  │
JA      014F   ────────────────┘  ▼
                                  🗑️ (discarded)

The shuffling of the instructions and removal of context changes the meaning of the code sufficiently for it to be no longer very meaningful or useful, and in particular where it came from; even if it still remained realistic-looking (at least at first glance). Even the way it was presented is at times a bit nonsensical: the CS: segment-override prefix appears in a separate dialogue box from the MOV [0BEA],AX instruction it modifies, even though they are very tightly coupled. It’s clear the code was meant to be mere filler text, and not an Easter egg for players to decipher. It’s not like the Spanish encountered earlier in the game is any more realistic.

Screenshot of the game, with a speech box at the top saying “Have zeee nice dayo.”, translated below as “Have a nice day.”

But what did the code do where it came from? It turns out the original disassembly was actually of two unrelated pieces of code that just happened to be placed near each other:

  • From the INT 21 up to the JMP 02B2 (offset range [0x12d, 0x13b] in the loaded execuable image), it is code that COMMAND.COM uses to spawn a subprocess corresponding to the entered command with the “exec” syscall, and then collect the exit code from it using the “wait”1 syscall.

    If you study the binary, you may notice that there is no code to set up register values near the first INT 21 instruction. This is no accident. The code which sets up those values is located in the transient (volatile) portion of COMMAND.COM, which may get overwritten after a program is loaded; once the transient portion sets up registers, it jumps to the permanent portion (which doesn’t get overwritten) to actually invoke the syscall, so that execution can continue from a safely allocated address.

  • The rest of the code, starting with the CS: segment-override prefix (offset +0x13c in the loaded image), is the beginning of the Ctrl-C handler installed by COMMAND.COM. It checks the state of registers at entry and of COMMAND.COM internal flags in order to determine how to service the event. Some cases are handled with an early return, which is why there is an IRET instruction present (which would be pretty puzzling in any other context).

Could this have been found without asking the original author? With a little effort, probably. Some of the instructions don’t have an unambiguous binary encoding; others, like the conditional branch opcodes, use a relative address, so recovering their binary code from disassembly using absolute addresses would have required knowing the address where they appeared, which wasn’t given. So searching for the original binary by reversing the disassembly would have been by no means trivial. But a few instructions are pretty characteristic and unlikely to be found spuriously: the longest instruction with an unambiguous encoding is (CS:) TEST BYTE PTR [0C59], xx which corresponds to the byte sequence (2E) F6 06 59 0C xx. Meanwhile, the first six instructions can be guessed to be CD 21 72 ?? B4 4D CD 21 2E A3 EA 0B, which also matches the COMMAND.COM executable. We would still have been pretty lucky nonetheless; if the code had come from a much more obscure source, like a program internal to Sierra, it might have been outright impossible to find.


0 This snippet appears only once in the original code, and in game data, but is displayed twice.

1 RBIL does not use this name, but MS-DOS did. It was one of many "Eunuchs" inspirations in MS-DOS 2.

4
  • Thank you for your answer and suggestions for byte searches. The closest match I could find for your CD 21 72 ?? B4 4D CD 21 2E A3 EA 0B byte string was 5F 5E 72 04 B4 4D CD 21 E9 C6 E7 00 which was in the install.exe file. This is in a segment of code that shortly after has copyright information for the "MS Run-Time Library". Commented Feb 25 at 14:52
  • 1
    @NoelWhitemore That’s not even close, actually. B4 4D CD 21 is just MOV AH,4D / INT 21, which is almost bound to appear somewhere if a DOS program spawns subprocesses at all, but it isn’t particularly specific. Commented Feb 25 at 15:21
  • @user3840170 I agree it's not close, but that's the closest match I've found in either of the executable files to part of your string. The other strings I couldn't match at all. Commented Feb 25 at 15:38
  • 5
    @user3840170 I heard back from Al. He thinks it might be from the COMMAND.COM file shipped with MS-DOS. I found a match for CD 21 72 D2 B4 4D CD 21 2E A3 early on in a random COMMAND.COM file from MS-DOS 3 I just downloaded off of archive.org so that looks promising. Commented Feb 27 at 0:29

You must log in to answer this question.

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