Andrew wrote:There seems to be no way to turn off all sounds in PoP1.
Unless you're using the PC speaker.
Andrew wrote:For example, is the joystick bug you mentioned above 1.4 or DOSBox's fault?
There is a "JoyX: %2d JoyY: %2d Btn: %2d" string in PRINCE.EXE. And the code in fact refers to that string.
Andrew wrote:I look forward eagerly to the results of your investigation.
Here it is: (very long!)
First I added memory-change breakpoints on the version string (BPLM 00019502 and BPLM 00019500).
The breakpoint was triggered by this code:
Code: Select all
01A3:140B 5E pop si
01A3:140C 0000 add [bx+si],al ; <-- overwritten!
01A3:140E 3E2843A7 sub ds:[bp+di-59],al
01A3:1412 7407 je 0000141B ($+7)
Those two statements look very suspicious, so I compared the code with the contents of the EXE:
Code: Select all
.0000140B:5E pop si
.0000140C:CB retf ; <-- original!
.0000140D:803E2843A7 cmp [+4328],A7
.00001412:7407 je .0000141B
Ah! Something writes zero to 01A3:140C, that is 1A30+140C=2E3C. So I set a memory-change breakpoint there (BPLM 00002E3C).
The debugger stopped here:
Code: Select all
0E17:208A 55 push bp
0E17:208B 8BEC mov bp,sp
0E17:208D 1E push ds
0E17:208E B84219 mov ax,1942
0E17:2091 8ED8 mov ds,ax
0E17:2093 33C0 xor ax,ax
0E17:2095 A3342E mov [2E34],ax
0E17:2098 A3362E mov [2E36],ax
0E17:209B A33E2E mov [2E3E],ax
0E17:209E A3402E mov [2E40],ax
0E17:20A1 C55606 lds dx,[bp+06]
0E17:20A4 B8003D mov ax,3D00
0E17:20A7 CD21 int 21
0E17:20A9 A33C2E mov [2E3C],ax ; <-- this
0E17:20AC 7308 jnc 000020B6 ($+8)
The contents of the registers:
Code: Select all
EAX=00000002 ESI=0000008D DS=0000 ES=0000 FS=0000 GS=0000 SS=1942 Real
EBX=000077FC EDI=00007555 CS=0E17 EIP=000020AC C1 Z1 S0 O0 A0 P1 D0 I1 T0
ECX=00000000 EBP=00007490 IOPL3 CPL0
EDX=00000000 ESP=0000748E 182627883
Hey! Why is DS=0?
It was the LDS instruction!
Let's look at the stack: (BP+6=7496)
Code: Select all
1942:7490 AE 74 50 1F 17 0E 00 00 00 00 42 19 55 75 AE 74
It is a far pointer argument passed on the stack.
Who called this procedure?
The answer is:
Code: Select all
0E17:1F32 833E242E00 cmp word [2E24],0000
0E17:1F37 740D je 00001F46 ($+d)
0E17:1F39 B8FC2D mov ax,2DFC
0E17:1F3C 8946F0 mov [bp-10],ax
0E17:1F3F 8C5EF2 mov [bp-0E],ds
0E17:1F42 8CDA mov dx,ds
0E17:1F44 EB03 jmp short 00001F49 ($+3)
0E17:1F46 2BC0 sub ax,ax ; <- ax=0
0E17:1F48 99 cwd ; <- dx=0
0E17:1F49 52 push dx
0E17:1F4A 50 push ax
0E17:1F4B 90 nop
0E17:1F4C 0E push cs
0E17:1F4D E83A01 call 0000208A ($+13a) ; <-- this
0E17:1F50 83C404 add sp,0004
Let's look at the annotated disassembly:
Code: Select all
seg009:1F32 cmp word_1A814, 0
seg009:1F37 jz loc_E686
seg009:1F39 mov ax, offset aDigi_drv ; "DIGI.DRV"
seg009:1F3C mov word ptr [bp+var_10], ax
seg009:1F3F mov word ptr [bp+var_10+2], ds
seg009:1F42 mov dx, ds
seg009:1F44 jmp short loc_E689
seg009:1F46 ; ───────────────────────────────────────────────────────────────────────────
seg009:1F46
seg009:1F46 loc_E686: ; CODE XREF: load_drivers+31j
seg009:1F46 sub ax, ax ; NULL
seg009:1F48 cwd
seg009:1F49
seg009:1F49 loc_E689: ; CODE XREF: load_drivers+3Ej
seg009:1F49 push dx
seg009:1F4A push ax ; filename
seg009:1F4B nop
seg009:1F4C push cs
seg009:1F4D call near ptr load_driver
seg009:1F50 add sp, 4
Code: Select all
seg009:208A load_driver proc far ; CODE XREF: load_drivers+47p
seg009:208A ; load_drivers+6Fp
seg009:208A
seg009:208A filename = dword ptr 6
seg009:208A
seg009:208A push bp
seg009:208B mov bp, sp
seg009:208D push ds
seg009:208E mov ax, seg seg016
seg009:2091 mov ds, ax
seg009:2093 xor ax, ax
seg009:2095 mov word ptr driver_code_ptr, ax
seg009:2098 mov word ptr driver_code_ptr+2, ax
seg009:209B mov word ptr driver_header_ptr, ax
seg009:209E mov word ptr driver_header_ptr+2, ax
seg009:20A1 lds dx, [bp+filename] ; ahem...
seg009:20A4 mov ax, 3D00h
seg009:20A7 int 21h ; DOS - 2+ - OPEN DISK FILE WITH HANDLE
seg009:20A7 ; DS:DX -> ASCIZ filename
seg009:20A7 ; AL = access mode
seg009:20A7 ; 0 - read
seg009:20A9 mov driv_pres_handle, ax
seg009:20AC jnb loc_E7F6
seg009:20AE xor ax, ax
seg009:20B0 mov driv_pres_handle, ax
seg009:20B3 jmp loc_E8C7
So, what happens is that the load_driver procedure expects a far pointer, but assumes that it points to the default data segment.
The procedure that calls load_driver passes either "DIGI.DRV" or NULL. In the latter case, load_driver should not be called at all. Or if it is called with NULL, it should check if this is the case.
Or it should always use "DIGI.DRV", and silently fail if that file doesn't exist.
(DIGI.DRV and MIDI.DRV are copied by SETUP.EXE from the SNDDRVRS folder if you choose Sound Blaster Pro, and they are deleted if you choose PC Speaker.)
Now, how do the SET statements come into play?
The EXE is loaded immediately after the area that stores the SET variables.
That is, if there are more or longer variables, the EXE is loaded at a different place.
But the rogue write always overwrites the same address, 0000:2E3C.
Various codes may end up at this address, depending on the size of the SET area.
Under real DOS (and in the DOS emulator built into Windows), some part of DOS (ntio.sys) is here, so this bug may end up breaking the system (or the DOS emulation)!
And which procedure is overwritten?
This:
Code: Select all
seg000:1400 call play_sound_from_buffer
seg000:1405
seg000:1405 loc_1405: ; CODE XREF: play_current_sound+6j
seg000:1405 ; play_current_sound+1Ej ...
seg000:1405 mov current_sound, 0FFFFh
seg000:140B pop si
seg000:140C retf ; <-- this becomes add [bx+si],al
seg000:140C play_current_sound endp
seg000:140D
seg000:140D ; int __pascal far check_sword_vs_sword()
seg000:140D check_sword_vs_sword proc far ; CODE XREF: sub_994+35p
seg000:140D cmp byte_1BD18, 0A7h ; 'ž' ; <-- this becomes sub ds:[bp+di-59],al
seg000:1412 jz loc_141B
seg000:1414 cmp byte_1BC98, 0A7h ; 'ž'
seg000:1419 jnz locret_1423
seg000:141B
seg000:141B loc_141B: ; CODE XREF: check_sword_vs_sword+5j
seg000:141B mov ax, 0Ah
seg000:141E push ax ; sound_id
seg000:141F push cs
seg000:1420 call near ptr play_sound
seg000:1423
seg000:1423 locret_1423: ; CODE XREF: check_sword_vs_sword+Cj
seg000:1423 retf
seg000:1423 check_sword_vs_sword endp ; sp = 2
Oh! Everytime play_current_sound is called, some random byte is changed, and the check_sword_vs_sword is also executed.
Perhaps that repeating beep is the sword vs. sword sound?
According to the debugger, that JZ does jump every second time, which causes that sound to be played.
SI and DI are always 4 bigger than last time.
(Debugging corrupted code is hard, because it follows no logic, so the following may be unclear.)
check_sword_vs_sword and play_current_sound are called in an alternating way.
I also found out what increases SI and DI: when check_sword_vs_sword is actually called, this code gets executed: (this is the overwritten code)
Code: Select all
01A3:140D 003E2843 add [4328],bh
01A3:1411 A7 cmpsw
01A3:1412 7407 je 0000141B ($+7)
CMPSW increases both SI and DI by 2, but then the sound is not played, and the "add [bx+si],al" and "sub ds:[bp+di-59],al" are not executed.
When play_current_sound is called, it does reach the add and sub, and at every second call (when AL=0, and the pointed byte is 0 and remains 0), the sound is played. At the other calls, AL is 1, so the pointed byte is changed.
Whew! That was long!
To sum up things:
0000:2E3C is always overwritten with zero. What it does depends on what is loaded there.
In your case it overwrites the boundary between a procedure that is called for every sound (play_current_sound), and a procedure that plays a certain sound effect (check_sword_vs_sword).
It is overwritten because DS is loaded from a far NULL pointer, but DS is not restored before the next access to a (global) variable.