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.
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.
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
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.
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
BX:CX, and store the sign-extended argument to this function in
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
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
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.
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.
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
13DB:3A5F C3 RET
Return to caller
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
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.
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.
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
0x180055plus 200 equals
CS:3A30will loop until
CS:DC16returns a value greater than or equal to
CS:DC16can never return a value greater than
CS:3A30will 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.