flat assembler
Message board for the users of flat assembler.

Index > DOS > Hook to execute something on every program termination

Author
Thread Post new topic Reply to topic
OgreVorbis



Joined: 20 Mar 2025
Posts: 3
OgreVorbis 20 Mar 2025, 02:10
I've been trying to accomplish this for a while now. It's been forever since I've coded in ASM and I never was very good at it even back in the day.

What I am trying to accomplish is a DOS TSR that hooks whatever INT occurs when any program terminates. What I want to do is run a batch file when any program terminates and goes back to the command.com. Why am I doing this? It's because I like to use DOS in a custom graphics mode (which I have a batch file that switches to), but every time I end a game, it goes back to the old default 80x25 lines. I want it to auto-run my batch file whenever a game ends. I thought about other options, but it would require me to have a custom batch file in every single game's folder. Not to mention, I like hacking and I want to figure this out. There must be a way.

So I asked ChatGPT. Here's what it gave me: (which wouldn't compile due to a size mismatch on line 36. Shouldn't "old_int21 dd" be "old_int21 dw"?)

Code:
org 100h  ; .COM file format (simpler for TSR)

jmp install_tsr

; Data Section
dos_terminate equ 4Ch  ; DOS function to terminate program
batch_filename db "TM.BAT",0  ; Batch file to execute

old_int21 dd ?  ; Placeholder for old INT 21h handler

; Hooked INT 21h handler
int21_hook:
    cmp ah, dos_terminate  ; Check if function 4Ch is called
    jne next_handler
    
    ; Call batch file before terminating
    push ax
    push dx
    
    mov dx, batch_filename
    mov ah, 4Bh  ; Execute program
    mov al, 0    ; Load and execute
    int 21h      ; Call DOS
    
    pop dx
    pop ax

next_handler:
    jmp far [cs:old_int21]  ; Call original INT 21h

; Installation routine
install_tsr:
    cli
    mov ax, 3521h  ; Get old INT 21h handler
    int 21h
    mov [old_int21], bx
    mov [old_int21+2], es  ; Store old handler
    
    mov dx, int21_hook
    mov ax, 2521h  ; Set new INT 21h handler
    int 21h
    sti
    
    ; Stay resident (allocate memory for TSR)
    mov dx, (end_program - install_tsr + 15) / 16  ; Calculate memory in paragraphs
    mov ah, 31h  ; DOS stay-resident function
    int 21h

end_program:
    


So I corrected the AI, and then it changed this: (BTW: I still think the old_int21 should be dw, right)

Code:
next_handler:
    jmp far [cs:old_int21]  ; Call original INT 21h
    


To this:

Code:
next_handler:
    db 0EAh  ; Opcode for FAR JMP
    dw old_int21  ; Offset
    dw old_int21+2  ; Segment
    
Post 20 Mar 2025, 02:10
View user's profile Send private message Visit poster's website Reply with quote
revolution
When all else fails, read the source


Joined: 24 Aug 2004
Posts: 20625
Location: In your JS exploiting you and your system
revolution 20 Mar 2025, 03:35
Opcode 0xea is a direct jump.

You need an indirect jump:
Code:
somewhere:
        dw 0x1234,0x5678
        ;...
        jmp far [somewhere]    


Last edited by revolution on 20 Mar 2025, 03:36; edited 1 time in total
Post 20 Mar 2025, 03:35
View user's profile Send private message Visit poster's website Reply with quote
macomics



Joined: 26 Jan 2021
Posts: 1149
Location: Russia
macomics 20 Mar 2025, 03:36
That's how it's supposed to work. But it still need to provide at least 8KB of free memory to run. (see ah=4a/int 21h)
Code:
org 100h  ; .COM file format (simpler for TSR)

jmp install_tsr

; Data Section
dos_terminate equ 4Ch  ; DOS function to terminate program
command_com    db "\COMMAND.COM",0
batch_filename db 7,"\TM.BAT"  ; Batch file to execute
null_string    db 0
execParamBlock:
.envOffset     dw 0
.envSegment    dw 0
.argOffset     dw batch_filename
.argSegment    dw 0
.inputHandle   dw 0
.inputSegment  dw 0
.outputHandle  dw 0
.outputSegment dw 0

old_int21 dd ?  ; Placeholder for old INT 21h handler
old_stack dd ?

