Search code examples
assemblydosx86-16tasmtsr

My TSR program freezes when executed a second time


I have got some experience with FASM and I learned that so well. Now, I wanted to learn TASM syntax. I wrote an example program which is TSR. Here`s my code

.model tiny
.8086
.stack 200h

.data
        Message db 'Example0 is loaded in memory',0,'$'
.code
        main proc       ;'main' is proc and code region
        mov ax,@data    ;Initialize data segment
        mov ds,ax       
        push es
        xor bx,bx
        mov es,bx
        ;Check interrupt vector 0f5h
        cmp word ptr es:[3d6h],0
        je init         ;If null initialize program and stay resident
        int 0f5h
        cmp ax,'ID'     ;Check string in ax
        jne init        ;If AX != 'GG' initialize TSR
        mov ah,9
        mov dx,offset Message
        int 21h
        init:
        ;Set interrupt vector 0f5h
        mov word ptr es:[3d4h],offset interruptroute
        mov word ptr es:[3d6h],cs
        pop es
        mov ax,3100h
        mov dx,64       ;Reserve 1KB (My .exe is lower than this)
        int 21h
        interruptroute proc far
                shl bx,2
                add bx,offset g0
                call far [cs:bx] ;I assume this array is in code segment 
                ;Here maybe fault in call far 
                iret
        endp interruptroute     


        g0:
                dw getID
                dw @code
        getID proc far
                mov ax,'ID'
                retf
        endp getID
        endp
        end main

And my VirtualBox`s screenshot:

Additional I show my command line:

tasm src\gdos.asm,bin\gdos.obj tlink bin\gdos.obj,bin\gdos.exe

Note: GDOS is my planned OS to build.


Solution

  • TL;DR: FASM's syntax and semantics can be quite different from that of MASM/TASM.


    In Turbo Assembler (TASM) the FAR modifier by itself should not be used on a JMP or CALL instruction. Instead use FAR PTR procname where procname is the name of a procedure defined with PROC. The code provided uses:

    call far [cs:bx]
    

    Since this is a CALL instruction a person may be inclined to try:

    call far ptr [cs:bx]
    

    As I stated above FAR PTR should only be used if the operand is a label. [cs:bx] isn't a label. The question is - what syntax can be used to do an indirect FAR JMP without a label? The answer is DWORD PTR:

    call dword ptr [cs:bx]
    

    The second problem is in this section of code:

    mov ah,9
    mov dx,offset Message
    int 21h
    init:
    ;Set interrupt vector 0f5h
    mov word ptr es:[3d4h],offset interruptroute
    mov word ptr es:[3d6h],cs
    

    Int 21h/AH=9 is being used to print a message to standard output. When finished it continues execution into the initialization code. An exit is needed after printing the already loaded message. Add a call to Int 21h/AH=4C after displaying the message. The code could look like:

    mov ah,9
    mov dx,offset Message
    int 21h
    mov ax, 4c00h                ; DOS exit function return 0
    int 21h
    
    init:
    ;Set interrupt vector 0f5h
    mov word ptr es:[3d4h],offset interruptroute
    mov word ptr es:[3d6h],cs
    

    Other Observations

    • In the provided code the TSR checks if it is already loaded by examining the segment of interrupt 0F5h (in the IVT) and comparing with 0. If it is 0 it is assumed the TSR isn't installed. That may work in the environments being tested with, but it may not work in a wider array of DOS/hardware environments. Consider looking at the Int 2F multiplexer interrupt. It can be used for detecting the presence of an existing TSR. Randall Hyde has good information on this subject in his book Art of Assembly. Chapter 18.5 Installing a TSR covers this subject in detail.
    • Although not a bug, there is this code that defines getID:

      getID proc far
              mov ax,'ID'
              retf
      endp getID
      

      I recommend not using retf here. In any function defined with PROC TASM/MASM are smart enough to convert any ret they encounter to a retf if the function is defined FAR. They will convert any ret found in a NEAR function to a retn. This can be useful if the function is changed from NEAR to FAR and vice-versa as the assembler will encode the proper return instruction.

    • The jump table g0 is defined as:

      g0:
              dw getID
              dw @code
      

      This works. Instead it could be:

      g0:
              dw getID
              dw seg GetID
      

      This method doesn't rely on explicitly using a fixed segment @code. If GetID is placed in a different segment other than @code in the future then there is no modification required to the table.

    • If all of the functions like getID are in the same segment as the interrupt handler then near pointers (offsets) are all that are needed in the jump table g0. Using just a table of NEAR offsets an indirect near jump in interruptroute can be used:

      shl bx,1           ; The table would have 2 byte NEAR pointers relative to CS
                         ; So multiply the index by 2 instead of 4
      add bx,offset g0   
      jmp [cs:bx]        ; This does a NEAR jmp to the address at [cs:bx]
      

      The jump table g0 would now simply look like this:

      g0:
              dw getID
      

      Functions like getID would be marked as being NEAR with:

      getID proc near
      

      This only works if all the functions called by the interruptroute interrupt handler are in the same code segment as getID. I assume the intention is to have a table of call vectors and BX is an index into a call table (g0). In this code it would work, but I don't know the design intent of the code to say that this would be suitable for the actual project being worked on.