Table of Contents
Early 2026 work on emu2 etc, test cases
2026-01-11
lDOS kernel
- Implement COMPAT option NOCOLLECTALLMCBS. Using this makes it so the lDOS allocate memory functions won't collect all free MCBs before running the main loop to allocate. Instead, every free MCB encountered during the main loop collects free MCBs after itself. This is compatible to MS-DOS and FreeDOS and EDR-DOS.
- Implement COLLECTFREEMCB. With this enabled, a memory free call will collect free MCBs after the one freed. This is compatible to EDR-DOS.
ETA:
- Harden CollectNextFreeMCB to check that the MCB to collect is contiguous to the collecting one. This is a cornercase that usually cannot occur, but it is possible to set up a pathological case in which the MCB chain gets corrupted. This is only a problem since the NOCOLLECTALLMCBS patch because prior to this CollectNextFreeMCB was always called with the "linear MCB flags".
patchini
- Add LIST keyword to patldos, to list all configurations.
- Display a message if LIST found nothing.
steptrac
New script to step through an emu2-style CPU log. This log is similar to a list of lDebug trace register dumps, except both register lines and the disassembly line are all on the same line. step.pl allows to specify an input file and an output file to append to, and provides several commands to step through the input. The output is expanded into the lines as expected by TracList.
The input loop was copied from TracList to allow receiving keys without trailing linebreaks.
- Beep on Start of File, End of File, or unknown key.
- Add append filename to write the trace to.
- Flush append file after every write.
- Add autorepeat for blank and return. (This was difficult because comparing the variable to KEY_ENTER or '
\n' didn't work."\n"did the trick. - Add g and G commands to skip to the very beginning or very end of the file. (G doesn't work exactly right yet.)
wwwecm scripts
Update sync-gitrepos-with-github to git fetch from origin, pass the -u switch, and pass '*:*'. This seems to do what we want, namely update the default log on bare repos. Repos need to be set up as follows:
- In ~/gitrepos:
git clone SOURCE -n REPO - In ~/mirror-gitrepos:
touch REPO
emu2
Robert pointed me to emu2 recently again. Loving to work with simple 8086 DOS emulators, I picked it up and worked some on my own fork, based on amuramatsu's fork. Here's what I did:
- Eliminate the current PSP variable internal to the emulator, only use the SDA field. lDebug and Enhanced Debug both expect to be able to change the current PSP without involving DOS simply by writing to this field.
- Handle int 20h as int 21h function 00h.
- Avoid double free caused by a duplicated fclose in 21.4B code.
- Terminate MCB name with a NUL if shorter than 8 bytes.
- Handle int 27h (by translating to int 21h function 3100h).
- Fix int 21h function 55h writing to the wrong Process Handle Table because it used a linear address as the segment instead of the segment value. Commit message describes this some more.
- Support pocess termination of self-parented process. This, like TSR termination, should not close handles or free memory.
- In process termination reset FL to return to the parent so that IF=1 TF=0 CF=0.
- Fix int 21h function 60h with the same buffer for input and output. This is documented as supported for MS-DOS, but would lead to various errors here.
- Pick a dmsc commit on a typo in naming of codepage 852.
- Pick dmsc commit on merging free MCBs during allocation. This was a response to one of our first bug reports to the original emu2 repo.
- Implement HLT to wait for next IRQ rather than unconditionally exiting the emulator. Only exit if IF=0 on the HLT.
- Use memory_mask throughout the CPU code instead of hardcoding the 0xFFFFF mask (like A20 disabled).
- Detect as an 186 using Eric Auer's cpulevel program as well.
- Fix Trace Flag set with repeated string instructions.
- Fix repnz or repz with input cx equal to zero, shouldn't affect Zero Flag.
- Fix EOIs for IRQ8 and higher with the default interrupt handlers.
- Limit amount of prefixes accepted, with a 16-byte limit. Unlike the 286 or 386, not all instruction bytes count towards the limit, such as ModR/M, displacement, and immediate bytes. The idea is to set a limit to the amount of prefixes accepted, as additional prefixes may be coded as nested function calls in the emulator. So with a 16 prefix bytes limit, we hope to avoid stack overflows.
- Do not fire an int 1 (trace interrupt) if entering another interrupt. This is unlike the original 8086 and NEC V20, but is in my opinion an improvement.
- Treat LOCK as a prefix (that does nothing) rather than as an instruction. This means a prefix before LOCK (eg ES segment override) can apply to the subsequent instruction. The prefix limit patch is required for this so as to avoid too deep nesting.
- Do interrupt lockout for mov or pop to ss. Unlike the NEC V20, this is precisely only for the instructions that write to ss, rather than all instructions writing any segregs (mov or pop) or mov from a segreg.
- Support CPU flag 1 and 2. 1 means detect as an 8086, 2 means detect as a 286.
- Set memory_mask to all-1s if A20 enabled. This is required to support non-power-of-2 memory sizes.
- Fix search_freemem when it doesn't find a suitable free XMS block. Would dereference a NULL pointer.
- Allow any number between 2 and 16 for amount of memory.
- Use memmove in XMS move to support overlapping source and destination. The check to avoid wrong overlaps was coded the wrong way around because it is documented wrongly in the XMS specifications (both XMS v2 and XMS v3). On our system, memcpy silently did the right thing when called with a wrong overlap, but this is obviously not portable.
- Insure to clear the HMA area as well (1088 KiB rather than 1024 KiB) on init.
- Fix to actually write an int 19h to the FFFFh:0 entrypoint, rather than retf.
Test programs
I uploaded a number of test programs that were used for recent changes:
testmcb.asm
testmcb.asm: Test program for the original emu2 repo, for a bug report on MCB coalescing. What it does: Allocate a number of 64 KiB memory blocks, free them in a certain order, then try to allocate a larger block.
cpu 8086 org 256 start: mov ah, 4Ah mov bx, 1000h int 21h mov di, array xor cx, cx loopalloc: mov ah, 48h mov bx, 1000h int 21h jc endalloc stosw inc cx jmp loopalloc endalloc: cmp cx, 4 jb errorinit mov si, array mov cx, 4 loopfree: lodsw mov es, ax mov ah, 49h int 21h loop loopfree mov ah, 48h mov bx, 3000h int 21h jc errorexit mov dx, msg.success mov ah, 09h int 21h mov ax, 4C00h int 21h errorexit: mov dx, msg.errorexit jmp error errorinit: mov dx, msg.errorinit error: mov ah, 09h int 21h mov ax, 4C01h int 21h msg: .errorinit: db "Too small initial allocation!",13,10,36 .errorexit: db "Failed last allocation!",13,10,36 .success: db "Success!",13,10,36 align 2 array:
testmcb2.asm
testmcb2.asm: Test program to check that lDOS kernel option COMPAT=COLLECTFREEMCB works as intended. What it does: Allocate several 64 KiB blocks, then free some of them in a certain order.
cpu 8086 org 256 start: mov ah, 4Ah mov bx, 1000h int 21h mov di, array xor cx, cx loopalloc: mov ah, 48h mov bx, 1000h int 21h jc endalloc stosw inc cx jmp loopalloc endalloc: cmp cx, 4 jb errorinit mov es, word [array + 4] mov ah, 49h int 21h mov es, word [array + 2] mov ah, 49h int 21h mov es, word [array + 0] mov ah, 49h int 21h mov dx, msg.success mov ah, 09h int 21h mov ax, 4C00h int 21h errorexit: mov dx, msg.errorexit jmp error errorinit: mov dx, msg.errorinit error: mov ah, 09h int 21h mov ax, 4C01h int 21h msg: .errorinit: db "Too small initial allocation!",13,10,36 .errorexit: db "Failed last allocation!",13,10,36 .success: db "Success!",13,10,36 align 2 array:
tsr.asm
tsr.asm: Test case for a simple TSR program that calls int 27h with dx equal to zero. Used to test the int 27h service in emu2. What it does: Close the first 20 process handles, free the environment block, and TSR using int 27h.
cpu 8086 org 256 start: mov cx, 20 xor bx, bx .loop: mov ah, 3Eh int 21h inc bx loop .loop xor ax, ax xchg ax, [2Ch] mov es, ax mov ah, 49h int 21h xor dx, dx int 27h mov ax, 4CFFh int 21h
testrep.asm
testrep.asm: Test case for tracing repeated string instructions. Used to develop emu2 Trace Flag support. What it does: Use rep lodsb with several different counter values in cx.
cpu 8086 org 256 start: xor ax, ax mov si, buffer mov cx, 0 rep lodsb mov si, buffer mov cx, 1 rep lodsb mov si, buffer mov cx, 2 rep lodsb mov si, buffer mov cx, 3 rep lodsb mov ax, 4C00h int 21h align 2 buffer: db 1, 2, 3, 4
testrepz.asm
testrepz.asm: Test case for repz and repnz prefixed string instructions to preserve all flags on execution with cx equal to zero. Used to develop emu2.
org 256 start: xor cx, cx cmp al, al repe cmpsb or al, 1 repe cmpsb cmp al, al repne cmpsb or al, 1 repne cmpsb mov ax, 4C00h int 21h
int6.asm
int6.asm: Run an interrupt 6 by means of int instruction.
cpu 8086 org 256 start: int 6 mov ax, 4C00h int 21h
testirq.asm
testirq.asm: Trigger IRQ #8 several times to test that the default ROM-BIOS handler correctly issues EOIs. This was used in conjunction with the int 2Dh test handler of emu2, to trigger the IRQ #8 (int 70h).
cpu 8086 org 256 start: mov ax, 3570h int 21h mov word [next + 2], es mov word [next + 0], bx mov ax, 2570h mov dx, handler int 21h in al, 0A1h and al, ~1 out 0A1h, al xor ax, ax int 2Dh mov dx, [variable] mov ax, 2600h int 2Dh mov dx, [variable] mov ax, 3800h int 2Dh mov dx, [variable] lds dx, [next] mov ax, 2570h int 21h mov ax, 4C00h int 21h handler: inc word [cs:variable] jmp 0:0 next: equ $ - 4 align 2 variable: dw 0
testxms.asm
testxms.asm: Test program to allocate or free XMS memory blocks. Not tested much. Used to check emu2 XMS allocation works and fails as expected.
%if 0
Usage of the works is permitted provided that this
instrument is retained with the works, so that any entity
that uses the works is notified of this instrument.
DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
%endif
cpu 8086
org 256
start:
mov si, 81h
.skip:
lodsb
cmp al, 32
je .skip
cmp al, 9
je .skip
cmp al, '!'
jne .notex
not byte [wantfree]
.notex:
dec si
call getnumber
init:
mov ax, 4300h
int 2Fh
cmp al, 80h
je .gotxms
mov dx, msg.noxms
jmp errorexit
.gotxms:
mov ax, 4310h
int 2Fh
mov word [handler], bx
mov word [handler + 2], es
rol byte [wantfree], 1
jc free
alloc:
mov dx, msg.alloc1
call disp_msg
mov ax, di
call disp_ax_dec
mov dx, msg.alloc2
call disp_msg
mov dx, di
mov ah, 09h
push cs
call callhandler
cmp ax, 1
jne .error
push dx
mov dx, msg.alloc3
call disp_msg
pop ax
call disp_ax_dec
mov dx, msg.alloc4
call disp_msg
mov ax, 4C00h
int 21h
.error:
push dx
mov dx, msg.alloc5
call disp_msg
mov al, bl
call disp_al_hex
mov dx, msg.alloc6
call disp_msg
pop ax
call disp_ax_dec
mov dx, msg.alloc7
errorexit:
call disp_msg
mov ax, 4C01h
int 21h
free:
mov dx, msg.free1
call disp_msg
mov ax, di
call disp_ax_dec
mov dx, msg.free2
call disp_msg
mov dx, di
mov ah, 0Ah
push cs
call callhandler
cmp ax, 1
jne .error
mov dx, msg.free3
call disp_msg
mov ax, 4C00h
int 21h
.error:
mov dx, msg.free5
call disp_msg
mov al, bl
call disp_al_hex
mov dx, msg.free6
jmp errorexit
disp_msg:
push ax
mov ah, 09h
int 21h
pop ax
retn
disp_ax_dec:
push ax
push cx
push dx
mov cx, 10
mov dx, -1
push dx
.loop1:
xor dx, dx
div cx
push dx
test ax, ax
jnz .loop1
.loop2:
pop ax
add al, '0'
jc .end
xchg dx, ax
mov ah, 02h
int 21h
jmp .loop2
.end:
pop dx
pop cx
pop ax
retn
disp_ax_hex:
xchg al, ah
call disp_al_hex
xchg al, ah
disp_al_hex:
push cx
mov cl, 4
rol al, cl
call disp_al_nybble_hex
rol al, cl
pop cx
disp_al_nybble_hex:
push ax
push dx
and al, 15
add al, '0'
cmp al, '9'
jbe .got
add al, 7
.got:
xchg dx, ax
mov ah, 02h
int 21h
pop dx
pop ax
retn
callhandler:
jmp 0:0
handler equ $ - 4
; INP: si -> text
; OUT: si - 1 -> end (nondigit)
; di = number
getnumber:
.skip:
lodsb
cmp al, 32
je .skip
cmp al, 9
je .skip
cmp al, '0'
jb error
cmp al, '9'
ja error
xor di, di
jmp .digit
.loop:
lodsb
.digit:
cmp al, '_'
je .loop
sub al, '0'
jb .end
cmp al, 10
jae .end
cbw
add di, di ; times 2
mov bx, di
add di, di ; times 4
add di, di ; times 8
add di, bx ; times 10
add di, ax
jmp .loop
.end:
retn
error:
mov dx, msg.error
jmp errorexit
wantfree: db 0
msg:
.error: db "Error!",13,10,36
.noxms: db "No XMS entry found!",13,10,36
.alloc1: db "Trying to allocate ",36
.alloc2: db " KiB, ",36
.alloc3: db "success, handle=",36
.alloc7:
.alloc4: db 13,10,36
.free5:
.alloc5: db "error, code=",36
.alloc6: db "h, dx=",36
.free1: db "Trying to free handle=",36
.free2: db ", ",36
.free6: db "h",13,10,36
.free3: db "success",13,10,36
testxmsm.asm
testxmsm.asm: Test two XMS move calls with differing overlapping buffers, all in conventional memory. Used to check that the XMS specification is, indeed, wrong about the overlapping move direction. This test program was used during emu2 development to test the various combinations of checks and move implementations.
On the int3 breakpoints, the following debugger command can be used to view the "destination" memory buffer: d word [si + 4 + 6 + 2] l word [si]
cpu 186 org 256 start: mov ax, 4310h int 2Fh push es push bx mov bp, sp push cs push lower1 ; dest push 0 push cs push higher1 ; source push 0 push 0 push length1 ; length mov si, sp mov ah, 0Bh call far [bp] int3 push cs push higher2 ; dest push 0 push cs push lower2 ; source push 0 push 0 push length2 ; length mov si, sp mov ah, 0Bh call far [bp] int3 mov ax, 4C00h int 21h align 16, db 0 lower1: times 16 db 0 higher1: times 4 db "Foo bar baz quux@" align 16, db 0 length1 equ $ - higher1 align 16, db 0 lower2: times 4 db "Foo bar baz quux@" align 16, db 0 length2 equ $ - lower2 higher2: equ lower2 + 16
testmcb3.asm
testmcb3.asm: Set up pathological MCB allocation situation to test that the lDOS kernel MCB collection is hardened appropriately. What it does: You need to run lDOS kernel with COMPAT=HIDEDOSENTRY in a dosemu2 instance, then run ldebug /t testmcb3.com so that the first MCB is the debuggee's environment block. Then, it will free its environment, check that the first MCB is free, get the first UMCB, walk until the 'Z' UMCB (last UMCB), check that this is an S MCB (owner 8, name "S", type 30h "excluded UMA") that's empty (size 0), and that the second-to-last UMCB is free. Then it sets up the memory allocation strategy 0382h and writes a 'Z' signature to the second-to-last UMCB. Finally, it tries to allocate memory with the impossible size (0FFFFh).
Prior to this lDOS kernel change, the allocation call would incorrectly collect the first LMCB upon encountering the last UMCB when both are free. This is because of the extended strategy:
- 200h = Treat all dual-area strategies as "one area".
- 100h = Ignore real status of the last LMCB's signature, enable UMCB use.
- 80h = Use dual areas: UMA then LMA (crucially, after the last UMCB the allocation walker encounters the first LMCB).
- 2 = Last fit (not needed here if the impossible size is used).
cpu 8086 org 256 start: xor di, di xor ax, ax xchg ax, [di + 2Ch] mov es, ax mov ah, 49h int 21h mov ah, 52h int 21h mov ax, [es:bx - 2] mov es, ax cmp word [es:di + 1], 0 jne error mov ax, 1261h stc int 2Fh jc error xor bx, bx .loop: mov ds, ax cmp byte [di], 'Z' je .last cmp byte [di], 'M' jne error mov bx, ax add ax, word [di + 3] inc ax jmp .loop .last: test bx, bx jz error cmp word [di + 1], 8 jne error cmp word [di + 3], 0 jne error cmp word [di + 8], "S" jne error cmp byte [di + 10], 30h jne error mov ds, bx cmp word [di + 1], 0 jne error mov ax, 5801h mov bx, 382h stc int 21h jc error int3 mov byte [di], 'Z' mov ah, 48h mov bx, -1 int 21h mov ax, 4C00h int 21h error: int3 mov ax, 4CFFh int 21h
