How do I properly hook Interrupt 28h in assembly for DOS, and restore it?
Asked Answered
B

2

5

I'm trying to set the handler of Interrupt 28h to my own routine, restore all the registers and flags involved, and restore the original Interrupt handler. I'm using NASM Assembler, under DOSBox and MS-DOS 6.22 in VirtualBox.

I've thought about debugging, but doing so on a TSR program sounds impossible. I've tried pushing the Data Segment onto the Code Segment, and saving the original Data Segment for restoring later, but it seems to hang the machine even after restoring the Data Segment.

section .text   ;Code Section
org 100h        ;DOS Executable Start
mov ah,35h      ;Get Interrupt Vector
mov al,28h      ;Of Interrupt 28h
int 21h         ;Call DOS Kernel
push cs         ;Push Code Segment
pop ds          ;Onto Data Segment
mov [oldseg],es ;Save Old Interrupt Vector Segment
mov [oldoff],bx ;Save Old Interrupt Vector Offset
mov ah,25h      ;Set Interrupt Vector
mov dx,resstart ;To Resstart
int 21h         ;Call DOS Kernel
mov dx,resend   ;Set Data Offset to Resend
sub dx,resstart ;Subtract Resstart
shr dx,4h       ;Shift Right 4 Bits for Paragraph
inc dx          ;One Extra Paragraph for PSP
mov ah,31h      ;Terminate and Stay Resident
xor al,al       ;Return Code
int 21h         ;Call DOS Kernel

resstart:       ;Resident Code Start
push ax         ;Save AX
push es         ;Save ES
push di         ;Save DI
push cx         ;Save CX
push ds         ;Save DS
push dx         ;Save DX
mov ah,00h      ;Set Video Mode
mov al,13h      ;To Mode 13h
int 10h         ;Call BIOS Video
mov ax,0A000h   ;VGA Segment
mov es,ax       ;Stored in ES
xor di,di       ;VGA Offset in DI
mov cx,0FA00h   ;Fill Entire Screen
mov al,09h      ;With Light Blue Color
rep stosb       ;Repeat Store AL at ES:DI
mov ah,25h      ;Set Interrupt Vector
mov al,28h      ;Of Interrupt 28h
mov ds,[oldseg] ;Restore Old Interrupt Vector Segment
mov dx,[oldoff] ;Restore Old Interrupt Vector Offset
int 21h         ;Call DOS Kernel
pop dx          ;Restore DX
pop ds          ;Restore DS
pop cx          ;Restore CX
pop di          ;Restore DI
pop es          ;Restore ES
pop ax          ;Restore AX
iret            ;Return and Restore Flags
resend:         ;Resident Code End

section .data
oldseg dw 0     ;Old Interrupt Vector Segment
oldoff dw 0     ;Old Interrupt Vector Offset

After returning the original interrupt vector address, and setting the new interrupt vector address to "resstart", the program should terminate and stay resident. After this, Interrupt 28h would be triggered automatically since DOS has nothing else to do, which would in turn run my Interrupt handler.

The Interrupt handler sets the video mode to 13h, tries to fill the entire screen with a light blue color, restores the original Interrupt 28h handler, restores all registers and flags involved, and returns to DOS. Executing this program yields no results, the system doesn't even hang. While running the part of setting video mode 13h and filling the entire screen with blue on its own separately, it works perfectly fine.

Bandylegged answered 1/6, 2019 at 2:5 Comment(8)
I assume from the assembly syntax you are using either NASM(or YASM) and you are creating a DOS COM program and not a DOS EXE program?Alsworth
What emulator or VM are you using to run the program? DOSBox? Something else?Alsworth
I am using NASM, and DOSBox. Ocassionally I will use VirtualBox MS-DOS 6.22 for more accuracy.Bandylegged
DOSBox will be a problem. It isn't a true MS-DOS and one interesting difference is that DOSBox doesn't do an Int 28h in its idle state. Even if you fix your code DOSBox won't run the code as you expect while a real version of DOS shouldAlsworth
Turns out, you're right, the screen does fill with blue using virtualbox, though the system hangs.Bandylegged
Since you say you are using NASM and you are using org 100h that you are iun fact creating a program with a .COM extension?Alsworth
Correct, org 100h NASM COM File.Bandylegged
Why not using Bochs internal debugger?Jocund
M
3
mov dx,resend ;Set Data Offset to Resend
sub dx,resstart ;Subtract Resstart
shr dx,4h ;Shift Right 4 Bits for Paragraph
inc dx ;One Extra Paragraph for PSP

In this .COM program you're saving and setting the interrupt vector correctly. However you don't calculate accurately the amount of paragraphs to keep by the DOS.TerminateAnd StayResident function.

The inc dx is needed to round up to the nearest paragraph higher. Certainly not to account for the PSP. That would require 16 paragraphs since the PSP has 256 bytes.

