Add interrupts module: INT 3 (CC breakpoint) vs INT 21h (DOS services)
NASM sources, DOSBox/FreeDOS Debug/Unicorn harness, captured output and 31 assertions backing the ankurm.com article "INT 3 vs INT 21h in 8086 Assembly". Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# interrupts -- INT 3 (breakpoint) vs INT 21h (DOS services)
|
||||
|
||||
Companion module for the article
|
||||
[INT 3 vs INT 21h in 8086 Assembly](https://ankurm.com/understanding-int-3h-vs-int-21h-in-8086-assembly/).
|
||||
Everything the article quotes as program output was produced by `scripts/run-all.sh` and is stored in `output/`.
|
||||
|
||||
## What was actually run, and what was not
|
||||
|
||||
| Tool | Version | Used for |
|
||||
|---|---|---|
|
||||
| NASM | 2.16.01 | assembling every `.asm` file (`bits 16`, flat `.COM` layout) |
|
||||
| DOSBox | 0.74-3 | running the `.COM` programs: a real INT 21h implementation, DOSBox's own default INT 3 vector |
|
||||
| FreeDOS Debug | 2.50 (MIT) | a DEBUG.EXE-compatible debugger, driven by a script, to watch CC vs CD 03 traps |
|
||||
| Unicorn | 2.1.4 | a software "debugger" experiment on a 16-bit x86 core (opcode lengths, return addresses) |
|
||||
|
||||
**Not run:** emu8086, Microsoft's own `DEBUG.EXE`, MASM/TASM, real MS-DOS on real hardware, a real 8086.
|
||||
DOSBox and Unicorn are emulators; the article says so wherever it matters. Claims about those unrun environments
|
||||
are labelled "documented, not executed".
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `asm/opcodes.asm` | which bytes `int3`, `int 3`, `db 0CDh,3`, `int 21h` assemble to (listing only) |
|
||||
| `asm/hello21.asm` | smallest INT 21h program: `AH=09h` print, `AH=4Ch` exit with code 7 |
|
||||
| `asm/fn_tour.asm` | one line of output per important INT 21h function (console, vectors, memory, files, EXEC) |
|
||||
| `asm/int3_handler.asm` | installs an INT 3 handler with `AH=25h`; triggers it with `CC` and with `CD 03` |
|
||||
| `asm/int3_default.asm` | executes INT 3 with no handler of its own |
|
||||
| `asm/dbg_cc.asm`, `asm/dbg_cd03.asm` | targets for the DEBUG sessions |
|
||||
| `asm/bp_target.asm` | flat routine patched by the Unicorn breakpoint experiment |
|
||||
| `asm/common.inc` | print helpers (`puts`, `putdec`, `puthex16`) |
|
||||
| `scripts/run-all.sh` | rebuild everything in `output/` and run the checks |
|
||||
| `scripts/dosbox_run.py` | assemble + run the `.COM` files headless in DOSBox |
|
||||
| `scripts/bp_experiment.py` | Unicorn breakpoint experiment |
|
||||
| `scripts/check.py` | 31 assertions that pin every claim the article makes about the output |
|
||||
| `scripts/fetch-debug.sh` | downloads FreeDOS Debug into `build/` (not committed) |
|
||||
| `output/` | captured results, numbered in the order the article uses them |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
sudo apt-get install nasm dosbox # Debian/Ubuntu
|
||||
pip install unicorn
|
||||
./scripts/run-all.sh # prints PASS/FAIL per assertion
|
||||
```
|
||||
|
||||
DOSBox runs headless (`SDL_VIDEODRIVER=dummy`). The memory figure on the `AH=48h` line and the segment numbers in the
|
||||
DEBUG transcripts depend on the DOSBox build and configuration.
|
||||
@@ -0,0 +1,13 @@
|
||||
; bp_target.asm -- a flat 16-bit routine used by the Unicorn breakpoint experiment.
|
||||
; Three one-byte INC CX instructions, so a two-byte CD 03 patch on the first one
|
||||
; overwrites the first byte of the second one too.
|
||||
bits 16
|
||||
org 0x7C00
|
||||
start:
|
||||
xor cx, cx
|
||||
site:
|
||||
inc cx ; 41 <- breakpoint goes here
|
||||
inc cx ; 41
|
||||
inc cx ; 41
|
||||
done:
|
||||
nop
|
||||
@@ -0,0 +1,64 @@
|
||||
; common.inc -- tiny output helpers shared by the demo programs (DOS .COM, NASM syntax)
|
||||
|
||||
; print the '$'-terminated string at DS:DX (INT 21h, AH=09h)
|
||||
puts:
|
||||
mov ah, 09h
|
||||
int 21h
|
||||
ret
|
||||
|
||||
; print CRLF
|
||||
crlf:
|
||||
mov dx, s_crlf
|
||||
jmp puts
|
||||
s_crlf db 0Dh, 0Ah, '$'
|
||||
|
||||
; print AX as four hex digits using INT 21h AH=02h
|
||||
puthex16:
|
||||
push ax
|
||||
mov al, ah
|
||||
call puthex8
|
||||
pop ax
|
||||
puthex8:
|
||||
push ax
|
||||
shr al, 1
|
||||
shr al, 1
|
||||
shr al, 1
|
||||
shr al, 1
|
||||
call nib
|
||||
pop ax
|
||||
and al, 0Fh
|
||||
nib:
|
||||
add al, '0'
|
||||
cmp al, '9'
|
||||
jbe .ok
|
||||
add al, 7
|
||||
.ok:
|
||||
mov dl, al
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
ret
|
||||
|
||||
; print AX as unsigned decimal
|
||||
putdec:
|
||||
push bx
|
||||
push cx
|
||||
push dx
|
||||
xor cx, cx
|
||||
mov bx, 10
|
||||
.d1:
|
||||
xor dx, dx
|
||||
div bx
|
||||
push dx
|
||||
inc cx
|
||||
test ax, ax
|
||||
jnz .d1
|
||||
.d2:
|
||||
pop dx
|
||||
add dl, '0'
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
loop .d2
|
||||
pop dx
|
||||
pop cx
|
||||
pop bx
|
||||
ret
|
||||
@@ -0,0 +1,8 @@
|
||||
; dbg_cc.asm -- target for a DEBUG session: a real one-byte breakpoint (CC) between two MOVs.
|
||||
bits 16
|
||||
org 0x100
|
||||
mov ax, 1111h
|
||||
int3 ; CC
|
||||
mov ax, 2222h
|
||||
mov ax, 4C00h
|
||||
int 21h
|
||||
@@ -0,0 +1,8 @@
|
||||
; dbg_cd03.asm -- the same program, but the breakpoint is the two-byte CD 03.
|
||||
bits 16
|
||||
org 0x100
|
||||
mov ax, 1111h
|
||||
db 0CDh, 03h ; CD 03 (NASM's `int 3` would emit exactly this)
|
||||
mov ax, 2222h
|
||||
mov ax, 4C00h
|
||||
int 21h
|
||||
@@ -0,0 +1,366 @@
|
||||
; fn_tour.asm -- exercises the important INT 21h functions, one labelled line of output each.
|
||||
; Run it under DOS (the harness uses DOSBox) with stdin redirected from IN.TXT.
|
||||
bits 16
|
||||
org 0x100
|
||||
; ---- shrink our memory block: a .COM owns all conventional memory (AH=4Ah) ----
|
||||
mov bx, 1000h ; keep 64 KB = 1000h paragraphs
|
||||
mov ah, 4Ah ; ES = our PSP segment already
|
||||
int 21h
|
||||
|
||||
; ---- AH=30h get DOS version ----
|
||||
mov dx, l_ver
|
||||
call puts
|
||||
mov ah, 30h
|
||||
int 21h ; AL = major, AH = minor
|
||||
push ax
|
||||
xor ah, ah
|
||||
call putdec
|
||||
mov dl, '.'
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
pop ax
|
||||
mov al, ah
|
||||
xor ah, ah
|
||||
call putdec
|
||||
call crlf
|
||||
|
||||
; ---- AH=2Ah / AH=2Ch date and time (values change every run, so we only range-check) ----
|
||||
mov dx, l_date
|
||||
call puts
|
||||
mov ah, 2Ah ; CX = year, DH = month, DL = day
|
||||
int 21h
|
||||
cmp cx, 1980
|
||||
jb .baddate
|
||||
cmp dh, 12
|
||||
ja .baddate
|
||||
mov dx, s_ok
|
||||
jmp .dateout
|
||||
.baddate:
|
||||
mov dx, s_bad
|
||||
.dateout:
|
||||
call puts
|
||||
mov ah, 2Ch ; CH = hour, CL = minute, DH = second
|
||||
int 21h
|
||||
mov dx, s_ok2
|
||||
cmp ch, 23
|
||||
jbe .timeok
|
||||
mov dx, s_bad
|
||||
.timeok:
|
||||
call puts
|
||||
call crlf
|
||||
|
||||
; ---- AH=02h / AH=06h character output ----
|
||||
mov dx, l_chr
|
||||
call puts
|
||||
mov dl, 'A'
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dl, 'B'
|
||||
mov ah, 06h ; direct console output (no ^C check)
|
||||
int 21h
|
||||
call crlf
|
||||
|
||||
; ---- AH=01h then AH=0Ah stdin: one echoed char, then a buffered line ----
|
||||
mov dx, l_in
|
||||
call puts
|
||||
mov ah, 01h ; AL = char read (and echoed)
|
||||
int 21h
|
||||
mov [ch1], al
|
||||
mov dx, buf
|
||||
mov ah, 0Ah ; buf[0] = max, buf[1] = count read, buf[2..] = text
|
||||
int 21h
|
||||
call crlf
|
||||
mov dx, l_got
|
||||
call puts
|
||||
mov al, [ch1]
|
||||
call puthex8 ; the character AH=01h returned
|
||||
mov dx, l_cnt
|
||||
call puts
|
||||
xor ah, ah
|
||||
mov al, [buf+1] ; number of characters AH=0Ah stored (CR not counted)
|
||||
mov bx, ax
|
||||
call putdec
|
||||
mov byte [buf+2+bx], '$'
|
||||
mov dx, l_txt
|
||||
call puts
|
||||
mov dx, buf+2
|
||||
call puts
|
||||
call crlf
|
||||
|
||||
; ---- AH=25h / AH=35h set and get an interrupt vector ----
|
||||
mov dx, l_vec
|
||||
call puts
|
||||
mov ax, 2560h
|
||||
mov dx, dummy_isr
|
||||
int 21h
|
||||
mov ax, 3560h
|
||||
int 21h ; ES:BX = vector 60h
|
||||
mov dx, s_bad
|
||||
cmp bx, dummy_isr
|
||||
jne .vecout
|
||||
mov ax, es
|
||||
mov cx, cs
|
||||
cmp ax, cx
|
||||
jne .vecout
|
||||
mov dx, s_ok
|
||||
.vecout:
|
||||
call puts
|
||||
call crlf
|
||||
|
||||
; ---- AH=48h / AH=49h allocate and free memory; failure returns CF=1, AX=8, BX=largest ----
|
||||
mov dx, l_mem
|
||||
call puts
|
||||
mov bx, 0100h ; 100h paragraphs = 4 KB
|
||||
mov ah, 48h
|
||||
int 21h
|
||||
jc .memfail
|
||||
mov es, ax ; ES = segment of the new block
|
||||
mov ah, 49h
|
||||
int 21h
|
||||
mov dx, s_ok
|
||||
jmp .memout
|
||||
.memfail:
|
||||
mov dx, s_bad
|
||||
.memout:
|
||||
call puts
|
||||
call crlf
|
||||
mov bx, 0FFFFh ; ask for 1 MB: must fail
|
||||
mov ah, 48h
|
||||
int 21h
|
||||
mov [err], ax ; save the results before printing clobbers them
|
||||
mov [big], bx
|
||||
mov byte [cfch], '0'
|
||||
jnc .memcf
|
||||
mov byte [cfch], '1'
|
||||
.memcf:
|
||||
mov dx, l_memfail
|
||||
call puts
|
||||
mov dl, [cfch]
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dx, l_ax
|
||||
call puts
|
||||
mov ax, [err] ; 0008h = insufficient memory
|
||||
call puthex16
|
||||
mov dx, l_big
|
||||
call puts
|
||||
mov ax, [big] ; BX = largest block available, in paragraphs
|
||||
call puthex16
|
||||
call crlf
|
||||
|
||||
; ---- file I/O: AH=3Ch create, 40h write, 3Eh close, 3Dh open, 3Fh read, 42h lseek ----
|
||||
mov dx, l_file
|
||||
call puts
|
||||
mov dx, fname
|
||||
xor cx, cx ; normal attributes
|
||||
mov ah, 3Ch
|
||||
int 21h
|
||||
jc .fileerr
|
||||
mov bx, ax ; BX = handle
|
||||
mov dx, payload
|
||||
mov cx, payload_len
|
||||
mov ah, 40h
|
||||
int 21h
|
||||
mov ah, 3Eh
|
||||
int 21h
|
||||
mov dx, fname
|
||||
mov ax, 3D00h ; open read-only
|
||||
int 21h
|
||||
jc .fileerr
|
||||
mov bx, ax
|
||||
mov dx, rdbuf
|
||||
mov cx, 64
|
||||
mov ah, 3Fh
|
||||
int 21h ; AX = bytes actually read
|
||||
call putdec ; bytes actually read
|
||||
mov dx, l_read
|
||||
call puts
|
||||
mov ax, 4202h ; LSEEK from end, offset 0
|
||||
xor cx, cx
|
||||
xor dx, dx
|
||||
int 21h ; DX:AX = file size
|
||||
mov [fsize], ax
|
||||
mov dx, l_seek
|
||||
call puts
|
||||
mov ax, [fsize]
|
||||
call putdec
|
||||
mov ah, 3Eh
|
||||
int 21h
|
||||
call crlf
|
||||
jmp .fileok
|
||||
.fileerr:
|
||||
mov dx, s_bad
|
||||
call puts
|
||||
call crlf
|
||||
.fileok:
|
||||
|
||||
; ---- AH=56h rename, AH=4Eh findfirst (DTA defaults to PSP:0080h), AH=41h delete ----
|
||||
mov dx, l_ff
|
||||
call puts
|
||||
mov dx, fname
|
||||
mov di, fname2
|
||||
push ds
|
||||
pop es
|
||||
mov ah, 56h
|
||||
int 21h
|
||||
mov dx, fname2
|
||||
xor cx, cx
|
||||
mov ah, 4Eh
|
||||
int 21h
|
||||
jc .ffbad
|
||||
mov dx, 80h+1Eh ; DTA+1Eh = ASCIZ name found
|
||||
mov si, dx ; copy it to a '$'-terminated buffer so AH=09h can print it
|
||||
mov di, ffname
|
||||
.cp:
|
||||
lodsb
|
||||
test al, al
|
||||
jz .cpend
|
||||
stosb
|
||||
jmp .cp
|
||||
.cpend:
|
||||
mov al, '$'
|
||||
stosb
|
||||
mov dx, ffname
|
||||
call puts
|
||||
mov dx, l_size
|
||||
call puts
|
||||
mov ax, [80h+1Ah] ; DTA+1Ah = file size (low word)
|
||||
call putdec
|
||||
jmp .ffend
|
||||
.ffbad:
|
||||
mov dx, s_bad
|
||||
call puts
|
||||
.ffend:
|
||||
call crlf
|
||||
mov dx, l_del
|
||||
call puts
|
||||
mov dx, fname2
|
||||
mov ah, 41h ; delete
|
||||
int 21h
|
||||
mov dx, fname2
|
||||
mov ax, 3D00h ; open it again: must fail
|
||||
int 21h
|
||||
mov [err], ax ; save AX and CF before printing clobbers them
|
||||
mov byte [cfch], '0'
|
||||
jnc .cfout
|
||||
mov byte [cfch], '1'
|
||||
.cfout:
|
||||
mov dl, [cfch]
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dx, l_ax
|
||||
call puts
|
||||
mov ax, [err]
|
||||
call puthex16
|
||||
call crlf
|
||||
|
||||
; ---- AH=39h mkdir, AH=3Ah rmdir, AH=19h current drive, AH=47h current directory ----
|
||||
mov dx, l_dir
|
||||
call puts
|
||||
mov dx, dname
|
||||
mov ah, 39h
|
||||
int 21h
|
||||
mov dx, dname
|
||||
mov ah, 3Ah
|
||||
int 21h
|
||||
mov dx, s_ok
|
||||
jnc .dirout
|
||||
mov dx, s_bad
|
||||
.dirout:
|
||||
call puts
|
||||
mov dx, l_drv
|
||||
call puts
|
||||
mov ah, 19h ; AL = 0 for A:, 1 for B:, 2 for C:
|
||||
int 21h
|
||||
xor ah, ah
|
||||
call putdec
|
||||
call crlf
|
||||
|
||||
; ---- AH=4Bh EXEC a child, AH=4Dh read its return code ----
|
||||
mov dx, l_exec
|
||||
call puts
|
||||
mov ax, cs
|
||||
mov [pb+4], ax ; command-tail segment
|
||||
mov [pb+8], ax ; FCB1 segment
|
||||
mov [pb+12], ax ; FCB2 segment
|
||||
mov dx, child
|
||||
mov bx, pb
|
||||
mov ax, 4B00h ; load and execute
|
||||
int 21h
|
||||
jc .execbad
|
||||
mov ah, 4Dh ; AL = child's return code, AH = termination type
|
||||
int 21h
|
||||
push ax
|
||||
mov dx, l_rc
|
||||
call puts
|
||||
pop ax
|
||||
push ax
|
||||
xor ah, ah
|
||||
call putdec
|
||||
mov dx, l_tt
|
||||
call puts
|
||||
pop ax
|
||||
mov al, ah
|
||||
xor ah, ah
|
||||
call putdec
|
||||
call crlf
|
||||
jmp .done
|
||||
.execbad:
|
||||
mov dx, s_bad
|
||||
call puts
|
||||
call crlf
|
||||
.done:
|
||||
mov ax, 4C00h ; AH=4Ch: exit, return code 0
|
||||
int 21h
|
||||
|
||||
dummy_isr:
|
||||
iret
|
||||
|
||||
l_ver db 'AH=30h DOS version : $'
|
||||
l_date db 'AH=2Ah/2Ch date+time ranges : $'
|
||||
l_chr db 'AH=02h + AH=06h : $'
|
||||
l_in db 'AH=01h + AH=0Ah (stdin) : echo -> $'
|
||||
l_got db ' AH=01h got 0x$'
|
||||
l_txt db ', text = $'
|
||||
l_cnt db ' ; AH=0Ah count = $'
|
||||
l_vec db 'AH=25h/35h vector 60h : $'
|
||||
l_mem db 'AH=48h/49h alloc+free 4 KB : $'
|
||||
l_memfail db 'AH=48h ask for 1 MB (fails) : CF=$'
|
||||
l_big db ' ; largest block (paragraphs) = $'
|
||||
l_file db 'AH=3Ch/40h/3Dh/3Fh/42h : $'
|
||||
l_read db ' bytes read ; $'
|
||||
l_ff db 'AH=56h + AH=4Eh findfirst : $'
|
||||
l_size db ' size = $'
|
||||
l_del db 'AH=41h delete, reopen fails : CF=$'
|
||||
l_seek db 'LSEEK(end) says size = $'
|
||||
l_ax db ' AX=$'
|
||||
l_dir db 'AH=39h/3Ah mkdir+rmdir : $'
|
||||
l_drv db ' ; AH=19h drive = $'
|
||||
l_exec db 'AH=4Bh exec child : $'
|
||||
l_rc db 'AH=4Dh return code = $'
|
||||
l_tt db ' type = $'
|
||||
s_ok db 'ok$'
|
||||
s_ok2 db ' ok$'
|
||||
s_bad db 'FAILED$'
|
||||
fname db 'T.TXT', 0
|
||||
fname2 db 'U.TXT', 0
|
||||
dname db 'SUBD', 0
|
||||
child db 'HELLO21.COM', 0
|
||||
payload db 'DOS file I/O'
|
||||
payload_len equ $ - payload
|
||||
ch1 db 0
|
||||
err dw 0
|
||||
fsize dw 0
|
||||
big dw 0
|
||||
cfch db 0
|
||||
buf db 20, 0
|
||||
times 22 db 0
|
||||
ffname times 16 db 0
|
||||
rdbuf times 64 db 0
|
||||
; EXEC parameter block: env seg (0 = inherit), cmd tail ptr, FCB1 ptr, FCB2 ptr
|
||||
pb dw 0, tail, 0, fcb1, 0, fcb2, 0
|
||||
tail db 0, 0Dh
|
||||
fcb1 times 16 db 0
|
||||
fcb2 times 16 db 0
|
||||
|
||||
%include "common.inc"
|
||||
@@ -0,0 +1,9 @@
|
||||
; hello21.asm -- the smallest useful INT 21h program: AH=09h then AH=4Ch with a return code
|
||||
bits 16
|
||||
org 0x100 ; .COM program
|
||||
mov ah, 09h ; AH selects the DOS function: write '$'-terminated string
|
||||
mov dx, msg ; DS:DX -> string (DS = CS in a .COM file)
|
||||
int 21h ; CD 21 -- the DOS entry point
|
||||
mov ax, 4C07h ; AH=4Ch terminate, AL=7 return code (ERRORLEVEL)
|
||||
int 21h
|
||||
msg db 'Hello from INT 21h, AH=09h', 0Dh, 0Ah, '$'
|
||||
@@ -0,0 +1,13 @@
|
||||
; int3_default.asm -- execute INT 3 with NO handler of our own and see whether the program survives.
|
||||
bits 16
|
||||
org 0x100
|
||||
mov dx, m1
|
||||
call puts
|
||||
int3
|
||||
mov dx, m2
|
||||
call puts
|
||||
mov ax, 4C00h
|
||||
int 21h
|
||||
m1 db 'before INT3', 0Dh, 0Ah, '$'
|
||||
m2 db 'after INT3 (program survived)', 0Dh, 0Ah, '$'
|
||||
%include "common.inc"
|
||||
@@ -0,0 +1,123 @@
|
||||
; int3_handler.asm -- install our own INT 3 handler (AH=25h), then trigger it twice:
|
||||
; once with the one-byte CC and once with the two-byte CD 03.
|
||||
; Prints how far past the trapping instruction the pushed return IP points, and the
|
||||
; state of IF (interrupt flag) inside the handler versus after IRET.
|
||||
bits 16
|
||||
org 0x100
|
||||
; --- save the old vector with AH=35h, install ours with AH=25h ---
|
||||
mov ax, 3503h
|
||||
int 21h ; ES:BX = current INT 3 handler
|
||||
mov [old_off], bx
|
||||
mov [old_seg], es
|
||||
mov ax, 2503h
|
||||
mov dx, handler
|
||||
int 21h ; DS:DX = new handler for vector 3
|
||||
|
||||
sti ; make sure IF=1 before we trap, so the handler's IF=0 is visible
|
||||
mov word [target], trap_cc
|
||||
mov dx, m_cc
|
||||
call puts
|
||||
trap_cc:
|
||||
int3 ; CC
|
||||
call report
|
||||
|
||||
mov word [target], trap_cd
|
||||
mov dx, m_cd
|
||||
call puts
|
||||
trap_cd:
|
||||
db 0CDh, 03h ; CD 03
|
||||
call report
|
||||
|
||||
; --- restore the old vector and exit ---
|
||||
push ds
|
||||
mov dx, [old_off]
|
||||
mov ds, [old_seg]
|
||||
mov ax, 2503h
|
||||
int 21h
|
||||
pop ds
|
||||
mov ax, 4C00h
|
||||
int 21h
|
||||
|
||||
; ------------------------------------------------------------------
|
||||
; The handler runs with IF=0 and TF=0 (the CPU cleared them when it took the interrupt).
|
||||
handler:
|
||||
push bp
|
||||
mov bp, sp
|
||||
push ax
|
||||
; stack now: [bp+0]=saved BP, [bp+2]=return IP, [bp+4]=return CS, [bp+6]=FLAGS
|
||||
mov ax, [bp+2]
|
||||
mov [ret_ip], ax
|
||||
mov ax, [bp+6]
|
||||
mov [flags_pushed], ax
|
||||
pushf
|
||||
pop ax ; FLAGS as the handler itself sees them
|
||||
mov [flags_inside], ax
|
||||
pop ax
|
||||
pop bp
|
||||
iret
|
||||
|
||||
report:
|
||||
mov dx, m_delta
|
||||
call puts
|
||||
mov ax, [ret_ip]
|
||||
sub ax, [target] ; how many bytes past the trapping instruction?
|
||||
call putdec
|
||||
mov dx, m_ifpush
|
||||
call puts
|
||||
mov ax, [flags_pushed]
|
||||
mov cl, 9
|
||||
shr ax, cl
|
||||
and al, 1
|
||||
add al, '0'
|
||||
mov dl, al
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dx, m_ifin
|
||||
call puts
|
||||
mov ax, [flags_inside]
|
||||
mov cl, 9
|
||||
shr ax, cl
|
||||
and al, 1
|
||||
add al, '0'
|
||||
mov dl, al
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dx, m_tfin
|
||||
call puts
|
||||
mov ax, [flags_inside]
|
||||
mov cl, 8
|
||||
shr ax, cl
|
||||
and al, 1
|
||||
add al, '0'
|
||||
mov dl, al
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
mov dx, m_ifafter
|
||||
call puts
|
||||
pushf
|
||||
pop ax
|
||||
mov cl, 9
|
||||
shr ax, cl
|
||||
and al, 1
|
||||
add al, '0'
|
||||
mov dl, al
|
||||
mov ah, 02h
|
||||
int 21h
|
||||
jmp crlf
|
||||
|
||||
m_cc db 'CC (INT3): $'
|
||||
m_cd db 'CD 03 (INT 3): $'
|
||||
m_delta db 'return IP = trap address + $'
|
||||
m_ifpush db ', IF pushed = $'
|
||||
m_ifin db ', IF in handler = $'
|
||||
m_tfin db ', TF in handler = $'
|
||||
m_ifafter db ', IF after IRET = $'
|
||||
|
||||
old_off dw 0
|
||||
old_seg dw 0
|
||||
target dw 0
|
||||
ret_ip dw 0
|
||||
flags_pushed dw 0
|
||||
flags_inside dw 0
|
||||
|
||||
%include "common.inc"
|
||||
@@ -0,0 +1,9 @@
|
||||
; opcodes.asm -- which bytes does each spelling assemble to? (read the listing, never run)
|
||||
bits 16
|
||||
org 0x100
|
||||
int3 ; the one-byte breakpoint -> CC
|
||||
int 3 ; "INT n" with n = 3 in NASM -> CD 03
|
||||
db 0CDh, 03h ; the same two bytes, written out -> CD 03
|
||||
int 21h ; DOS entry point -> CD 21
|
||||
int 10h ; BIOS video -> CD 10
|
||||
into ; one-byte INTO -> CE
|
||||
@@ -0,0 +1,9 @@
|
||||
1 ; opcodes.asm -- which bytes does each spelling assemble to? (read the listing, never run)
|
||||
2 bits 16
|
||||
3 org 0x100
|
||||
4 00000000 CC int3 ; the one-byte breakpoint -> CC
|
||||
5 00000001 CD03 int 3 ; "INT n" with n = 3 in NASM -> CD 03
|
||||
6 00000003 CD03 db 0CDh, 03h ; the same two bytes, written out -> CD 03
|
||||
7 00000005 CD21 int 21h ; DOS entry point -> CD 21
|
||||
8 00000007 CD10 int 10h ; BIOS video -> CD 10
|
||||
9 00000009 CE into ; one-byte INTO -> CE
|
||||
@@ -0,0 +1,2 @@
|
||||
Hello from INT 21h, AH=09h
|
||||
ERRORLEVEL is at least 7
|
||||
@@ -0,0 +1,15 @@
|
||||
AH=30h DOS version : 5.0
|
||||
AH=2Ah/2Ch date+time ranges : ok ok
|
||||
AH=02h + AH=06h : AB
|
||||
AH=01h + AH=0Ah (stdin) : echo -> Ankur
|
||||
|
||||
AH=01h got 0x5A ; AH=0Ah count = 5, text = Ankur
|
||||
AH=25h/35h vector 60h : ok
|
||||
AH=48h/49h alloc+free 4 KB : ok
|
||||
AH=48h ask for 1 MB (fails) : CF=1 AX=0008 ; largest block (paragraphs) = 8E6C
|
||||
AH=3Ch/40h/3Dh/3Fh/42h : 12 bytes read ; LSEEK(end) says size = 12
|
||||
AH=56h + AH=4Eh findfirst : U.TXT size = 12
|
||||
AH=41h delete, reopen fails : CF=1 AX=0002
|
||||
AH=39h/3Ah mkdir+rmdir : ok ; AH=19h drive = 2
|
||||
AH=4Bh exec child : Hello from INT 21h, AH=09h
|
||||
AH=4Dh return code = 7 type = 0
|
||||
@@ -0,0 +1,2 @@
|
||||
CC (INT3): return IP = trap address + 1, IF pushed = 1, IF in handler = 0, TF in handler = 0, IF after IRET = 1
|
||||
CD 03 (INT 3): return IP = trap address + 2, IF pushed = 1, IF in handler = 0, TF in handler = 0, IF after IRET = 1
|
||||
@@ -0,0 +1,2 @@
|
||||
before INT3
|
||||
after INT3 (program survived)
|
||||
@@ -0,0 +1,26 @@
|
||||
-u 100 L9
|
||||
072E:0100 B81111 MOV AX,1111
|
||||
072E:0103 CC INT 3
|
||||
072E:0104 B82222 MOV AX,2222
|
||||
072E:0107 B8004C MOV AX,4C00
|
||||
-g
|
||||
Unexpected breakpoint interrupt
|
||||
AX=1111 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
|
||||
072E:0104 B82222 MOV AX,2222
|
||||
-r
|
||||
AX=1111 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
|
||||
072E:0104 B82222 MOV AX,2222
|
||||
-t
|
||||
AX=2222 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0107 NV UP EI PL ZR NA PE NC
|
||||
072E:0107 B8004C MOV AX,4C00
|
||||
-r
|
||||
AX=2222 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0107 NV UP EI PL ZR NA PE NC
|
||||
072E:0107 B8004C MOV AX,4C00
|
||||
-g
|
||||
|
||||
Program terminated normally (0000)
|
||||
-q
|
||||
@@ -0,0 +1,25 @@
|
||||
-u 100 L9
|
||||
072E:0100 B81111 MOV AX,1111
|
||||
072E:0103 CD03 INT 03
|
||||
072E:0105 B82222 MOV AX,2222
|
||||
072E:0108 B8004C MOV AX,4C00
|
||||
-g
|
||||
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
|
||||
072E:0104 03B82222 ADD DI,[BX+SI+2222] DS:2222=0000
|
||||
-r
|
||||
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
|
||||
072E:0104 03B82222 ADD DI,[BX+SI+2222] DS:2222=0000
|
||||
-t
|
||||
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0108 NV UP EI PL ZR NA PE NC
|
||||
072E:0108 B8004C MOV AX,4C00
|
||||
-r
|
||||
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
|
||||
DS=072E ES=072E SS=072E CS=072E IP=0108 NV UP EI PL ZR NA PE NC
|
||||
072E:0108 B8004C MOV AX,4C00
|
||||
-g
|
||||
|
||||
Program terminated normally (0000)
|
||||
-q
|
||||
@@ -0,0 +1,9 @@
|
||||
clean run, no breakpoint:
|
||||
CX=3
|
||||
|
||||
CC patch=CC trap: vector=3 return IP=7C03 (site=7C02, +1) debugger restores IP=7C02
|
||||
after resume: CX=3 (expected 3), final IP=7C05
|
||||
CD 03 patch=CD03 trap: vector=3 return IP=7C04 (site=7C02, +2) debugger restores IP=7C03
|
||||
after resume: CX=0 (expected 3), final IP=7C64
|
||||
|
||||
RESULT: PASS
|
||||
@@ -0,0 +1,8 @@
|
||||
Title: DEBUG
|
||||
Version: 2.50
|
||||
Entered-date: 2024-06-01
|
||||
Copying-policy: MIT License
|
||||
nasm: NASM version 2.16.01
|
||||
dosbox: DOSBox version 0.74-3
|
||||
unicorn: 2.1.4
|
||||
python: Python 3.11.15
|
||||
@@ -0,0 +1,31 @@
|
||||
PASS int3 assembles to the single byte CC
|
||||
PASS NASM 'int 3' assembles to CD 03
|
||||
PASS int 21h assembles to CD 21
|
||||
PASS into assembles to CE
|
||||
PASS hello21 prints its string
|
||||
PASS AH=4Ch return code 7 visible as ERRORLEVEL
|
||||
PASS AH=30h reports a version
|
||||
PASS AH=2Ah/2Ch ranges ok
|
||||
PASS AH=02h+06h write AB
|
||||
PASS AH=01h returned 5A ('Z') and AH=0Ah counted 5 chars 'Ankur'
|
||||
PASS AH=25h/35h round trip
|
||||
PASS AH=48h/49h alloc+free ok
|
||||
PASS AH=48h 1 MB request fails CF=1 AX=0008
|
||||
PASS file I/O: 12 bytes read, LSEEK says 12
|
||||
PASS findfirst sees U.TXT size 12
|
||||
PASS reopening deleted file: CF=1 AX=0002
|
||||
PASS mkdir/rmdir ok
|
||||
PASS child exit code 7, termination type 0 via AH=4Dh
|
||||
PASS CC: return IP = trap + 1
|
||||
PASS CD 03: return IP = trap + 2
|
||||
PASS IF and TF are 0 inside the handler, IF back to 1 after IRET
|
||||
PASS IF pushed on the stack was 1
|
||||
PASS INT 3 with no handler: program survives under DOSBox
|
||||
PASS DEBUG + CC: 'Unexpected breakpoint interrupt' and IP=0104
|
||||
PASS DEBUG + CC: next instruction after trap is MOV AX,2222
|
||||
PASS DEBUG + CC: single-step then AX=2222
|
||||
PASS DEBUG + CD 03: no breakpoint message
|
||||
PASS DEBUG + CD 03: stops at IP=0104 inside the INT 03 instruction
|
||||
PASS DEBUG + CD 03: MOV AX,2222 never runs (AX stays 1111 after stepping)
|
||||
PASS Unicorn: CC patch trap return IP = site+1, resume gives CX=3
|
||||
PASS Unicorn: CD 03 patch trap return IP = site+2 and resume goes wrong
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mini software debugger on Unicorn (16-bit x86 core).
|
||||
|
||||
Puts a breakpoint on the first INC CX of bp_target.asm twice: once patched with the
|
||||
one-byte CC, once with the two-byte CD 03. A real debugger saves ONE byte (because the
|
||||
breakpoint opcode is one byte) and, on a trap, backs IP up by ONE. The experiment shows
|
||||
why that only works for CC.
|
||||
|
||||
Unicorn is a modern x86 core in real mode, not an 8086: it is used here only for
|
||||
instruction-decoding and control-flow facts (opcode lengths, return addresses).
|
||||
"""
|
||||
import subprocess, sys, pathlib
|
||||
from unicorn import Uc, UcError, UC_ARCH_X86, UC_MODE_16, UC_HOOK_INTR
|
||||
from unicorn.x86_const import UC_X86_REG_IP, UC_X86_REG_CX, UC_X86_REG_EFLAGS, UC_X86_REG_CS
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
ASM = HERE.parent / "asm" / "bp_target.asm"
|
||||
BASE = 0x7C00
|
||||
|
||||
def assemble():
|
||||
out = HERE.parent / "build" / "bp_target.bin"
|
||||
out.parent.mkdir(exist_ok=True)
|
||||
subprocess.run(["nasm", "-f", "bin", str(ASM), "-o", str(out)], check=True)
|
||||
return out.read_bytes()
|
||||
|
||||
def run(code, patch, label):
|
||||
SITE, DONE = BASE + 2, BASE + 5 # offsets fixed by the listing (xor cx,cx = 2 bytes)
|
||||
uc = Uc(UC_ARCH_X86, UC_MODE_16)
|
||||
uc.mem_map(0, 0x100000)
|
||||
uc.mem_write(BASE, code)
|
||||
saved = bytes(uc.mem_read(SITE, 1)) # a debugger saves exactly ONE byte: CC is one byte
|
||||
uc.mem_write(SITE, patch)
|
||||
uc.reg_write(UC_X86_REG_CS, 0)
|
||||
uc.reg_write(UC_X86_REG_EFLAGS, 0x202)
|
||||
events = []
|
||||
def on_int(uc, intno, _):
|
||||
ip = uc.reg_read(UC_X86_REG_IP) # return address (first byte after the trapping instruction)
|
||||
events.append((intno, ip))
|
||||
if intno == 3:
|
||||
bp = ip - 1 # "back up one byte" -- only right for CC
|
||||
if bp == SITE:
|
||||
uc.mem_write(SITE, saved) # restore the original byte, re-run it
|
||||
uc.reg_write(UC_X86_REG_IP, bp)
|
||||
uc.hook_add(UC_HOOK_INTR, on_int)
|
||||
err = None
|
||||
try:
|
||||
uc.emu_start(BASE, DONE, count=50)
|
||||
except UcError as e:
|
||||
err = str(e)
|
||||
cx = uc.reg_read(UC_X86_REG_CX)
|
||||
ip = uc.reg_read(UC_X86_REG_IP)
|
||||
print("%-8s patch=%-6s trap: vector=%d return IP=%04X (site=%04X, +%d) " % (
|
||||
label, patch.hex().upper(), events[0][0], events[0][1], SITE, events[0][1] - SITE) +
|
||||
"debugger restores IP=%04X" % (events[0][1] - 1))
|
||||
print(" after resume: CX=%d (expected 3), final IP=%04X%s" % (cx, ip, (" [" + err + "]") if err else ""))
|
||||
return cx, err
|
||||
|
||||
def main():
|
||||
code = assemble()
|
||||
print("clean run, no breakpoint:")
|
||||
uc = Uc(UC_ARCH_X86, UC_MODE_16); uc.mem_map(0, 0x100000); uc.mem_write(BASE, code)
|
||||
uc.reg_write(UC_X86_REG_CS, 0); uc.emu_start(BASE, BASE + 5, count=50)
|
||||
print(" CX=%d\n" % uc.reg_read(UC_X86_REG_CX))
|
||||
cx1, e1 = run(code, b"\xCC", "CC")
|
||||
cx2, e2 = run(code, b"\xCD\x03", "CD 03")
|
||||
ok = (cx1 == 3 and e1 is None and cx2 != 3)
|
||||
print("\nRESULT:", "PASS" if ok else "UNEXPECTED")
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assertions that pin every claim the article makes about the captured output."""
|
||||
import pathlib, re, sys
|
||||
OUT = pathlib.Path(__file__).resolve().parent.parent / "output"
|
||||
fails = []
|
||||
def rd(n): return (OUT / n).read_text()
|
||||
def check(name, cond):
|
||||
print(("PASS " if cond else "FAIL ") + name)
|
||||
if not cond: fails.append(name)
|
||||
|
||||
lst = rd("01-opcodes.lst")
|
||||
check("int3 assembles to the single byte CC", re.search(r" CC\s+int3", lst) is not None)
|
||||
check("NASM 'int 3' assembles to CD 03", re.search(r" CD03\s+int 3", lst) is not None)
|
||||
check("int 21h assembles to CD 21", re.search(r" CD21\s+int 21h", lst) is not None)
|
||||
check("into assembles to CE", re.search(r" CE\s+into", lst) is not None)
|
||||
|
||||
t = rd("02-hello21.txt")
|
||||
check("hello21 prints its string", "Hello from INT 21h, AH=09h" in t)
|
||||
check("AH=4Ch return code 7 visible as ERRORLEVEL", "ERRORLEVEL is at least 7" in t)
|
||||
|
||||
t = rd("03-fn-tour.txt")
|
||||
check("AH=30h reports a version", re.search(r"AH=30h DOS version\s+: \d+\.\d+", t) is not None)
|
||||
check("AH=2Ah/2Ch ranges ok", "okok" not in t and re.search(r"ranges : ok ok", t) is not None)
|
||||
check("AH=02h+06h write AB", re.search(r"AH=06h\s+: AB", t) is not None)
|
||||
check("AH=01h returned 5A ('Z') and AH=0Ah counted 5 chars 'Ankur'", "AH=01h got 0x5A ; AH=0Ah count = 5, text = Ankur" in t)
|
||||
check("AH=25h/35h round trip", re.search(r"vector 60h\s+: ok", t) is not None)
|
||||
check("AH=48h/49h alloc+free ok", re.search(r"alloc\+free 4 KB\s+: ok", t) is not None)
|
||||
check("AH=48h 1 MB request fails CF=1 AX=0008", "(fails) : CF=1 AX=0008" in t)
|
||||
check("file I/O: 12 bytes read, LSEEK says 12", "12 bytes read ; LSEEK(end) says size = 12" in t)
|
||||
check("findfirst sees U.TXT size 12", "U.TXT size = 12" in t)
|
||||
check("reopening deleted file: CF=1 AX=0002", "CF=1 AX=0002" in t)
|
||||
check("mkdir/rmdir ok", "mkdir+rmdir : ok" in t)
|
||||
check("child exit code 7, termination type 0 via AH=4Dh", "AH=4Dh return code = 7 type = 0" in t)
|
||||
|
||||
t = rd("04-int3-handler.txt")
|
||||
check("CC: return IP = trap + 1", "CC (INT3): return IP = trap address + 1" in t)
|
||||
check("CD 03: return IP = trap + 2", "CD 03 (INT 3): return IP = trap address + 2" in t)
|
||||
check("IF and TF are 0 inside the handler, IF back to 1 after IRET",
|
||||
t.count("IF in handler = 0, TF in handler = 0, IF after IRET = 1") == 2)
|
||||
check("IF pushed on the stack was 1", t.count("IF pushed = 1") == 2)
|
||||
|
||||
t = rd("05-int3-default.txt")
|
||||
check("INT 3 with no handler: program survives under DOSBox", "after INT3 (program survived)" in t)
|
||||
|
||||
if (OUT / "06-debug-cc.txt").exists():
|
||||
t = rd("06-debug-cc.txt")
|
||||
check("DEBUG + CC: 'Unexpected breakpoint interrupt' and IP=0104", "Unexpected breakpoint interrupt" in t and "IP=0104" in t)
|
||||
check("DEBUG + CC: next instruction after trap is MOV AX,2222", "0104 B82222 MOV AX,2222" in t)
|
||||
check("DEBUG + CC: single-step then AX=2222", "AX=2222" in t)
|
||||
t = rd("07-debug-cd03.txt")
|
||||
check("DEBUG + CD 03: no breakpoint message", "Unexpected breakpoint interrupt" not in t)
|
||||
check("DEBUG + CD 03: stops at IP=0104 inside the INT 03 instruction", "IP=0104" in t and "03B82222 ADD DI,[BX+SI+2222]" in t)
|
||||
check("DEBUG + CD 03: MOV AX,2222 never runs (AX stays 1111 after stepping)", "AX=2222" not in t)
|
||||
else:
|
||||
print("SKIP DEBUG checks (run scripts/fetch-debug.sh first)")
|
||||
|
||||
t = rd("08-bp-experiment.txt")
|
||||
check("Unicorn: CC patch trap return IP = site+1, resume gives CX=3", "CC patch=CC trap: vector=3 return IP=7C03 (site=7C02, +1)" in t and "CX=3 (expected 3)" in t)
|
||||
check("Unicorn: CD 03 patch trap return IP = site+2 and resume goes wrong", "return IP=7C04 (site=7C02, +2)" in t and "CX=0 (expected 3)" in t)
|
||||
|
||||
sys.exit(1 if fails else 0)
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble the .COM demos with NASM and run them inside DOSBox (headless).
|
||||
|
||||
DOSBox provides a real MS-DOS-compatible INT 21h implementation (and its own INT 3
|
||||
default vector), so this is where the INT 21h and INT 3 behaviour is actually executed.
|
||||
"""
|
||||
import os, pathlib, re, shutil, subprocess, sys, tempfile
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
ASM, BUILD, OUT = ROOT / "asm", ROOT / "build", ROOT / "output"
|
||||
BUILD.mkdir(exist_ok=True); OUT.mkdir(exist_ok=True)
|
||||
|
||||
PROGRAMS = { # source -> DOS 8.3 name
|
||||
"hello21": "HELLO21.COM", "fn_tour": "TOUR.COM", "int3_handler": "I3H.COM",
|
||||
"int3_default": "I3D.COM", "dbg_cc": "DBGCC.COM", "dbg_cd03": "DBGCD.COM",
|
||||
}
|
||||
|
||||
def nasm(src, out, listing=None):
|
||||
cmd = ["nasm", "-f", "bin", str(ASM / (src + ".asm")), "-o", str(out)]
|
||||
if listing:
|
||||
cmd += ["-l", str(listing)]
|
||||
subprocess.run(cmd, check=True, cwd=ASM)
|
||||
|
||||
def clean(text):
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
return "\n".join(l.rstrip() for l in text.split("\n")).rstrip("\n") + "\n"
|
||||
|
||||
def main():
|
||||
# 1. opcode listing (assembled, never executed)
|
||||
nasm("opcodes", BUILD / "opcodes.bin", OUT / "01-opcodes.lst")
|
||||
lst = (OUT / "01-opcodes.lst").read_text()
|
||||
lst = "\n".join(l for l in lst.split("\n") if l.strip())
|
||||
(OUT / "01-opcodes.lst").write_text(lst + "\n")
|
||||
|
||||
work = pathlib.Path(tempfile.mkdtemp(prefix="dosbox_"))
|
||||
for src, dos in PROGRAMS.items():
|
||||
nasm(src, work / dos)
|
||||
have_debug = (BUILD / "DEBUG.COM").exists()
|
||||
if have_debug:
|
||||
shutil.copy(BUILD / "DEBUG.COM", work / "DEBUG.COM")
|
||||
(work / "IN.TXT").write_bytes(b"ZAnkur\r\n") # stdin for fn_tour: AH=01h reads Z, AH=0Ah reads Ankur
|
||||
# DEBUG script: list code, run to the breakpoint, show registers, single-step once, show registers, run on, quit
|
||||
(work / "DC.TXT").write_bytes(b"u 100 L9\r\ng\r\nr\r\nt\r\nr\r\ng\r\nq\r\n")
|
||||
|
||||
steps = [
|
||||
("HELLO21.COM > O1.TXT", None),
|
||||
("if errorlevel 7 echo ERRORLEVEL is at least 7 >> O1.TXT", None),
|
||||
("TOUR.COM < IN.TXT > O2.TXT", None),
|
||||
("I3H.COM > O3.TXT", None),
|
||||
("I3D.COM > O4.TXT", None),
|
||||
]
|
||||
if have_debug:
|
||||
steps += [("DEBUG DBGCC.COM < DC.TXT > O5.TXT", None), ("DEBUG DBGCD.COM < DC.TXT > O6.TXT", None)]
|
||||
conf = "[sdl]\nfullscreen=false\n[autoexec]\nmount c %s\nc:\n%s\nexit\n" % (work, "\n".join(s for s, _ in steps))
|
||||
(work / "run.conf").write_text(conf)
|
||||
env = dict(os.environ, SDL_VIDEODRIVER="dummy", SDL_AUDIODRIVER="dummy")
|
||||
subprocess.run(["dosbox", "-conf", str(work / "run.conf"), "-noconsole"], env=env, timeout=180,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
names = {"O1.TXT": "02-hello21.txt", "O2.TXT": "03-fn-tour.txt", "O3.TXT": "04-int3-handler.txt",
|
||||
"O4.TXT": "05-int3-default.txt", "O5.TXT": "06-debug-cc.txt", "O6.TXT": "07-debug-cd03.txt"}
|
||||
for src, dst in names.items():
|
||||
p = work / src
|
||||
if p.exists():
|
||||
(OUT / dst).write_text(clean(p.read_text(errors="replace")))
|
||||
else:
|
||||
print("missing", src, "(DEBUG.COM not fetched? run scripts/fetch-debug.sh)", file=sys.stderr)
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download FreeDOS "Debug" (MIT licence, a DEBUG.EXE-compatible debugger) into ../build/.
|
||||
# It is NOT committed to the repository.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
mkdir -p build
|
||||
if [ ! -f build/DEBUG.COM ]; then
|
||||
curl -fsSL -o build/debug.zip https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/repositories/1.3/base/debug.zip
|
||||
unzip -o -q -j build/debug.zip BIN/DEBUG.COM APPINFO/DEBUG.LSM -d build
|
||||
fi
|
||||
grep -E '^(Title|Version|Entered-date|Copying-policy):' build/DEBUG.LSM || true
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rebuild everything in output/ and check it. Needs: nasm, dosbox, python3 with `unicorn`.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
./fetch-debug.sh > ../output/09-tool-versions.txt || true
|
||||
{
|
||||
echo "nasm: $(nasm -v)"
|
||||
echo "dosbox: $(dosbox --version 2>/dev/null | grep -i 'version' | head -1 | sed 's/, copyright.*//')"
|
||||
echo "unicorn: $(python3 -c 'import unicorn;print(unicorn.__version__)')"
|
||||
echo "python: $(python3 --version)"
|
||||
} | grep -v 'Picked up' >> ../output/09-tool-versions.txt
|
||||
python3 dosbox_run.py
|
||||
python3 bp_experiment.py > ../output/08-bp-experiment.txt
|
||||
python3 check.py | tee ../output/10-checks.txt
|
||||
Reference in New Issue
Block a user