如何在DOS中使用汇编正确地挂钩(hook)28h中断,并恢复它?

5
我正在尝试将Interrupt 28h的处理程序设置为我的自定义程序,恢复所有相关的寄存器和标志,并还原原始的中断处理程序。我正在使用NASM汇编器,在VirtualBox中的DOSBox和MS-DOS 6.22下进行操作。
我考虑过调试,但是在TSR程序上这样做似乎是不可能的。我已经尝试将数据段推入代码段并保存原始的数据段以便稍后恢复,但是即使在恢复了数据段之后也会导致机器挂起。
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

在返回原始中断向量地址并将新的中断向量地址设置为"resstart"后,程序应该终止并保持驻留状态。此后,中断28h会自动触发,因为DOS没有其他事情要做,这会依次运行我的中断处理程序。

中断处理程序将视频模式设置为13h,尝试用浅蓝色填充整个屏幕,恢复原始的中断28h处理程序,恢复所有涉及到的寄存器和标志,并返回到DOS。执行此程序不会产生任何结果,甚至系统也不会停机。但单独运行设置视频模式13h和用蓝色填充整个屏幕的部分却可以正常工作。


我假设从汇编语法来看,您正在使用NASM(或YASM),并且正在创建一个DOS COM程序而不是DOS EXE程序? - Michael Petch
你使用什么模拟器或虚拟机来运行程序?DOSBox?还是其他的东西? - Michael Petch
1
DOSBox会是一个问题。它不是真正的MS-DOS,其中一个有趣的区别是在其空闲状态下,DOSBox不会执行Int 28h操作(参见https://sourceforge.net/p/dosbox/code-0/4097/tree//dosbox/trunk/src/dos/dev_con.h#l54)。即使您修复了代码,DOSBox也不会按您预期的方式运行代码,而真正的DOS版本应该会。 - Michael Petch
正确,org 100h NASM COM文件。 - AaronRules5
为什么不使用Bochs内部调试器? - Michael Chourdakis
显示剩余3条评论
2个回答

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
在这个.COM程序中,您正确保存和设置中断向量。但是,您没有准确计算DOS.TerminateAnd StayResident函数要保留的段数。inc dx是必需的,以使其向上舍入到最近的段。当然不是为了考虑PSP。这需要16个段,因为PSP有256字节。
分配给此.COM程序的内存从PSP开始,因此DX计数也必须从那里开始。
mov     dx, resend 
shr     dx, 4
inc     dx
mov     ax, 3100h   ; DOS.TerminateAndStayResident
int     21h

提示如果您将此重新发送标签对齐到段落边界,则不再需要inc dx

如果您当前的代码在虚拟机virtualbox中部分工作,那是因为您的程序以前占用的内存尚未被其他程序(如程序壳)覆盖。与DOS不同,模拟器可以从远处执行命令解释器。

使用virtualbox时,屏幕确实变成了蓝色,但系统挂起了

如果当我正在写东西时有人突然关掉灯,我也会崩溃!这就是当处理程序突然更改视频模式时所发生的事情...


对于TSR程序,我们通常跳过要保留的部分,因此系统可以回收一次性设置占用的空间。

您还可以使用另一种技巧,将旧的中断向量的偏移和段直接写入将恢复该向量的指令中。这样,处理程序中的段寄存器问题就没有了。

这是我重写的程序:

    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

1
如果有人感兴趣,可以在此处找到一个与NASM配合使用的程序变体,它可以在汇编时进行额外的重定位并计算段落:https://pastebin.com/eCSnH9Mw - Michael Petch
FASM是否支持mov dx, ? - Michael Petch
1
所以我想知道它是哪个汇编器,因为细节有时很重要。这是你自己的。好吧,但如果你展示代码,我至少建议使用一些更通用的主流汇编器。实际上,我会修改你的答案,将所有寄存器在push和pop上分开,将“?”替换为“0”。如果您还想在汇编时进行段落计算,则在开始处(在“jmp”之前)创建一个标签,从“Setup”中减去它,然后执行+15和/16,这将使其与FASM和NASM兼容(不确定您的汇编器)。 - Michael Petch
@MichaelPetch,我喜欢你关于将常驻代码移入PSP的想法。你认为我们可以走多远?005Ch是我们能到达的最低点吗?移动到0042h是否可以?除了“保留”和从未使用的“far dispatch”之外,也许没有太多理由不这样做。 - Sep Roland
(Setup-Start+15)/16 是不正确的,你需要为PSP添加256。 - ecm
显示剩余9条评论

1

你的程序存在多个问题:

问题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

这四个mov指令假定ds寄存器指向.data部分。

然而,在前两个mov指令的情况下,由于push cs - pop ds序列,ds将指向.text部分,而不是.data部分。

.COM文件的情况下,.text.data部分通常是相同的;但是在.EXE文件中,它们通常不相同。

在第三个mov指令的情况下,ds几乎不可能指向与您的程序相关的任何部分。在第四个指令的情况下,几乎不可能,因为第三个mov指令改变了ds寄存器。

解决方案是使用.text段来存储数据。这在“实模式”操作系统(如MS-DOS)中是可能的,但在“保护模式”操作系统(如Windows)中不可能:

将两个dw 0行(例如,oldseg dw 0)放置在section .data行之前。现在,四个字节的数据存储位于与您的代码相同的部分中。然后,您可以按以下方式访问数据:

 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

"cs:"将告诉CPU,您访问的数据位于cs指向的部分;而cs始终指向当前正在执行的代码所在的部分。这就是.text部分。
请注意,正确的语法(行中字母"cs:"的位置)因汇编程序而异:
 mov dx,cs:[oldoff]
 cs:mov dx,[oldoff]
 mov dx,[cs:oldoff]

可能你的汇编器使用了另一种语法。

问题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

int 21h内部调用int 21h(并且从int 21h内部调用int 28h)也不是一个好主意。

然而,函数25h什么都不会做,只会向中断向量表写入4个字节的数据(使用cli禁用中断):

您可以通过直接将偏移量存储到地址0:0A0h和段存储到地址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

cli 的作用是确保在执行指令 mov [0A0h],dxmov [0A2h],ax 期间不会发生硬件中断。

如果你能确保 int 28h 不会被硬件中断调用,那么就不需要这样做。

iret 指令将自动恢复中断的旧状态(启用或禁用)。

问题3

int 28h 中断调用复杂函数(例如 int 10h)似乎也不是最好的想法。


3
在DOS中,Int 28h具有一个特性,即DOS Int 21h调用(超过子功能0ch)在大多数情况下可以被调用。这与传统的中断处理程序不同,其中DOS可重入性是一个问题。此外,这是一个使用NASM的DOS COM程序。虽然NASM将使数据段的开头对齐,但数据和文本部分将全部合并为一个部分。 - Michael Petch
5
中断处理程序中的另一个问题是它们使用字符串指令(rep stosb),但实际上并没有确保方向标志正在使用向前移动。人们永远无法确定中断发生时方向标志是否已经清除。解决方法是在 rep stosb 之前,在中断处理程序中明确调用 CLD,以确保方向标志被清除。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接