The memory that was allocated to this .COM program starts with the PSP and so the DX count must start there also.

mov     dx, resend 
shr     dx, 4
inc     dx
mov     ax, 3100h   ; DOS.TerminateAndStayResident
int     21h

Tip If you align this resend label to a paragraph boundary, the inc dx is no longer required.

If your present code worked partially in an emulator like virtualbox it's because the memory formerly occupied by your program was not yet overwritten by e.g. the program shell. Emulators, unlike DOS, have the luxury to execute the command interpreter from a far distance.

the screen does fill with blue using virtualbox, though the system hangs

I would hang too if someone switch off the lights while I'm in the middle of writing something! That's what your handler does when it suddenly changes the video mode...


For a TSR program we usually jump over the part that is to be kept resident, so the space occupied by the one-time setup can be recycled by the system.

One more trick you can use, is to write the offset and segment of the old interrupt vector directly in the instructions that will restore the vector. No more problems with segment registers in the handler.

This is my rewrite of your program:

    org     100h
Start:
    jmp     Setup

MyInt28:
    push    ax
    push    es
    push    di
    push    cx
    push    ds
    push    dx
    mov     ax, 0013h   ; BIOS.SetVideoMode
    int     10h
    mov     ax, 0A000h
    mov     es, ax
    xor     di, di
    mov     cx, 64000/2
    mov     ax, 0909h
    cld
    rep stosw
PatchA:
    mov     ax, 0       ; Don't change this to 'xor ax,ax'
    mov     ds, ax
PatchB:
    mov     dx, 0       ; Don't change this to 'xor dx,dx'
    mov     ax, 2528h   ; DOS.SetInterruptVector
    int     21h
    pop     dx
    pop     ds
    pop     cx
    pop     di
    pop     es
    pop     ax 
    iret

Setup:                  ; Resident part ends here.
    mov     ax, 3528h   ; DOS.GetInterruptVector
    int     21h         ; -> ES:BX
    mov     [PatchA + 1], es
    mov     [PatchB + 1], bx
    mov     dx, MyInt28
    mov     ah, 25h     ; DOS.SetInterruptVector
    int     21h
    mov     dx, (256+Setup-Start+15)/16
    mov     ax, 3100h   ; DOS.TerminateAndStayResident
    int     21h
Morphophonemics answered 2/6, 2019 at 19:30 Comment(14)
He is also running under DOSBox as well as VirtualBox. DOSBox doesn't call int 28h when idle so won't do anything.I have a comment about that and a link to the DOSBox source code. Which assembler is this for? Lastly, with a COM program the size of the resident code/data can be computed at assemble time. You could put a label (Start) before the initial jump and compute (Setup-Start+100h+15)/16 and put it in DX directly. By adding 15 you can drop the alignment (reduce code size on disk) as the calculation should round up to nearest paragraph boundary.Alsworth
I'd even go a step further. I'd actually write the code so it starts by moving itself to offset 0x5c in the PSP. If your resident program isn't doing DOS file I/O doing so should be safe.This allows you to reduce the number of paragraphs needed to remain resident.Alsworth
A variant of your program that works with NASM that does the extra relocation and computes the paragraphs at assemble time can be found here if anyone that is interested: pastebin.com/eCSnH9MwAlsworth
@MichaelPetch I think that, apart from the multi-operand forms of pushand pop that FASM supports, NASM should be able to assemble my code.Morphophonemics
Does FASM support mov dx, ??Alsworth
I*'m asking these questions because anyone who comes by to look at this answer may be curious what assembler you were using to get this to work.Alsworth
@MichaelPetch Regarding "You could put a label (Start) before the initial jump and compute (Setup-Start+100h+15)/16". Wouldn't that label in my code be at address 256 and thus allow for the simpler calculation mov dx, (Setup + 15) / 16 ?Morphophonemics
Not if the assembler you are using requires absolute values - it may choke on mov dx, (Setup + 15) / 16 because Setup may not be seen as absolute. The difference between two labels would be, but again this would be a detail of how the assembler works. Which is again, why I'm curious what you are using. You suggest FASM but even then my version chokes on the ? If i replace ? (which I assume might suggest any operand) with 0 fasm assembles it.Alsworth
In FASM you can just use (Setup+15)/16 , in NASM that calculation would yield an error: division operator may only be applied to scalar values. Shift or division require scalar values in NASM. By subtracting a label at the start would allow FASM and NASM to accept it.Alsworth
@MichaelPetch In my assembler (literally my assembler, being its author) the question mark always represents zero, but indicating that the value itself is of no real importance. Over the years I've seen that FASM bears the most ressemblance to my assembler. That's perhaps why I somewhat easily say that I write code for FASM where in fact small differences will creep up from time to time...Morphophonemics
So I wanted to know what assembler it was, because the details sometimes matter. It is your own. Fine, but if you present code I'd at least recommend something a bit more generic for mainstream assemblers. I'd actually modify your answer to separate all the registers on the push and pop, replace ? with 0. If you also wanted to do the paragraph computation at assemble time then creating a label at the start (before the jmp) and subtracting it from Setup and then doing +15 and /16 would make it compatible with both FASM and NASM (not sure about your assembler)Alsworth
@MichaelPetch I like your idea about moving the resident code into the PSP. How far down to you think we could go? Is 005Ch the lowest we can go? Would it be OK to move to 0042h? Apart from 'reserved' and the never used 'far dispatch' there's perhaps little reason to stay away.Morphophonemics
Thanks everyone for the help, having the resident section at the top and jumping to the installation of the new interrupt handler at the bottom let me correctly calculate the number of paragraphs without the installer stealing space reserved for the resident section.Bandylegged
(Setup-Start+15)/16 is incorrect, you need to add 256 for the PSP.Dymoke
U
1

