home *** CD-ROM | disk | FTP | other *** search
/ Hall of Fame / HallofFameCDROM.cdr / util2 / tsr.lzh / RELEASE.PAS < prev    next >
Pascal/Delphi Source File  |  1986-07-20  |  22KB  |  631 lines

  1. {**************************************************************************
  2. *   Releases memory above the last MARK call made.                        *
  3. *   Copyright (c) 1986 Kim Kokkonen, TurboPower Software.                 *
  4. *   Released to the public domain for personal, non-commercial use only.  *
  5. ***************************************************************************
  6. *   Version 1.0 2/8/86                                                    *
  7. *     original public release.                                            *
  8. *     (thanks to Neil Rubenking for an outline of the method used)        *
  9. *   Version 1.1 2/11/86                                                   *
  10. *     fixed problem with processes which deallocate their environment.    *
  11. *   Version 1.2 2/13/86                                                   *
  12. *     fixed another problem with processes which deallocate environment.  *
  13. *   Version 1.3 2/15/86                                                   *
  14. *     added support for "named" marks.                                    *
  15. *   Version 1.4 2/23/86                                                   *
  16. *     added support for releasing programs which use Expanded Memory.     *
  17. *   Version 1.5 2/28/86                                                   *
  18. *     added more bulletproof method of finding first allocation block.    *
  19. *   Version 1.6 3/20/86                                                   *
  20. *     restore all FF interrupts.                                          *
  21. *     restore the termination address to the local process.               *
  22. *     reduce number of EMS blocks to 32.                                  *
  23. *     fix bug in number of EMS handles in EMS release step.               *
  24. *     restore a mysterious address in the PSP which allows RELEASE of a   *
  25. *       COMMAND shell (emulates the EXIT command).                        *
  26. *   Version 1.7 (date not recorded)                                       *
  27. *     add "protected" marks.                                              *
  28. *   Version 1.8 4/21/86                                                   *
  29. *     fix problem when mark is installed as 'MARK '.                      *
  30. *   Version 1.9 5/22/86                                                   *
  31. *     release the environment of MARK when it is not contiguous with      *
  32. *       the MARK itself.                                                  *
  33. *     capture RELEASE calls from within batch files and don't release the *
  34. *       batch control block.                                              *
  35. *     fiddle with different methods of restoring interrupt vectors in     *
  36. *       an attempt to successfully remove DoubleDos. No success, not      *
  37. *       implemented. Note, after more effort: DDos apparently             *
  38. *       reprograms the 8259 as well as patching the operating system.     *
  39. *   Version 2.0 6/17/86                                                   *
  40. *     support "file" marks placed by the new program FMARK.               *
  41. *   Version 2.1 7/18/86                                                   *
  42. *     fix bug in restoring "parent" address in RELEASE PSP.               *
  43. *                                                                         *
  44. ***************************************************************************
  45. *   telephone: 408-378-3672, CompuServe: 72457,2131.                      *
  46. *   requires Turbo version 3 to compile.                                  *
  47. *   Compile with mAx dynamic memory = FFFF.                               *
  48. ***************************************************************************}
  49.  
  50. {$P128}
  51. {$C-}
  52.  
  53. program ReleaseTSR;
  54.   {-release system memory above the last mark call}
  55.   {-release expanded memory blocks allocated since the last mark call}
  56.  
  57. const
  58.   Version = '2.1';
  59.   ProtectChar = '!';          {marks whose name begins with this will be
  60.                               released ONLY if an exact name match occurs}
  61.   MaxBlocks = 128;            {max number of DOS allocation blocks supported}
  62.   MaxHandles = 32;            {max number of EMS allocation blocks supported}
  63.   EMSinterrupt = $67;         {the vector used by the expanded memory manager}
  64.  
  65.   markID = 'MARK PARAMETER BLOCK FOLLOWS'; {marking string for TSR MARK}
  66.   fmarkID = 'FMARK TSR'; {marking string for TSR FMARK}
  67.  
  68.   {offsets into resident copy of MARK.COM for data storage}
  69.   markOffset = $103;          {where markID is found in MARK TSR}
  70.   fmarkOffset = $60;          {where fmarkID is found in FMARK TSR}
  71.   vectorOffset = $120;        {where vector table is stored}
  72.   EMScntOffset = $520;        {where count of EMS active pages is stored}
  73.   EMSmapOffset = $522;        {where the page map is stored}
  74.  
  75.   debug = false;              {set true for detailed output report}
  76.  
  77. type
  78.   registers =
  79.   record case Integer of
  80.     1 : (ax, bx, cx, dx, bp, si, di, ds, es, flags : Integer);
  81.     2 : (al, ah, bl, bh, cl, ch, dl, dh : Byte);
  82.   end;
  83.  
  84.   HandlePageRecord =
  85.   record
  86.     handle : Integer;
  87.     numpages : Integer;
  88.   end;
  89.  
  90.   PageArray = array[1..MaxHandles] of HandlePageRecord;
  91.   PageArrayPtr = ^PageArray;
  92.  
  93.   Block =
  94.   record                      {store info about each memory block}
  95.     mcb : Integer;
  96.     psp : Integer;
  97.     releaseIt : Boolean;
  98.   end;
  99.  
  100.   BlockType = 0..MaxBlocks;
  101.   BlockArray = array[BlockType] of Block;
  102.   AllStrings = string[255];
  103.   HexString = string[4];
  104.  
  105. var
  106.   Blocks : BlockArray;
  107.   bottomBlock, blockNum : BlockType;
  108.   markName : AllStrings;
  109.   Regs : registers;
  110.   FilMarkHandles, ReturnCode, StartMCB, StoredHandles, EMShandles : Integer;
  111.   FilMarkPageMap, Map, StoredMap : PageArrayPtr;
  112.   TrappedBytes : Real;
  113.   MemMark, FilMark : Boolean;
  114.   Vectors : array[0..1023] of Byte;
  115.  
  116.   procedure FindTheBlocks;
  117.     {-scan memory for the allocated memory blocks}
  118.   const
  119.     MidBlockID = $4D;         {byte DOS uses to identify part of MCB chain}
  120.     EndBlockID = $5A;         {byte DOS uses to identify last block of MCB chain}
  121.   var
  122.     mcbSeg : Integer;         {segment address of current MCB}
  123.     nextSeg : Integer;        {computed segment address for the next MCB}
  124.     gotFirst : Boolean;       {true after first MCB is found}
  125.     gotLast : Boolean;        {true after last MCB is found}
  126.     idbyte : Byte;            {byte that DOS uses to identify an MCB}
  127.  
  128.     function GetStartMCB : Integer;
  129.       {-return the first MCB segment}
  130.     begin
  131.       Regs.ah := $52;
  132.       MsDos(Regs);
  133.       GetStartMCB := MemW[Regs.es:(Regs.bx-2)];
  134.     end {getstartmcb} ;
  135.  
  136.     procedure StoreTheBlock(var mcbSeg, nextSeg : Integer;
  137.                             var gotFirst, gotLast : Boolean);
  138.       {-store information regarding the memory block}
  139.     var
  140.       nextID : Byte;
  141.       pspAdd : Integer;       {segment address of the current PSP}
  142.       mcbLen : Integer;       {size of the current memory block in paragraphs}
  143.  
  144.     begin
  145.  
  146.       mcbLen := MemW[mcbSeg:3]; {size of the MCB in paragraphs}
  147.       nextSeg := Succ(mcbSeg+mcbLen); {where the next MCB should be}
  148.       pspAdd := MemW[mcbSeg:1]; {address of program segment prefix for MCB}
  149.       nextID := Mem[nextSeg:0];
  150.  
  151.       if gotLast or (nextID = EndBlockID) or (nextID = MidBlockID) then begin
  152.         blockNum := Succ(blockNum);
  153.         gotFirst := True;
  154.         with Blocks[blockNum] do begin
  155.           mcb := mcbSeg;
  156.           psp := pspAdd;
  157.         end;
  158.       end;
  159.  
  160.     end {storetheblock} ;
  161.  
  162.   begin
  163.  
  164.     {initialize}
  165.     StartMCB := GetStartMCB;
  166.     mcbSeg := StartMCB;
  167.     gotFirst := False;
  168.     gotLast := False;
  169.     blockNum := 0;
  170.  
  171.     {scan all memory until the last block is found}
  172.     repeat
  173.       idbyte := Mem[mcbSeg:0];
  174.       if idbyte = MidBlockID then begin
  175.         StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
  176.         if gotFirst then mcbSeg := nextSeg else mcbSeg := Succ(mcbSeg);
  177.       end else if gotFirst and (idbyte = EndBlockID) then begin
  178.         gotLast := True;
  179.         StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
  180.       end else begin
  181.         {start block was invalid}
  182.         WriteLn('Corrupted allocation chain or program error....');
  183.         Halt(1);
  184.       end;
  185.     until gotLast;
  186.  
  187.   end {findtheblocks} ;
  188.  
  189.   function StUpcase(s : AllStrings) : AllStrings;
  190.     {-return the uppercase string}
  191.   var
  192.     i : Byte;
  193.  
  194.   begin
  195.     for i := 1 to Length(s) do
  196.       s[i] := UpCase(s[i]);
  197.     StUpcase := s;
  198.   end {stupcase} ;
  199.  
  200.   function FindMark(markName : AllStrings) : Integer;
  201.     {-find the last memory block matching idstring at offset idoffset}
  202.   var
  203.     b : BlockType;
  204.  
  205.     function HasIDstring(segment : Integer;
  206.                          idString : AllStrings;
  207.                          idOffset : Integer) : Boolean;
  208.       {-return true if idstring is found at segment:idoffset}
  209.     var
  210.       tString : AllStrings;
  211.       len : Byte;
  212.  
  213.     begin
  214.       len := Length(idString);
  215.       tString[0] := Chr(len);
  216.       Move(Mem[segment:idOffset], tString[1], len);
  217.       HasIDstring := (tString = idString);
  218.     end {HasIDstring} ;
  219.  
  220.     function GetMarkName(segment : Integer) : AllStrings;
  221.       {-return a cleaned up mark name from the segment's PSP}
  222.     var
  223.       tString : AllStrings;
  224.       tlen : Byte absolute tString;
  225.  
  226.     begin
  227.       Move(Mem[segment:$80], tString[0], 128);
  228.       while (tlen > 0) and ((tString[1] = ' ') or (tString[1] = ^I)) do
  229.         Delete(tString, 1, 1);
  230.       while (tlen > 0) and ((tString[tlen] = ' ') or (tString[tlen] = ^I)) do
  231.         tlen := Pred(tlen);
  232.       GetMarkName := StUpcase(tString);
  233.     end;                      {GetMarkName}
  234.  
  235.     function MatchMemMark(segment : Integer;
  236.                           markName : AllStrings;
  237.                           var b : BlockType) : Boolean;
  238.       {-return true if MemMark is unnamed or matches current name}
  239.     var
  240.       tString : AllStrings;
  241.       FoundIt : Boolean;
  242.  
  243.     begin
  244.       {check the mark name stored in the PSP of the mark block}
  245.       tString := GetMarkName(segment);
  246.       if (markName <> '') then begin
  247.         FoundIt := (tString = StUpcase(markName));
  248.         if not(FoundIt) then
  249.           if (tString <> '') and (tString[1] = ProtectChar) then
  250.             {current mark is protected, stop searching}
  251.             b := 1;
  252.       end else if (tString <> '') and (tString[1] = ProtectChar) then begin
  253.         {stored mark name is protected}
  254.         FoundIt := False;
  255.         {stop checking}
  256.         b := 1;
  257.       end else
  258.         {match any mark}
  259.         FoundIt := True;
  260.       if not(FoundIt) then
  261.         b := Pred(b);
  262.       MatchMemMark := FoundIt;
  263.     end {MatchMemMark} ;
  264.  
  265.     function MatchFilMark(segment : Integer;
  266.                           markName : AllStrings;
  267.                           var b : BlockType) : Boolean;
  268.       {-return true if FilMark is unnamed or matches current name}
  269.     var
  270.       tString : AllStrings;
  271.       FoundIt : Boolean;
  272.  
  273.       function ExistFile(path : AllStrings) : Boolean;
  274.         {-return true if file exists}
  275.       var
  276.         f : file;
  277.  
  278.       begin
  279.         Assign(f, path);
  280.         {$I-}
  281.         Reset(f);
  282.         {$I+}
  283.         ExistFile := (IOResult = 0);
  284.         Close(f);
  285.       end;                    {existfile}
  286.  
  287.     begin
  288.       {check the mark name stored in the PSP of the mark block}
  289.       tString := GetMarkName(segment);
  290.       if (markName <> '') then begin
  291.         markName := StUpcase(markName);
  292.         FoundIt := (tString = markName);
  293.         if FoundIt then begin
  294.           {Assure named file exists}
  295.           WriteLn('finding mark file: ', markName);
  296.           FoundIt := ExistFile(markName);
  297.           if not(FoundIt) then
  298.             {stop checking}
  299.             b := 1;
  300.         end;
  301.       end else
  302.         {file marks must be named on RELEASE command line}
  303.         FoundIt := False;
  304.       if not(FoundIt) then
  305.         b := Pred(b);
  306.       MatchFilMark := FoundIt;
  307.     end {MatchFilMark} ;
  308.  
  309.   begin
  310.     {scan from the last block down to find the last MARK TSR}
  311.     b := blockNum;
  312.     MemMark := False;
  313.     FilMark := False;
  314.     repeat
  315.       if blocks[b].psp=cseg then
  316.         {assure this program's command line is not matched}
  317.         b:=pred(b)
  318.       else if HasIDstring(Blocks[b].psp, markid, markOffset) then
  319.         {an in-memory mark}
  320.         MemMark := MatchMemMark(Blocks[b].psp, markName, b)
  321.       else if hasidstring(blocks[b].psp, fmarkid, fmarkoffset) then
  322.         {a file mark}
  323.         FilMark := MatchFilMark(Blocks[b].psp, markName, b)
  324.       else
  325.         {not a mark}
  326.         b:=pred(b);
  327.     until (b < 1) or MemMark or FilMark;
  328.     if not(MemMark or FilMark) then begin
  329.       WriteLn('No matching marker found, or protected marker encountered.');
  330.       Halt(1);
  331.     end;
  332.     FindMark := b;
  333.   end {findmark} ;
  334.  
  335.   function Hex(i : Integer) : HexString;
  336.     {-return hex representation of integer}
  337.   const
  338.     hc : array[0..15] of Char = '0123456789ABCDEF';
  339.   var
  340.     l, h : Byte;
  341.  
  342.   begin
  343.     l := Lo(i); h := Hi(i);
  344.     Hex := hc[h shr 4]+hc[h and $F]+hc[l shr 4]+hc[l and $F];
  345.   end {hex} ;
  346.  
  347.   procedure ReadMarkFile(markName : AllStrings);
  348.     {-read the mark file info into memory}
  349.   var
  350.     f : file;
  351.  
  352.   begin
  353.     Assign(f, markName);
  354.     Reset(f, 1);
  355.     {read the vector table from the mark file, into a temporary memory area}
  356.     BlockRead(f, Vectors, 1024);
  357.     {read the number of handles stored}
  358.     BlockRead(f, FilMarkHandles, 2);
  359.     {get a page map area and read the page map into it}
  360.     GetMem(FilMarkPageMap, 4*FilMarkHandles);
  361.     BlockRead(f, FilMarkPageMap^, 4*FilMarkHandles);
  362.     Close(f);
  363.     {delete the mark file so it causes no mischief later}
  364.     Erase(f);
  365.   end {ReadMarkFile} ;
  366.  
  367.   procedure CopyVectors(bottomBlock : BlockType; vectorOffset : Integer);
  368.     {-put interrupt vectors back into table}
  369.  
  370.   begin
  371.     {interrupts off}
  372.     inline($FA);
  373.     {restore the main interrupt vector table}
  374.     if FilMark then
  375.       Move(Vectors, Mem[0:0], 1024)
  376.     else
  377.       Move(Mem[Blocks[bottomBlock].psp:vectorOffset], Mem[0:0], 1024);
  378.     {move the old termination/break/error addresses into this program}
  379.     Move(Mem[0:$88], Mem[CSeg:$0A], 12);
  380.     {restore the "parent address" used by the DOS EXIT command to remove a shell}
  381.     Move(Mem[CSeg:$0C], Mem[CSeg:$16], 2);
  382.     {interrupts on}
  383.     inline($FB);
  384.   end {CopyVectors} ;
  385.  
  386.   procedure MarkBlocks(bottomBlock : BlockType);
  387.     {-mark those blocks to be released}
  388.   var
  389.     b, t : BlockType;
  390.     commandPsp, markPsp : Integer;
  391.     ch:char;
  392.  
  393.     function cardinal(i : Integer) : Real;
  394.       {-return "unsigned integer" in range 0..65535}
  395.     var
  396.       r : Real;
  397.  
  398.     begin
  399.       r := i;
  400.       if r < 0.0 then r := r+65536.0;
  401.       cardinal := r;
  402.     end {cardinal} ;
  403.  
  404.   begin
  405.  
  406.     commandPsp := Blocks[2].psp;
  407.     markPsp := Blocks[bottomBlock].psp;
  408.  
  409.     for b := 1 to blockNum do with Blocks[b] do begin
  410.  
  411.       if (b < bottomBlock) then
  412.         {release any trapped environment block}
  413.         releaseIt := (psp<> cseg) and (cardinal(psp) >= cardinal(markPsp))
  414.       else begin
  415.         {release all but RELEASE itself and any blocks owned by COMMAND.COM}
  416.         releaseIt := (psp <> CSeg) and (psp <> commandPsp);
  417.  
  418.         if (psp = commandPsp) then begin
  419.           {warn about the trapping effect of batch files}
  420.           WriteLn('Memory space for TSRs installed prior to batch file');
  421.           WriteLn('will not be released until batch file completes.');
  422.           WriteLn;
  423.           ReturnCode := 1;
  424.           {compute number of bytes temporarily trapped}
  425.           TrappedBytes := 0.0;
  426.           for t := 1 to b do
  427.             if Blocks[t].releaseIt then
  428.               TrappedBytes := TrappedBytes+16.0*cardinal(MemW[Blocks[t].mcb:3]);
  429.         end;
  430.       end;
  431.     end;
  432.  
  433.     if debug then
  434.       for b := 1 to blockNum do with Blocks[b] do
  435.         WriteLn(b:3, ' ', Hex(psp), ' ', Hex(mcb), ' ', releaseIt);
  436.  
  437.   end {MarkBlocks} ;
  438.  
  439.   procedure ReleaseMem;
  440.     {release DOS memory marked for release}
  441.   var
  442.     b : BlockType;
  443.  
  444.   begin
  445.     with Regs do
  446.       for b := 1 to blockNum do with Blocks[b] do
  447.         if releaseIt then begin
  448.           ah := $49;
  449.           {the block is always 1 paragraph above the MCB}
  450.           es := Succ(mcb);
  451.           MsDos(Regs);
  452.           if Odd(flags) then begin
  453.             WriteLn('Could not release block at segment ', Hex(es));
  454.             WriteLn('Memory is now a mess... Please reboot');
  455.             Halt(1);
  456.           end;
  457.         end;
  458.   end {releasemem} ;
  459.  
  460.   function EMSpresent : Boolean;
  461.     {-return true if EMS memory manager is present}
  462.   var
  463.     f : file;
  464.  
  465.   begin
  466.     {"file handle" defined by the expanded memory manager at installation}
  467.     Assign(f, 'EMMXXXX0');
  468.     {$I-}
  469.     Reset(f);
  470.     {$I+}
  471.     EMSpresent := (IOResult = 0);
  472.     Close(f);
  473.   end {EMSpresent} ;
  474.  
  475.   procedure RestoreEMSmap;
  476.     {-restore EMS to state at time of mark}
  477.  
  478.     function EMShandlesActive : Integer;
  479.       {-return the number of active EMS handles}
  480.  
  481.     begin
  482.       Regs.ah := $4B;
  483.       Intr(EMSinterrupt, Regs);
  484.       if Regs.ah <> 0 then begin
  485.         WriteLn('EMS device not responding');
  486.         EMShandlesActive := 0;
  487.         Exit;
  488.       end;
  489.       EMShandlesActive := Regs.bx;
  490.     end {EMShandlesActive} ;
  491.  
  492.     function GetHandles(bottomBlock : BlockType; EMScntOffset : Integer) : Integer;
  493.       {-return the number of handles stored by mark}
  494.     var
  495.       gh : Integer;
  496.  
  497.     begin
  498.       if FilMark then
  499.         GetHandles := FilMarkHandles
  500.       else begin
  501.         Move(Mem[Blocks[bottomBlock].psp:EMScntOffset], gh, 2);
  502.         GetHandles := gh;
  503.       end;
  504.     end {gethandles} ;
  505.  
  506.     function getstoredmap(bottomBlock : BlockType; EMSmapOffset : Integer) : PageArrayPtr;
  507.       {-returns a pointer to the stored page array}
  508.  
  509.     begin
  510.       if FilMark then
  511.         getstoredmap := FilMarkPageMap
  512.       else
  513.         getstoredmap := Ptr(Blocks[bottomBlock].psp, EMSmapOffset);
  514.     end {GetStoredMap} ;
  515.  
  516.     procedure EMSpageMap(var PageMap : PageArray);
  517.       {-return an array of the allocated memory blocks}
  518.  
  519.     begin
  520.       Regs.ah := $4D;
  521.       Regs.es := Seg(PageMap);
  522.       Regs.di := Ofs(PageMap);
  523.       Regs.bx := 0;
  524.       Intr(EMSinterrupt, Regs);
  525.       if Regs.ah <> 0 then
  526.         WriteLn('EMS device not responding');
  527.     end {EMSpageMap} ;
  528.  
  529.     procedure ReleaseEMSblocks(var oldmap, newmap : PageArray);
  530.       {-release those EMS blocks allocated since MARK was installed}
  531.     var
  532.       o, n, nhandle : Integer;
  533.  
  534.       procedure EMSdeallocate(EMShandle : Integer);
  535.         {-release the allocated expanded memory}
  536.  
  537.       begin
  538.         Regs.ah := $45;
  539.         Regs.dx := EMShandle;
  540.         Intr(EMSinterrupt, Regs);
  541.         if Regs.ah <> 0 then begin
  542.           WriteLn('Program error or EMS device not responding');
  543.           WriteLn('EMS memory is now a mess... Please reboot');
  544.           Halt(1);
  545.         end;
  546.       end;                    {EMSdeallocate}
  547.  
  548.     begin
  549.       for n := 1 to EMShandles do begin
  550.         {scan all current handles}
  551.         nhandle := newmap[n].handle;
  552.         if StoredHandles > 0 then begin
  553.           {see if current handle matches one stored by MARK}
  554.           o := 1;
  555.           while (oldmap[o].handle <> nhandle) and (o <= StoredHandles) do
  556.             o := Succ(o);
  557.           {if not, deallocate the current handle}
  558.           if (o > StoredHandles) then
  559.             EMSdeallocate(nhandle);
  560.         end else
  561.           {no handles stored by MARK, deallocate all current handles}
  562.           EMSdeallocate(nhandle);
  563.       end;
  564.     end {releaseEMSblocks} ;
  565.  
  566.   begin
  567.     {see how many EMS handles are currently active}
  568.     EMShandles := EMShandlesActive;
  569.     if EMShandles > MaxHandles then
  570.       WriteLn('EMS process count exceeds capacity of RELEASE - no action taken')
  571.     else if EMShandles <> 0 then begin
  572.       {see how many handles were active when MARK was installed}
  573.       StoredHandles := GetHandles(bottomBlock, EMScntOffset);
  574.       {get the existing EMS page map}
  575.       GetMem(Map, 4*EMShandles);
  576.       EMSpageMap(Map^);
  577.       {get the stored page map}
  578.       StoredMap := getstoredmap(bottomBlock, EMSmapOffset);
  579.       {compare the two maps and deallocate pages not in the stored map}
  580.       ReleaseEMSblocks(StoredMap^, Map^);
  581.     end;
  582.   end;
  583.  
  584. begin
  585.  
  586.   WriteLn;
  587.   ReturnCode := 0;
  588.  
  589.   {see if a particular mark is named}
  590.   if ParamCount > 0 then
  591.     markName := ParamStr(1)
  592.   else
  593.     markName := '';
  594.  
  595.   {get all allocated memory blocks in normal memory}
  596.   FindTheBlocks;
  597.  
  598.   {find the last one marked with the MARK idstring, and MarkName if specified}
  599.   bottomBlock := FindMark(markName);
  600.  
  601.   {mark those blocks to be released}
  602.   MarkBlocks(bottomBlock);
  603.  
  604.   {get file mark information into memory}
  605.   if FilMark then
  606.     ReadMarkFile(markName);
  607.  
  608.   {copy the vector table from the MARK copy}
  609.   CopyVectors(bottomBlock, vectorOffset);
  610.  
  611.   {release normal memory marked for release}
  612.   ReleaseMem;
  613.  
  614.   {deal with expanded memory}
  615.   if EMSpresent then
  616.     RestoreEMSmap;
  617.  
  618.   {DOS will release this program's memory when it exits}
  619.   {write success message}
  620.   Write('RELEASE ', Version, ' - Memory released above last MARK ');
  621.   if markName <> '' then
  622.     WriteLn('(', StUpcase(markName), ')')
  623.   else
  624.     WriteLn;
  625.  
  626.   if ReturnCode <> 0 then
  627.     WriteLn(TrappedBytes:0:0, ' bytes temporarily trapped until batch file completes');
  628.  
  629.   Halt(ReturnCode);
  630. end.
  631.