; Hooked INT 21h handler
int21_hook:
    cmp  ah, dos_terminate  ; Check if function 4Ch is called
    jne  next_handler
    
    ; Call batch file before terminating
    pushad
    push ds
    push es
    mov  word [cs:old_stack], sp
    mov  word [cs:old_stack+2], ss

    mov  [execParamBlock.envSegment], cs ; PSP
    mov  [execParamBlock.argSegment], cs ; CS:batch_filename
    mov  [execParamBlock.inputSegment], cs
    mov  [execParamBlock.outputSegment], cs
    push cs
    pop  ds
    push cs
    pop  es
    mov  bx, execParamBlock
    mov  dx, command_com
    mov  ah, 4Bh  ; Execute program
    mov  al, 0    ; Load and execute
    int  21h      ; Call DOS

    lss  sp, [cs:ols_stack]
    pop  es
    pop  ds
    popad

next_handler:
    jmp  far [cs:old_int21]  ; Call original INT 21h

; Installation routine
install_tsr:
    cli
    mov ax,  3521h  ; Get old INT 21h handler
    int 21h
    mov word [old_int21],   bx
    mov word [old_int21+2], es  ; Store old handler

    push cs
    pop  ds
    mov  dx, int21_hook
    mov  ax, 2521h  ; Set new INT 21h handler
    int  21h
    sti
    
    ; Stay resident (allocate memory for TSR)
    mov  dx, (end_program - install_tsr + 15) / 16  ; Calculate memory in paragraphs
    mov  ah, 31h  ; DOS stay-resident function
    int  21h

end_program:    
Post 20 Mar 2025, 03:36
View user's profile Send private message Reply with quote
Core i7



Joined: 14 Nov 2024
Posts: 47
Location: Socket on motherboard
Core i7 20 Mar 2025, 04:02
ax=3521h corrupts ES, so it needs to be preserved
Post 20 Mar 2025, 04:02
View user's profile Send private message Reply with quote
macomics



Joined: 26 Jan 2021
Posts: 1149
Location: Russia
macomics 20 Mar 2025, 04:10
ver 2.0
Code:
org 100h  ; .COM file format (simpler for TSR)

jmp install_tsr

; Data Section
dos_terminate equ 4Ch  ; DOS function to terminate program
command_com    db "\COMMAND.COM",0
batch_filename db 10,"/C \TM.BAT"  ; Batch file to execute
null_string    db 0
execParamBlock:
.envSegment    dw 0
.argOffset     dw batch_filename
.argSegment    dw 0
.inputHandle   dw fcb0
.inputSegment  dw 0
.outputHandle  dw fcb1
.outputSegment dw 0
fcb0:
.drive         db 2
.file_name     db '        ', '   '
.block         db 20 dup (0)
.rec_curr      db 0
.rec_numb      db 0,0,0
fcb1:
.drive         db 2
.file_name     db '        ', '   '
.block         db 20 dup (0)
.rec_curr      db 0
.rec_numb      db 0,0,0

old_int21  dd ?  ; Placeholder for old INT 21h handler
old_stack  dd ?
old_mysssp dd ?

; Hooked INT 21h handler
int21_hook:
    cmp  ah, dos_terminate  ; Check if function 4Ch is called
    jne  next_handler

    ; Call batch file before terminating
    mov  word [cs:old_stack], sp ; Save stack pointer of the current program
    mov  word [cs:old_stack+2], ss
    mov  word [cs:old_mysssp], stack_area + 254 ; Switching to the stack block for this resident
    mov  word [cs:old_mysssp+2],cs
    lss  sp,  [cs:old_mysssp]
    pushad  ; Save all general-purpose registers for the current program
    push ds ; and data segment registers
    push es

    mov  ax, 2521h ; Remove the hook so that the program does not call us before exiting.
    lds  dx, [old_int21]
    int  21h

    push cs ; Truncating the memory of this resident so that there is a place to load the new program.
    pop  es
    mov  bx, (end_program - 8177) / 16
    mov  ah, 4Ah
    int  21h

    push cs ; Run program
    pop  ds
    push cs
    pop  es
    mov  word [cs:old_mysssp], sp
    mov  word [cs:old_mysssp+2], ss
    mov  [execParamBlock.envSegment], cs ; PSP
    mov  [execParamBlock.argSegment], cs ; CS:batch_filename
    mov  [execParamBlock.inputSegment], cs
    mov  [execParamBlock.outputSegment], cs
    mov  bx, execParamBlock
    mov  dx, command_com
    mov  ah, 4Bh  ; Execute program
    mov  al, 0    ; Load and execute
    int  21h      ; Call DOS

    lss  sp, [cs:old_mysssp] ; Restoring the stack pointer value before calling the program

    call set_hook            ; Restoring the hook

    push cs                  ; Restoring a memory block
    pop  es
    mov  bx, (end_program + 15) / 16
    mov  ah, 4Ah
    int  21h

    pop  es                  ; Restoring registers to the pre-interrupt state
    pop  ds
    popad
    lss  sp, [cs:ols_stack]

next_handler:
    jmp  far [cs:old_int21]  ; Call original INT 21h