There are multiple problems in your program:

Problem 1

push cs ;Push Code Segment
pop ds ;Onto Data Segment
mov [oldseg],es ;Save Old Interrupt Vector Segment
mov [oldoff],bx ;Save Old Interrupt Vector Offset
...
mov ds,[oldseg] ;Restore Old Interrupt Vector Segment
mov dx,[oldoff] ;Restore Old Interrupt Vector Offset

The four mov instructions assume that the ds register points to the .data section.

However, in the case of the first two mov instructions ds will point to the .text section, not to the .data section because of the push cs - pop ds sequence.

In the case of a .COM file .text and .data section are typically the same; however in .EXE files they are normally not the same.

In the case of the third mov instruction it is very improbable that ds points to any section that is related to your program. And in the case of the fourth one it is nearly impossible because the third mov instruction changed the ds register.

A solution would be to use the .text segment to store data. This is possible in "real-mode" operating systems (such as MS-DOS), but not in "protected-mode" operating systems (such as Windows):

Place the two dw 0 lines (eg. oldseg dw 0) before the section .data line. Now the four bytes of data storage are located in the same section as your code. Then you can access the data the following way:

 push cs
 pop ds
 mov [oldseg],es ;We know that ds=cs, so no "cs:" is required here
 ...
 mov ds,cs:[oldseg] ;Restore Old Interrupt Vector Segment
 mov dx,cs:[oldoff] ;Restore Old Interrupt Vector Offset

The "cs:" will tell the CPU that the data you access is located in the section cs points to; and cs always points to the section containing the code currently being executed. And this is the .text section.

Please note that the correct syntax (the location of the letters "cs:" in the line) differs from assembler to assembler:

 mov dx,cs:[oldoff]
 cs:mov dx,[oldoff]
 mov dx,[cs:oldoff]

Maybe your assembler uses another syntax.

Problem 2

mov ah,25h ;Set Interrupt Vector
mov al,28h ;Of Interrupt 28h
mov ds,[oldseg] ;Restore Old Interrupt Vector Segment
mov dx,[oldoff] ;Restore Old Interrupt Vector Offset
int 21h ;Call DOS Kernel

Calling int 21h from inside int 21h (and int 28h is called from inside int 21h) is also not a good idea.

However, function 25h will do nothing but writing 4 bytes of data to the interrupt vector table (while the interrupts are disabled using cli):

You may do this directly by simply storing the offset to address 0:0A0h and the segment to address 0:0A2h:

mov ax,0      ;You might also use "xor ax,ax" or "sub ax,ax"
mov ds,ax     ;Now ds=0
mov ax,cs:[oldseg]
mov dx,cs:[oldoff]
cli           ;Disable the interrupts
mov [0A0h],dx ;Write dx to ds:0A0h which is 0:0A0h
mov [0A2h],ax ;Write ax to ds:0A2h which is 0:0A2h

The cli is there to ensure that no hardware interrupt can happen between the two instructions mov [0A0h],dx and mov [0A2h],ax.

If you can ensure that int 28h is not called from a hardware interrupt, you do not need to do this.

The iret instruction will automatically restore the old state of the interrupts (enabled or disabled).

Problem 3

Calling complex functions (such as int 10h) from the int 28h interrupt seems also not to be the best idea.

Unbrace answered 1/6, 2019 at 4:42 Comment(2)
Int 28h in DOS has the property that DOS Int 21h calls (above subfunction 0ch) can be called in most cases. This is unlike a traditional interrupt handler where DOS re-entrancy is a problem. As well. this is a DOS COM program and OP is using NASM. The DATA and TEXT sections will all be combined into one although NASM will align the beginning of the data section.Alsworth
One other issue in the interrupt handler is that they use a string instruction (rep stosb) but they don't actually make sure the direction flag is using forward movement. One can never be certain whether the direction flag when the interrupt occurred was already cleared. The fix is to ensure CLD is explicitly called in the interrupt handler before the rep stosb)Alsworth

© 2022 - 2024 — McMap. All rights reserved.