ἅπαξ λεγόμενον
146.190.13.172

32-bit Ada TSR


June 2024

Introduction

The Weightless Ada Runtime for DPMI (ward) makes it possible for us to compile Ada programs for a variety of DOS operating systems (Gregory 2024). The DOS extender used in tandem with it, DOS32A, fully supports 32-bit TSR (terminate-and-stay-resident) programs.

Even though such programs require rather unusual methods of setting up (including direct system calls and handling system addresses), most of these can actually be represented as regular procedure calls and be handled the exact same way ward handles other operating system interaction. This article focuses on abstracting as much logic as possible in a form of a regular Ada program, only using inline assembly as a last resort.

How programs like these can be used is beyond the scope of this article, as it will only focus on the technical aspect of implementing such a program. This article assumes the reader knows what a TSR is and has at least some knowledge when it comes to setting them up and/or using them.

The included example provides a simple procedure displaying the words “TIC” and “TAC” on the screen, alternating every second. It is only presented here as a proof of concept and by no means represents how an actual TSR should act (e.g., it lacks a check to see if it is already installed in memory).

Implementation

The example we are going to work on requires ward with the system time and DPMI API. The new functions and procedures we need can be found in the following vpatch: weightless-time-dpmi-api.vpatch (seal).

Our main program is shown below.

procedure AdaClock is
   Unused_Error : DPMI_Error;
begin
   Disable_Interrupts;
   Proper_Selector := Get_Data_Selector;
   Unused_Error :=
    Get_Protected_Mode_Interrupt_Vector(IRQ_0,
                                       Old_Handler.Selector,
                                       Old_Handler.Address);
   Unused_Error :=
    Set_Protected_Mode_Interrupt_Vector(IRQ_0,
                                        Get_Code_Selector,
                                        Display_Clock_Wrapper'Address);
   Enable_Interrupts;
   Terminate_And_Stay_Resident(0);
end AdaClock;

This program should be rather self-explanatory.

The IRQ_0 is the constant value (8) representing the hardware timer interrupt.

Querying the DPMI API for the current interrupt handler and setting the new one must happen with the interrupts disabled, so the API calls are placed between the Disable_Interrupts and Enable_Interrupts routines. Their definitions appear below.

   procedure Disable_Interrupts is
   begin
      Asm("cli", Volatile => True);
   end Disable_Interrupts;

   procedure Enable_Interrupts is
   begin
      Asm("sti", Volatile => True);
   end Enable_Interrupts;

The data selector is going to become unreliable when the interrupt itself is handled, so we save it here and manually override it later. We also need the current code selector, so we define the two following functions.

   function Get_Code_Selector return Selector_Type is
      Code_Selector : Selector_Type;
   begin
      Asm("mov %%cs,%0",
          Outputs => Selector_Type'Asm_Output("=g", Code_Selector),
          Volatile => True);
      return Code_Selector;
   end Get_Code_Selector;

   function Get_Data_Selector return Selector_Type is
      Data_Selector : Selector_Type;
   begin
      Asm("mov %%ds,%0",
          Outputs => Selector_Type'Asm_Output("=g", Data_Selector),
          Volatile => True);
      return Data_Selector;
   end Get_Data_Selector;

We end the program with a call to Terminate_And_Stay_Resident. It makes the process exit while leaving our handling routines in memory.

The Display_Clock_Wrapper is probably the ugliest procedure in our example, since it is entirely written using inline assembly.

   procedure Display_Clock_Wrapper is
   begin
     Asm("pushal" & ASCII.LF & ASCII.HT &
          "push %%ds" & ASCII.LF & ASCII.HT &
          "mov %%cs:%P0, %%ds" & ASCII.LF & ASCII.HT &
          "call %P1" & ASCII.LF & ASCII.HT &
          "pushfl" & ASCII.LF & ASCII.HT &
          "lcall %2" & ASCII.LF & ASCII.HT &
          "pop %%ds" & ASCII.LF & ASCII.HT &
          "popal" & ASCII.LF & ASCII.HT &
          "iret",
         Inputs => (System.Address'Asm_Input("i", Proper_Selector'Address),
                    System.Address'Asm_Input("i", Display_Clock'Address),
                    Exception_Handler'Asm_Input("m", Old_Handler)),
         Volatile => True);
   end Display_Clock_Wrapper;

This procedure saves our registers, sets up the proper data selector and calls our handler procedure, before calling the old handler to keep the chain and restoring the registers to their original values. The reader might notice it ends with an iret instruction—it is emitted before the regular ret instruction by the compiler, so this procedure never returns normally (and thus should not be called).

The handler procedure itself does not use inline assembly and is also rather self-explanatory.

   procedure Display_Clock is
      Unused_H  : Hour;
      Unused_M  : Minutes;
      S         : Seconds;
      Unused_SS : Centiseconds;
   begin
      Get_System_Time(Unused_H,
                      Unused_M,
                      S,
                      Unused_SS);

      if S mod 2 = 1 then
         Display("TIC");
      else
         Display("TAC");
      end if;
   end Display_Clock;

The display procedure writes directly to the CGA character buffer (and always sets the color values to white on black).

   procedure Display(Item : String) is
      Addr :  System.Address := To_Address(16#b8076#);
      Addr1 : System.Address := To_Address(16#b8077#);
   begin
      for i in Item'Range loop
         declare
            Char : Character with Address => Addr, Alignment => 1;
            Color : Unsigned_8 with Address => Addr1, Alignment => 1;
         begin
            Char := Item(i);
            Color := 16#07#;
         end;
         Addr := Addr + 2;
         Addr1 := Addr1 + 2;
      end loop;
   end Display;

The entire example can be found in the following vpatch: adaclock-genesis.vpatch (seal). It is possible to build it using regular gprbuild by passing it the appropriate target and RTS arguments.

Conclusions

The author believes the included example shows how to properly handle interrupts and interact with the system when it comes to setting up the interrupt vector. Even though such operations may seem low-level, both the Ada language and the GNAT compliler provide ways to handle it in an elegant way.

References

Gregory, David [hapax, pseud.]. 2024. “PEH.EXE: Running Peh on DOS,” Hapax Legomenon (February). http://146.190.13.172/articles/peh-exe-running-peh-on-dos/.