I'm trying to replace the int 21h vector with a custom routine in MS-DOS. This method works for the timer tick interrupt (1Ch), but for some reason hangs after around 20 calls to 21h.
My code is currently (WASM style):
.model tiny
.code
org 100h
start:
jmp install
; -----------------------------------------------------------------------------
; RESIDENT PART
; -----------------------------------------------------------------------------
old_int21_vector dd 0
new_int21_handler:
pushf
call dword ptr cs:old_int21_vector ; Let DOS do its thang.
return_point:
call print_star
iret
print_star:
push ax
push bx
mov ah, 0Eh
mov al, '*'
mov bx, 0
int 10h
pop bx
pop ax
ret
resident_end label byte
; -----------------------------------------------------------------------------
; INSTALLATION PART
; -----------------------------------------------------------------------------
install:
; Get the old vector
mov ax, 3521h
int 21h
mov word ptr old_int21_vector[0], bx
mov word ptr old_int21_vector[2], es
; Install the new vector
mov dx, offset new_int21_handler
mov ax, 2521h
int 21h
; Leave the program resident
mov ax, 3100h
mov dx, offset resident_end
add dx, 15
mov cl, 4
shr dx, cl
int 21h
end start
And at the DOS prompt, I get:
C:\PROJECTS\TEST>test
************************
***C:\PROJECTS\TEST**_
where "_" is the flashing cursor.
Removing the print_star function has no impact. I've tried manually rebuilding the stack pointer to where I think it should return, but I think there's something I'm missing. Is there something special that needs to be done for this interrupt?
Note: the code is very minimal right now, but eventually the plan is to check which function is being called and only respond on certain functions. Specifically, I wanted to hook the call to change directory.
You have to pass the current flags (especially Zero Flag and Carry Flag) after the call to your downlink back to your caller. This is because many DOS functions return a meaningful status in the Carry Flag and also some functions use the Zero Flag as well. Your iret
uses the original caller's flags on the stack, so you should modify the fl
word in your iret
frame on the stack to pass along the DOS call's returned flags.
At return_point
try:
push bp
mov bp, sp
push ax
lahf
mov byte ptr [bp + 6], ah
pop ax
pop bp
After this your iret
should pop the correct flags.
Other than that, calls with input ah
of 0, 26h, 4Bh, 4Ch, 31h may need to be special cased, you should detect them and pass these through to your downlink with a far jump rather than pushf
+ far call.
Functions 0 and 26h use the caller's CS as an input (at least on original MS-DOS), while 31h, 4Ch, and 4Bh use the stack in special ways for process termination handling. This is also why dosemu2 passes these functions like a far jump rather than a pushf
+ far call.
Here's a working example. I uploaded an earlier version to our server on https://pushbx.org/ecm/test/20250729 but I modified it now to pass along the original flags when chaining to the downlink and in the live flags when calling the downlink. This is better because some DOS calls are known to return Carry Flag unchanged (eg LFN functions 71h, Extended Open/Create function 6Ch, if not supported).
Update: I added some code which I would recommend to include in any TSR that uses int 21h function 31h. The first bit is about freeing the program's environment block. The second is to close all file handles, as you will leak System File Table (SFT) entries if someone runs your program with redirection, such as test.com > nul
. Of course, both the environment and the file handles could be used by a resident program, in which case you want to retain them. But the vast majority of TSRs do not use either after the TSR terminate, so they should be released.
.model tiny
.code
org 100h
start:
jmp install
; -----------------------------------------------------------------------------
; RESIDENT PART
; -----------------------------------------------------------------------------
old_int21_vector dd 0
new_int21_handler:
pushf
cmp ah, 0
je chain_popf
cmp ah, 26h
je chain_popf
cmp ah, 31h
je chain_popf
cmp ah, 4Bh
je chain_popf
cmp ah, 4Ch
je chain_popf
popf
pushf
call dword ptr cs:old_int21_vector ; Let DOS do its thang.
return_point:
push bp
mov bp, sp
push ax
lahf
mov byte ptr [bp + 6], ah
pop ax
pop bp
call print_star
iret
chain_popf:
popf
jmp dword ptr cs:old_int21_vector
print_star:
push ax
push bx
mov ah, 0Eh
mov al, '*'
mov bx, 0
int 10h
pop bx
pop ax
ret
resident_end label byte
; -----------------------------------------------------------------------------
; INSTALLATION PART
; -----------------------------------------------------------------------------
install:
; Get the old vector
mov ax, 3521h
int 21h
mov word ptr old_int21_vector[0], bx
mov word ptr old_int21_vector[2], es
; Install the new vector
mov dx, offset new_int21_handler
mov ax, 2521h
int 21h
xor ax, ax
xchg ax, word ptr ds:[2Ch]
mov es, ax
mov ah, 49h
int 21h
xor bx, bx
mov cx, word ptr ds:[32h]
@@:
mov ah, 3Eh
int 21h
inc bx
loop @B
; Leave the program resident
mov ax, 3100h
mov dx, offset resident_end
add dx, 15
mov cl, 4
shr dx, cl
int 21h
end start
Builds like this:
$ jwasm test.asm
$ warplink.sh /mx test.o,test.exe,test.map;
$ x2b2.sh test.exe test.com