Since it’s a holiday week, I’m just going to do a quick followup to a loose end from last week. In that post, I hypothesized that the function located at CS:3A30
in my run-time copy of the Neuromancer executable existed solely to create a delay for the user. Today, I’d like to test that hypothesis by examining some more assembly code.
Assembly code
If one disassembles the machine code at CS:3A30
(e.g. with DOS DEBUG – see the last several posts for more details on how to locate and disassemble particular fragments of code at run-time using this tool) one finds the following short sequence of instructions:
13DB:3A30 55 PUSH BP
13DB:3A31 8BEC MOV BP, SP
13DB:3A33 83EC04 SUB SP, +04
13DB:3A36 E8DDA1 CALL DC16
13DB:3A39 8BC8 MOV CX, AX
13DB:3A3B 8B4604 MOV AX, [BP+04]
13DB:3A3E 8BDA MOV BX, DX
13DB:3A40 99 CWD
13DB:3A41 03C1 ADD AX, CX
13DB:3A43 13D3 ADC DX, BX
13DB:3A45 8946FC MOV [BP-04], AX
13DB:3A48 8956FE MOV [BP-02], DX
13DB:3A4B E8C8A1 CALL DC16
13DB:3A4E 3B56FE CMP DX, [BP-02]
13DB:3A51 7F09 JNLE 3A5C
13DB:3A53 7C05 JL 3A5A
13DB:3A55 3B46FC CMP AX, [BP-04]
13DB:3A58 7302 JNB 3A5C
13DB:3A5A EBEF JMP 3A4B
13DB:3A5C 8BE5 MOV SP, BP
13DB:3A5E 5D POP BP
13DB:3A5F C3 RET
We can quickly analyze these instructions.
Stack setup
13DB:3A30 55 PUSH BP
13DB:3A31 8BEC MOV BP, SP
13DB:3A33 83EC04 SUB SP, +04
Setup the stack frame, by storing the previous frame’s Base Pointer on the stack, setting the current frame’s Base Pointer to the current Stack Pointer, and allocating 4 bytes of local memory on the stack, addressable as [BP-04]
through [BP-01]
.
Mystery function
13DB:3A36 E8DDA1 CALL DC16
Call the function at CS:DC16
, passing no arguments to it. At the moment, the significance of this function is not clear.
DWORD shuffling
13DB:3A39 8BC8 MOV CX, AX
13DB:3A3B 8B4604 MOV AX, [BP+04]
13DB:3A3E 8BDA MOV BX, DX
13DB:3A40 99 CWD
Store the 32-bit return value of CS:DC16
(passed, in the C calling convention, in DX:AX
) in BX:CX
, and store the sign-extended argument to this function in DX:AX
.
DWORD addition
13DB:3A41 03C1 ADD AX, CX
13DB:3A43 13D3 ADC DX, BX
13DB:3A45 8946FC MOV [BP-04], AX
13DB:3A48 8956FE MOV [BP-02], DX
Add BX:CX
to DX:AX
, and store the 32-bit result in LSB order at [BP-04]
. The DWORD at [BP-04]
is now equal to the DWORD return value of CS:DC16
plus the sign-extended WORD argument to this function.
Mystery function (redux)
13DB:3A4B E8C8A1 CALL DC16
Another call to CS:DC16
.
Test (part 1)
13DB:3A4E 3B56FE CMP DX, [BP-02]
13DB:3A51 7F09 JNLE 3A5C
Jump to the end of this function iff the high WORD of the new CS:DC16
return value is greater than the high WORD calculated in the DWORD addition step.
Test (part 2)
13DB:3A53 7C05 JL 3A5A
Jump to the bottom of the current loop (for, as we’ll soon see, we’re in a loop) iff the high WORD of the new CS:DC16
return value is less than the high WORD calculated in the DWORD addition step.
Test (part 3)
13DB:3A55 3B46FC CMP AX, [BP-04]
13DB:3A58 7302 JNB 3A5C
Jump to the end of this function iff the low WORD of the new CS:DC16
return value is greater than or equal to the low WORD calculated in the DWORD addition step.
Loop
13DB:3A5A EBEF JMP 3A4B
Loop back to the CS:DC16
call at CS:3A4B
– busywait, basically. This instruction is reached iff the most recent CS:DC16
return value (passed in DX:AX
) is less than the DWORD stored at [BP-04]
. Assuming that the CS:DC16
function returns some sort of timer, this behaviour is consistent with our hypothesis that this function exists simply to create a delay.
Stack teardown
13DB:3A5C 8BE5 MOV SP, BP
13DB:3A5E 5D POP BP
Restore the caller’s stack frame, by setting the Stack Pointer to this frame’s Base Pointer, and then popping the caller’s Base Pointer off
the stack.
Return
13DB:3A5F C3 RET
Return to caller
CS:DC16
At this point, the only question left is whether or not the CS:DC16
function returns some sort of measure of the current time. Let’s check:
13DB:DC16 2BC0 SUB AX, AX
13DB:DC18 CD1A INT 1A
13DB:DC1A 8BC2 MOV AX, DX
13DB:DC1C 8BD1 MOV DX, CX
13DB:DC1E C3 RET
This code invokes the 00h
subfunction of the 0x1A
interrupt, and moves its 32-bit return value from CX:DX
to DX:AX
, for return to the caller. According to Ralf Brown’s interrupt list, this interrupt returns the number of clock ticks since the most recent midnight in CX:DX
– there are 0x1800B0
ticks per 24hr period.
Conclusions
CS:3A30
is, indeed, a delay function, creating a pause in processing approximately equal to its argument, divided by 20, in seconds. It’s interesting to note that this function contains a game-killing hazard: If invoked shortly before midnight, the function can enter an infinite loop.
The CS:DC16
function on which CS:3A30
relies returns values between 0 and 0x1800AF
. If the sum of the argument to CS:3A30
and the return value of the first call to CS:DC16
are greater than 0x1800AF
, then this function will wait for CS:DC16
to return a value that it never will.
For instance, if this function is invoked 5s before midnight, with a 200 tick (~11s) delay:
- The first call to
CS:DC16
will return0x180055
0x180055
plus 200 equals0x18011d
CS:3A30
will loop untilCS:DC16
returns a value greater than or equal to0x18011d
CS:DC16
can never return a value greater than0x1800AF
- Therefore,
CS:3A30
will never return
This is unlikely to happen in practice, but it’s interesting in theory. (For some value of “interesting”.)
Merry Christmas, and a Happy New Year.