set_hook:
    push cs
    pop  ds
    mov  dx, int21_hook
    mov  ax, 2521h  ; Set new INT 21h handler
    int  21h
    retn

; Installation routine
install_tsr:
    cli
    mov ax,  3521h  ; Get old INT 21h handler
    int 21h
    mov word [old_int21],   bx
    mov word [old_int21+2], es  ; Store old handler
    call set_hook
    sti
    
    ; Stay resident (allocate memory for TSR)
    mov  dx, (end_program - install_tsr + 15) / 16  ; Calculate memory in paragraphs
    mov  ah, 31h  ; DOS stay-resident function
    int  21h

stack_area rb 256
free_block rb 8192
end_program:    
Post 20 Mar 2025, 04:10
View user's profile Send private message Reply with quote
OgreVorbis



Joined: 20 Mar 2025
Posts: 3
OgreVorbis 21 Mar 2025, 05:35
Thanks for the help, but neither of those work.
First, I had to correct the line "lss sp, [cs:ols_stack]".
It says "ols" instead of "old". Easy fix and now the program compiles. Geat!
After fixing the typo, the first program doesn't work though. The TSR does seem to install itself correctly, but what ends up happening is that whenever any program terminates, it just starts beeping the PC speaker constantly and locks up the OS. So it's not working correctly, but it does seem to install the TSR and run at the appropriate time.

The second version won't compile even after fixing the typo of "ols" to "old". It has a synatx error on line 21. It says there are trailing characters after "db 20 dup (0)". However, to me it seems there is nothing wrong, so I suspect maybe there is a problem with the version of FASM I am running. I think it's version 1.6 if I remember correctly.

I'll try to update my FASM to see if that fixes it.

===========================================================================================

I am not sure why you need command.com written in there. Can't it just run the batch file, or does it have to run it in a new instance of command.com? Just to be more clear. The TM.BAT is located in C:\DOS and is part of the PATH, hence why I don't write a path for it.
Post 21 Mar 2025, 05:35
View user's profile Send private message Visit poster's website Reply with quote
OgreVorbis



Joined: 20 Mar 2025
Posts: 3
OgreVorbis 21 Mar 2025, 06:07
OK, so I did find out that the reason V2.0 wasn't compiling was because of an old version of FASM V1.6.
So I updated and the program compiled successfully.

But, well, it doesn't work though. It no longer crashes the PC when exiting other programs, but it just doesn't do anything. A few minutes later the PC just locked up though, so...

I have a feeling this is one of those problems that will never be solved properly. It's going way over my head. I can't believe DOS has this annoying problem. In windows, when you terminate a program which uses a different screen resolution than the OS, it will change the resolution back.
Post 21 Mar 2025, 06:07
View user's profile Send private message Visit poster's website Reply with quote
macomics



Joined: 26 Jan 2021
Posts: 1149
Location: Russia
macomics 21 Mar 2025, 07:56
OgreVorbis wrote:
After fixing the typo, the first program doesn't work though. The TSR does seem to install itself correctly, but what ends up happening is that whenever any program terminates, it just starts beeping the PC speaker constantly and locks up the OS. So it's not working correctly, but it does seem to install the TSR and run at the appropriate time.
for 1.6
Code:
org 100h  ; .COM file format (simpler for TSR)

jmp install_tsr
struc EPB { ; EXEC parameters block
  .envSegment    dw 0
  .argOffset     dw 0
  .argSegment    dw 0
  .inputHandle   dw 0
  .inputSegment  dw 0
  .outputHandle  dw 0
  .outputSegment dw 0
}
struc FCB { ; File control block
  .drive         db 2
  .file_name     db '        ', '   '
  .block         db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  .rec_curr      db 0
  .rec_numb      db 0,0,0
}
; Data Section
dos_terminate equ 4Ch  ; DOS function to terminate program
command_com    db "\COMMAND.COM",0
batch_filename db 10,"/C \TM.BAT"  ; Batch file to execute
null_string    db 0
execParamBlock EPB
fcb0 FCB
fcb1 FCB

old_int21  dd ?  ; Placeholder for old INT 21h handler
old_stack  dd ?
old_mysssp dd ?

; Hooked INT 21h handler
int21_hook:
    cmp  ah, dos_terminate  ; Check if function 4Ch is called
    jne  next_handler

    ; Call batch file before terminating
    mov  word [cs:old_stack], sp ; Save stack pointer of the current program
    mov  word [cs:old_stack+2], ss
    mov  word [cs:old_mysssp], stack_area + 254 ; Switching to the stack block for this resident
    mov  word [cs:old_mysssp+2],cs
    lss  sp,  [cs:old_mysssp]
    pushad  ; Save all general-purpose registers for the current program
    push ds ; and data segment registers
    push es

    mov  ax, 2521h ; Remove the hook so that the program does not call us before exiting.
    lds  dx, [cs:old_int21]
    int  21h

    push cs ; Truncating the memory of this resident so that there is a place to load the new program.
    pop  es
    mov  bx, (end_program - 8177) / 16
    mov  ah, 4Ah
    int  21h

    push cs ; Run program
    pop  ds
    push cs
    pop  es
    mov  word [old_mysssp], sp
    mov  word [old_mysssp+2], ss
    mov  [execParamBlock.envSegment], cs ; PSP
    mov  [execParamBlock.argOffset], batch_filename
    mov  [execParamBlock.argSegment], cs ; CS:batch_filename
    mov  [execParamBlock.inputHandle], fcb0
    mov  [execParamBlock.inputSegment], cs
    mov  [execParamBlock.outputHandle], fcb1
    mov  [execParamBlock.outputSegment], cs
    mov  bx, execParamBlock
    mov  dx, command_com
    mov  ax, 4B00h  ; Execute program (Load and execute)
    int  21h      ; Call DOS

    push cs
    pop  ds
    lss  sp, [old_mysssp] ; Restoring the stack pointer value before calling the program

    call set_hook            ; Restoring the hook

    push cs                  ; Restoring a memory block
    pop  es
    mov  bx, (end_program + 15) / 16
    mov  ah, 4Ah
    int  21h

    pop  es                  ; Restoring registers to the pre-interrupt state
    pop  ds
    popad
    lss  sp, [cs:old_stack]

next_handler:
    jmp  far [cs:old_int21]  ; Call original INT 21h

set_hook:
    mov  dx, int21_hook
    mov  ax, 2521h  ; Set new INT 21h handler
    int  21h
    retn

; Installation routine
install_tsr:
    cli
    mov ax,  3521h  ; Get old INT 21h handler
    int 21h
    mov word [old_int21],   bx
    mov word [old_int21+2], es  ; Store old handler
    call set_hook
    sti
    
    ; Stay resident (allocate memory for TSR)
    mov  dx, (end_program - install_tsr + 15) / 16  ; Calculate memory in paragraphs
    mov  ah, 31h  ; DOS stay-resident function
    int  21h

stack_area rb 256
free_block rb 8192
end_program:    
Here's an option for 1.6, but you'd better download the latest version 1.73.32. I rechecked all memory accesses in the program. There were several mistakes that caused it to end up in the wrong addresses.

But in general, I have shown you how the program should be built.
1) Before starting a new program, it is necessary to restore the hook because the new program will also end and there will be a repeated (endless) launch upon completion.
2) MS DOS allocates all available memory for a single running program. Even before termination, all the memory is still occupied.
To load another program into memory, you need to find a free memory block. Such a block is kept by a resident. He releases it for the duration of the program launch and takes it back after its completion.
3) You cannot run BAT files via EXEC. It is necessary to call the executable program. In this case, it is COMMAND.COM which, as I assume in my program, should be located at the root of the current drive. If the current drive is D: and there is no COMMAND.COM , then the BAT file will not start.
4) All registers must be saved before starting. Although it's not particularly important. But I did it out of habit. And I keep all the 32-bit general purpose registers.
5) To run a program, you may need more stack space than the current program has. So I switch to my stack memory block for work. That's where all the registers are saved.

ADD: There is also an int 20h interrupt to terminate the program. It also needs to be intercepted and processed in a similar way.
Post 21 Mar 2025, 07:56
View user's profile Send private message Reply with quote
macomics



Joined: 26 Jan 2021
Posts: 1149
Location: Russia
macomics 21 Mar 2025, 08:19
One more thing. It is better not to use int 21h, but to call directly
Code:
; instead every int 21h
pushf
call far [cs:old_int21h]    
Post 21 Mar 2025, 08:19
View user's profile Send private message Reply with quote
Core i7



Joined: 14 Nov 2024
Posts: 47
Location: Socket on motherboard
Core i7 21 Mar 2025, 09:58
OgreVorbis wrote:
I have a feeling this is one of those problems that will never be solved properly. It's going way over my head. I can't believe DOS has this annoying problem.

you need to write a bootloader to run the games. Such programs in DOS are known as "semi-resident". In your bootloader, you free all the memory after yourself AH=4Ah, where you then load the game AX=4B01h, and launch it via JMP. When the game is over, it returns control back to your bootloader. For your case, this is better than resident.
Post 21 Mar 2025, 09:58
View user's profile Send private message Reply with quote
Display posts from previous:
Post new topic Reply to topic

Jump to:  


< Last Thread | Next Thread >
Forum Rules:
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Copyright © 1999-2025, Tomasz Grysztar. Also on GitHub, YouTube.

Website powered by rwasa.