{$R-,S-,I+}

{**************************************************************************
*   Maps system memory blocks for MS/PCDOS 2.0 and higher.                *
*   Also maps expanded memory allocation blocks                           *
*   Copyright (c) 1986 Kim Kokkonen, TurboPower Software.                 *
*   Released to the public domain for personal, non-commercial use only.  *
***************************************************************************
*   version 1.0 1/2/86                                                    *
*   version 1.1 1/10/86                                                   *
*     running under DOS 2.X, where block owner names are unknown          *
*   version 1.2 1/22/86                                                   *
*     a bug in parsing the owner name of the block                        *
*     a quirk in the way that the DOS PRINT buffer installs itself        *
*     minor cosmetic changes                                              *
*   version 1.3 2/6/86                                                    *
*     smarter filtering for processes that deallocate their environment   *
*   version 1.4 2/23/86                                                   *
*     add a map of Expanded memory (EMS) as well                          *
*   version 1.5 2/26/86                                                   *
*     change format of last memory block                                  *
*     change to more reliable scheme of finding first block               *
*       (thanks to Chris Dunford for pointing out a useful                *
*        undocumented DOS function).                                      *
*     support environment lengths up to 32K                               *
*   version 1.6 3/8/86                                                    *
*     support "verbose" output mode                                       *
*       display open file handles                                         *
*       show command line of each block                                   *
*   version 1.7 3/24/86                                                   *
*     work around Turbo 3.00B bug with Delete procedure and length 255    *
*     filter out command lines of programs which relocate over their      *
*       command line at PSP:$80                                           *
*     fix treatment of handle counts from PSP                             *
*     add display of number of memory blocks per PSP to verbose mode      *
*     accept V, -V, or /V for the verbose switch                          *
*   version 1.8 4/20/86                                                   *
*     change verbose mode to show each block individually                 *
*   version 1.9 5/22/86                                                   *
*     synchronize with RELEASE                                            *
*   version 2.0 6/17/86                                                   *
*     synchronize with RELEASE                                            *
*   version 2.1 7/18/86                                                   *
*     wrap long vector lists                                              *
*   version 2.2 3/4/87                                                    *
*     add support for WATCH files                                         *
*   version 2.3 5/1/87                                                    *
*     use in-memory WATCH data                                            *
*     display disabled status of TSRs                                     *
*   version 2.4 5/17/87                                                   *
*     avoid use of EMS call $4B, which doesn't work in many EMS           *
*       implementations                                                   *
*   version 2.5 5/26/87                                                   *
*     correct problem with MAPMEM run in batch file with WATCH            *                                    *
*   version 2.6 1/15/89                                                   *
*     make changes to deal with 386-to-the-Max                            *
*     convert to Turbo Pascal 5.0                                         *
*   version 2.7                                                           *
*     skipped                                                             *
*   version 2.8 3/10/89                                                   *
*     clean up a few Turbo 3 leftovers                                    *
*     add total extended memory report                                    *
*   version 2.9 5/4/89                                                    *
*     fix bug when no EMS blocks allocated                                *
***************************************************************************
*   telephone: 408-438-8608, CompuServe: 72457,2131.                      *
*   requires Turbo version 5 to compile.                                  *
***************************************************************************}

program MapMem;
  {-look at the system memory map using DOS memory control blocks}
uses
  dos;

const
  Version = '2.9';
  MaxBlocks = 100;            {max number of DOS memory blocks checked}
  MaxVector = $FF;            {highest interrupt vector checked for trapping}

  WatchID = 'TSR WATCHER';    {marking string for WATCH}

  {offsets into resident copy of WATCH.COM for data storage}
  WatchOffset = $81;
  NextChange = $104;
  ChangeVectors = $220;
  OrigVectors = $620;
  CurrVectors = $A20;

  ATclass = $FC;              {machine ID bytes}
  Model80 = $F8;

type
  Pathname = string[64];
  AllStrings = string[255];

  BlockType = 0..MaxBlocks;
  Block =
  record                      {store info about each memory block as it is found}
    idbyte : Byte;
    mcb : Word;
    psp : Word;
    len : Word;
    psplen : Word;
    env : Word;
    cnt : Word;
  end;
  BlockArray = array[BlockType] of Block;

var
  Blocks : BlockArray;
  WatchBlock, BlockNum : BlockType;
  WroteHeader, UseHook, Verbose, UseWatch : Boolean;
  MachineId : Byte absolute $FFFF : $000E;

  procedure Abort(msg : AllStrings);
    {-halt in case of error}
  begin
    WriteLn(msg);
    Halt(1);
  end;

  function StUpcase(s : Pathname) : Pathname;
    {-return the upper case of a string}
  var
    i : Byte;
  begin
    for i := 1 to Length(s) do
      s[i] := UpCase(s[i]);
    StUpcase := s;
  end;

  procedure FindTheBlocks;
    {-scan memory for the allocated memory blocks}
  const
    MidBlockID = $4D;         {byte DOS uses to identify part of MCB chain}
    EndBlockID = $5A;         {byte DOS uses to identify last block of MCB chain}
  var
    mcbSeg : Word;         {segment address of current MCB}
    nextSeg : Word;        {computed segment address for the next MCB}
    gotFirst : Boolean;       {true after first MCB is found}
    gotLast : Boolean;        {true after last MCB is found}
    idbyte : Byte;            {byte that DOS uses to identify an MCB}

    function GetStartMCB : Word;
      {-return the first MCB segment}
    var
      reg : registers;
    begin
      reg.ah := $52;
      MsDos(reg);
      GetStartMCB := MemW[reg.es:(reg.bx-2)];
    end;

    procedure StoreTheBlock(var mcbSeg, nextSeg : Word;
                            var gotFirst, gotLast : Boolean);
      {-store information regarding the memory block}
    var
      nextID : Byte;
      pspAdd : Word;       {segment address of the current PSP}
      mcbLen : Word;       {size of the current memory block in paragraphs}

    begin

      mcbLen := MemW[mcbSeg:3]; {size of the MCB in paragraphs}
      nextSeg := Succ(mcbSeg+mcbLen); {where the next MCB should be}
      pspAdd := MemW[mcbSeg:1]; {address of program segment prefix for MCB}
      nextID := Mem[nextSeg:0];

      if gotLast or (nextID = EndBlockID) or (nextID = MidBlockID) then begin
        BlockNum := Succ(BlockNum);
        gotFirst := True;
        with Blocks[BlockNum] do begin
          idbyte := Mem[mcbSeg:0];
          mcb := mcbSeg;
          psp := pspAdd;
          env := MemW[pspAdd:$2C];
          len := mcbLen;
          psplen := 0;
          cnt := 1;
        end;
      end;

    end;

  begin

    {initialize}
    mcbSeg := GetStartMCB;
    gotFirst := False;
    gotLast := False;
    BlockNum := 0;

    {scan all memory until the last block is found}
    repeat
      idbyte := Mem[mcbSeg:0];
      if idbyte = MidBlockID then begin
        StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
        if gotFirst then mcbSeg := nextSeg else mcbSeg := Succ(mcbSeg);
      end else if gotFirst and (idbyte = EndBlockID) then begin
        gotLast := True;
        StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
      end else
        {start block was invalid}
        Abort('corrupted allocation chain or program error');
    until gotLast;

  end;

  function FindMark(markName : AllStrings; markOffset : Word) : Word;
    {-find the last memory block matching idstring at offset idoffset}
  var
    b : BlockType;
    MemMark : Boolean;

    function HasIDstring(segment : Word;
                         idString : AllStrings;
                         idOffset : Word) : Boolean;
      {-return true if idstring is found at segment:idoffset}
    var
      len : Byte absolute idString;
      tString : AllStrings;
      tlen : Byte absolute tString;

    begin
      tlen := len;
      Move(Mem[segment:idOffset], tString[1], len);
      HasIDstring := (tString = idString);
    end;

  begin
    {scan from the last block down to find the last MARK TSR}
    b := BlockNum;
    MemMark := False;
    repeat
      if Blocks[b].psp = PrefixSeg then
        {assure this program's command line is not matched}
        b := Pred(b)
      else if HasIDstring(Blocks[b].psp, markName, markOffset) then
        {Mark found}
        MemMark := True
      else
        {Keep looking}
        b := Pred(b);
    until (b < 1) or MemMark;

    UseWatch := MemMark;
    FindMark := b;

  end;

  procedure StripNonAscii(var t : Pathname);
    {-return an empty string if t contains any non-printable characters}
  var
    ipos : Byte;
    goodname : Boolean;
  begin
    goodname := True;
    for ipos := 1 to Length(t) do
      if (t[ipos] <> #0) and ((t[ipos] < ' ') or (t[ipos] > '}')) then
        goodname := False;
    if not(goodname) then t := '';
  end;

  function DOSversion : Byte;
    {-return the major version number of DOS}
  var
    reg : registers;
  begin
    reg.ah := $30;
    MsDos(Dos.Registers(reg));
    DOSversion := reg.al;
  end;

  procedure ShowTheBlocks;
    {-analyze and display the blocks found}
  const
    hookst : string[14] = 'hooked vectors';
    chainst : string[15] = 'chained vectors';
  type
    HexString = string[4];
    Address = record
                offset, segment : Word;
              end;
    VectorType = 0..MaxVector;
  var
    st, cline : Pathname;
    b : BlockType;
    StLen, DOSv : Byte;
    CommandPSP, WatchPSP : Word;
    Vectors : array[VectorType] of Address absolute 0 : 0;
    Vtable : array[VectorType] of LongInt;
    SumNum : BlockType;
    Sum : BlockArray;

    function HexB(b : Byte) : HexString;
      {-return hex representation of byte}
    const
      hc : array[0..15] of Char = '0123456789ABCDEF';
    begin
      HexB := hc[b shr 4]+hc[b and $F];
    end;

    function HexW(i : Word) : HexString;
      {-return hex representation of Word}
    begin
      HexW := HexB(Hi(i))+HexB(Lo(i));
    end;

    function Owner(startadd : Word) : Pathname;
      {-return the name of the owner program of an MCB}
    type
      chararray = array[0..32767] of Char;
    var
      e : ^chararray;
      i : Word;
      t : Pathname;

      function LongPos(m : Pathname; var s : chararray) : Word;
        {-return the position number of m in s, or 0 if not found}
      var
        mc : Char;
        ss : Pathname;
        i, maxindex : Word;
        found : Boolean;
      begin
        i := 0;
        maxindex := SizeOf(s)-Length(m);
        ss[0] := m[0];
        if Length(m) > 0 then begin
          mc := m[1];
          repeat
            while (s[i] <> mc) and (i <= maxindex) do
              i := Succ(i);
            if s[i] = mc then begin
              Move(s[i], ss[1], Length(m));
              found := (ss = m);
              if not(found) then i := Succ(i);
            end;
          until found or (i > maxindex);
          if not(found) then i := 0;
        end;
        LongPos := i;
      end;

      procedure StripPathname(var pname : Pathname);
        {-remove leading drive or path name from the input}
      var
        spos, cpos, rpos : Byte;
      begin
        spos := Pos('\', pname);
        cpos := Pos(':', pname);
        if spos+cpos = 0 then Exit;
        if spos <> 0 then begin
          {find the last slash in the pathname}
          rpos := Length(pname);
          while (rpos > 0) and (pname[rpos] <> '\') do rpos := Pred(rpos);
        end else
          rpos := cpos;
        Delete(pname, 1, rpos);
      end;

      procedure StripExtension(var pname : Pathname);
        {-remove the file extension}
      var
        dotpos : Byte;
      begin
        dotpos := Pos('.', pname);
        if dotpos <> 0 then
          Delete(pname, dotpos, 64); {<255 needed for Turbo version 3.00B bug}
      end;

    begin
      {point to the environment string}
      e := Ptr(startadd, 0);

      {find end of the standard environment}
      i := LongPos(#0#0, e^);
      if i = 0 then begin
        {something's wrong, exit gracefully}
        Owner := '';
        Exit;
      end;

      {end of environment found, get the program name that follows it}
      t := '';
      i := i+4;               {skip over #0#0#args}
      repeat
        t := t+e^[i];
        i := Succ(i);
      until (Length(t) > 63) or (e^[i] = #0);

      StripNonAscii(t);
      if t = '' then
        Owner := 'N/A'
      else begin
        StripPathname(t);
        StripExtension(t);
        if t = '' then t := 'N/A';
        Owner := StUpcase(t);
      end;

    end;

    procedure InitVectorTable;
      {-build real equivalent of vector addresses}
    var
      v : VectorType;

      function RealAdd(a : Address) : LongInt;
        {-return the real equivalent of an address (pointer)}
      begin
        with a do
          RealAdd := (LongInt(segment) shl 4)+offset;
      end;

    begin
      for v := 0 to MaxVector do
        Vtable[v] := RealAdd(Vectors[v]);
    end;

    procedure WriteVecs(start, stop, startcol, wrapcol : Word);
      {-Show either trapped or chained interrupt vectors}

      procedure WriteHooks(start, stop, startcol, wrapcol : Word);
        {-show the trapped interrupt vectors}
      var
        v : VectorType;
        sadd, eadd : LongInt;
        col : Word;
      begin
        sadd := LongInt(start) shl 4;
        eadd := LongInt(stop) shl 4;
        col := startcol;
        for v := 0 to MaxVector do
          if (Vtable[v] >= sadd) and (Vtable[v] <= eadd) then begin
            if col+3 > wrapcol then begin
              {wrap to next line}
              WriteLn;
              Write('':Pred(startcol));
              col := startcol;
            end;
            Write(HexB(v), ' ');
            col := col+3;
          end;
      end;

      procedure WriteChained(pspA, startcol, wrapcol : Word);
        {-Write Chained interrupts as determined from watch data}
      type
        ChangeBlock =
        record                {Store info about each vector takeover}
          VecNum : Byte;
          case ID : Byte of
            0, 1 : (VecOfs, VecSeg : Word);
            2 : (SaveCode : array[1..6] of Byte);
            $FF : (pspAdd : Word);
        end;
        {
        ID is interpreted as follows:
        00 = ChangeBlock holds the new pointer for vector vecnum
        01 = ChangeBlock holds pointer for vecnum but the block is disabled
        02 = ChangeBlock holds the code underneath the vector patch
        FF = ChangeBlock holds the segment of a new PSP
        }
      var
        p : ^ChangeBlock;
        i, maxchg, col : Word;
        found : Boolean;
      begin
        {Initialize}
        maxchg := MemW[WatchPSP:NextChange];
        col := startcol;
        found := False;
        i := 0;

        while i < maxchg do begin
          p := Ptr(WatchPSP, ChangeVectors+i);
          with p^ do
            case ID of
              $FF :           {ChangeBlock starts a new PSP}
                found := (pspA = pspAdd);
              $00 :           {ChangeBlock describes an active vector takeover}
                if found then begin
                  {ChangeBlock specifies a vector taken over}
                  if col >= wrapcol then begin
                    Write(^M^J, '':Pred(startcol));
                    col := startcol;
                  end;
                  Write(HexB(Lo(VecNum)), ' ');
                  col := col+3;
                end;
              $01 :           {ChangeBlock specifies a disabled takeover}
                if found then begin
                  Write('disabled');
                  {Don't write this more than once}
                  Exit;
                end;
            end;
          i := i+SizeOf(ChangeBlock);
        end;
      end;

    begin
      if start <> stop then
        if UseWatch then
          WriteChained(start, startcol, wrapcol)
        else
          WriteHooks(start, stop, startcol, wrapcol);
    end;

    procedure SortByPSP(var Blocks : BlockArray; BlockNum : BlockType);
      {-sort in order of ascending PSP}
    var
      i, j : BlockType;
      temp : Block;
    begin
      for i := 1 to Pred(BlockNum) do
        for j := BlockNum downto Succ(i) do
          if Blocks[j].psp < Blocks[Pred(j)].psp then begin
            temp := Blocks[j];
            Blocks[j] := Blocks[Pred(j)];
            Blocks[Pred(j)] := temp;
          end;
    end;

    procedure SumTheBlocks(var Blocks : BlockArray;
                           BlockNum : BlockType;
                           var Sum : BlockArray;
                           var SumNum : BlockType);
      {-combine the blocks with equivalent PSPs}
    var
      prevPSP : Word;
      b : BlockType;
    begin
      SumNum := 0;
      prevPSP := 0;
      for b := 1 to BlockNum do begin
        if Blocks[b].psp <> prevPSP then begin
          SumNum := Succ(SumNum);
          Sum[SumNum] := Blocks[b];
          prevPSP := Blocks[b].psp;
          if prevPSP = PrefixSeg then
            {don't include the environment as part of free block's length}
            Sum[SumNum].len := 0;
        end else
          with Sum[SumNum] do begin
            cnt := Succ(cnt);
            len := len+Blocks[b].len;
          end;
        {get length of the block which owns the executable program}
        {for checking vector trapping next}
        if Succ(Blocks[b].mcb) = Blocks[b].psp then
          Sum[SumNum].psplen := Blocks[b].len;
      end;
    end;

    procedure TransferTheBlocks(var Blocks : BlockArray;
                                BlockNum : BlockType;
                                var Sum : BlockArray;
                                var SumNum : BlockType);
      {-fill in the Sum array with a little initialization}
    var
      b : BlockType;
    begin
      for b := 1 to BlockNum do begin
        Sum[b] := Blocks[b];
        with Sum[b] do begin
          cnt := 1;
          if (Succ(mcb) = psp) and (psp <> 0) then
            psplen := len
          else
            psplen := 0;
        end;
      end;
      SumNum := BlockNum;
    end;

    function OpenHandles(psp : Word) : Word;
      {-return the number of open handles owned by a process}
    var
      h, o : Word;
      b : Byte;
    begin
      h := 0;
      if (psp <> 8) and (cline <> 'N/A') then
        for o := 0 to 19 do begin
          b := Mem[psp:$18+o];
          if not(b in [$FF, 0..2]) then
            h := Succ(h);
        end;
      OpenHandles := h;
    end;

    function CommandLine(psp : Word) : Pathname;
      {-return the command line of the PSP}
    var
      t, s : Pathname;
    begin
      if (psp <> 8) then begin
        Move(Mem[psp:$80], t, 65);
        if t[0] > #64 then t[0] := #64;
        s := t;
        StripNonAscii(t);
        if s <> t then
          {command line has been written over}
          t := 'N/A'
        else
          {strip leading blanks}
          while (Length(t) > 0) and (t[1] = #32) do Delete(t, 1, 1);
      end else
        {psp=8 is a special block owned by DOS containing the CONFIG.SYS drivers}
        t := '';
      CommandLine := t;
    end;

    function PrevBlock(b : BlockType; psp : Word) : BlockType;
      {-return highest block with number less than b having a PSP matching psp}
      {-return 0 if none}
    var
      t : BlockType;
      found : Boolean;
    begin
      found := False;
      t := Pred(b);
      while (t > 0) and not(found) do begin
        found := (Sum[t].psp = psp);
        if not(found) then t := Pred(t);
      end;
      PrevBlock := t;
    end;

    procedure WriteTitle;
    begin
      Write('Allocated Memory Map - by TurboPower Software - Version ', Version);

      if Verbose then begin
        WriteLn('  (verbose)');
        WriteLn;
        Write(' PSP  MCB files bytes owner    command line  ');
        if UseWatch then
          WriteLn(chainst)
        else
          WriteLn(hookst);
        WriteLn('---- ---- ----- ----- -------- ------------- -----------------------------');
      end else begin
        WriteLn;
        WriteLn;
        Write(' PSP  blks bytes owner    command line        ');
        if UseWatch then
          WriteLn(chainst)
        else
          WriteLn(hookst);
        WriteLn('----- ---- ----- -------- ------------------- ------------------------------');
      end;
    end;

  begin

    WriteTitle;

    {Get critical PSP addresses before sorting blocks}
    CommandPSP := Blocks[2].psp;
    if UseWatch then
      WatchPSP := Blocks[WatchBlock].psp
    else
      InitVectorTable;

    {Rearrange the blocks for presentation}
    if Verbose then
      TransferTheBlocks(Blocks, BlockNum, Sum, SumNum)
    else begin
      SortByPSP(Blocks, BlockNum);
      SumTheBlocks(Blocks, BlockNum, Sum, SumNum);
    end;

    {Get DOS version number to see whether environment has program names}
    DOSv := DOSversion;

    for b := 1 to SumNum do
    with Sum[b] do begin

      {get the command line which invoked the program}
      if b = SumNum then
        cline := ''
      else
        cline := CommandLine(psp);

      {write out numerical information}
      Write(HexW(psp), ' ');   {PSP address}
      if Verbose then begin
        Write(HexW(mcb), '  ', {MCB address}
        OpenHandles(psp):2, '  '); {number of open file handles}
      end else
        Write(cnt:3, '  ');   {number of blocks}

      Write((LongInt(len) shl 4):6, ' '); {size of block in bytes}

      {get the program owning this block by scanning the environment}
      if psp = PrefixSeg then
        st := 'free'
      else if psp = CommandPSP then
        st := 'command'
      else if psp = Sum[1].psp then
        st := 'config'
      else if (DOSv >= 3) then begin
        if Verbose then begin
          if Succ(mcb) = env then
            {this is the environment block}
            st := Owner(env)
          else if PrevBlock(b, psp) <> 0 then
            {this is the block that goes with the environment}
            st := Owner(Sum[PrevBlock(b, psp)].env)
          else
            st := 'N/A';
        end else if cnt > 1 then
          st := Owner(env)
        else
          st := 'N/A';
      end else
        st := 'N/A';
      while Length(st) < 9 do
        st := st+' ';
      Write(st);

      {write the command line that invoked the program}
      if Verbose then
        StLen := 13
      else
        StLen := 19;
      if Length(cline) > StLen-3 then
        cline := Copy(cline, 1, StLen-3)+'...'
      else
        while Length(cline) < StLen do cline := cline+' ';
      Write(cline, ' ');

      {write the trapped interrupt vectors}
      if Verbose then
        WriteVecs(psp, psp+psplen, 46, 75)
      else if (psp <> PrefixSeg) then
        WriteVecs(psp, psp+psplen, 47, 75);

      WriteLn;
    end;

  end;

  procedure ShowTheEMSblocks;
    {-map out expanded memory, if present}
  const
    EMSinterrupt = $67;       {the vector used by the expanded memory manager}
    MaxHandles = 255;

  type
    HandlePageRecord =
    record
      handle : Word;
      numpages : Word;
    end;

    PageArray = array[0..MaxHandles] of HandlePageRecord;
    PageArrayPtr = ^PageArray;
    Pathname = string[64];

  var
    EMSregs : registers;
    EMShandles : Word;
    Map : PageArrayPtr;
    TotalPages : Word;

    function EMSpresent : Boolean;
      {-Return true if EMS memory manager is present}
    var
      f : file;
    begin
      {"file handle" defined by the expanded memory manager at installation}
      Assign(f, 'EMMXXXX0');
      {$I-}
      Reset(f);
      {$I+}
      if IOResult = 0 then begin
        EMSpresent := True;
        Close(f);
      end else
        EMSpresent := False;
    end;

    function EMSpagesAvailable(var TotalPages : Word) : Word;
      {-return the number of 16K expanded memory pages available and unallocated}
    begin
      EMSregs.ah := $42;
      Intr(EMSinterrupt, Dos.Registers(EMSregs));
      if EMSregs.ah <> 0 then begin
        WriteLn('EMS device not responding');
        EMSpagesAvailable := 0;
        Exit;
      end;
      EMSpagesAvailable := EMSregs.bx;
      TotalPages := EMSregs.dx;
    end;

    procedure EMSpageMap(var PageMap : PageArray; var EMShandles : Word);
      {-return an array of the allocated memory blocks}
    begin
      EMSregs.ah := $4D;
      EMSregs.es := Seg(PageMap);
      EMSregs.di := Ofs(PageMap);
      EMSregs.bx := 0;
      Intr(EMSinterrupt, Dos.Registers(EMSregs));
      if EMSregs.ah <> 0 then begin
        WriteLn('EMS device not responding');
        EMShandles := 0;
      end else
        EMShandles := EMSregs.bx;
    end;

    procedure WriteEMSmap(PageMap : PageArray; handles : Word);
      {-write out the EMS page map}
    var
      h : Word;
    begin
      WroteHeader := True;
      WriteLn('block   bytes   (Expanded Memory)');
      WriteLn('-----   ------');
      if Handles > 0 then
        for h := 0 to Pred(handles) do
          if PageMap[h].numpages <> 0 then
            WriteLn(h:5, '  ', (LongInt(16384)*PageMap[h].numpages):7);
    end;

  begin
    if not(EMSpresent) then
      Exit;
    WriteLn;
    {Get space for the largest possible page map}
    GetMem(Map, 2048);
    EMSpageMap(Map^, EMShandles);
    WriteEMSmap(Map^, EMShandles);
    WriteLn(' free  ', (16384*LongInt(EMSpagesAvailable(TotalPages))):7);
    WriteLn('total  ', (16384*LongInt(TotalPages)):7);
  end;

  function ExtMemTotalPrim : LongInt;
    {-Returns total number of bytes of extended memory in the system.}
  inline(
    $B4/$88/                 {mov ah,$88     ;get extended memory function}
    $CD/$15/                 {int $15        ;returns KB avail in AX}
    $B9/$00/$04/             {mov cx,1024    ;CX = 1024 = bytes per KB}
    $F7/$E1);                {mul cx         ;DX:AX has result}

  function ExtMemPossible : Boolean;
    {-Return true if extended memory is possible}
  begin
    {don't allow allocation if running PC or XT, or under DOS 2.x or OS/2}
    ExtMemPossible := False;
    case DosVersion of
      3..4 :
        case MachineId of
          ATclass, Model80 : ExtMemPossible := True;
        end;
    end;
  end;

  procedure ShowTheExtendedMemory;
    {-Show extended memory, if any}
  var
    Total : LongInt;
  begin
    if ExtMemPossible then
      Total := ExtMemTotalPrim
    else
      Total := 0;
    if Total = 0 then
      Exit;
    if WroteHeader then
      WriteLn('                (Extended Memory)')
    else begin
      WroteHeader := True;
      WriteLn;
      WriteLn('block   bytes   (Extended Memory)');
      WriteLn('-----   ------');
    end;
    WriteLn('total  ', Total:7);
  end;

  procedure GetOptions;
    {-Analyze command line for options}
  const
    unknop : string[24] = 'Unknown command option: ';
  var
    arg : AllStrings;
    arglen : Byte absolute arg;
    i : Word;

    procedure WriteHelp;
      {-Show the options}
    begin
      WriteLn('MAPMEM ', Version, ', by TurboPower Software');
      WriteLn;
      WriteLn('MAPMEM produces a report showing what memory resident programs are installed,');
      WriteLn('how much memory each uses, and what interrupt vectors are taken over.');
      WriteLn;
      WriteLn('MAPMEM accepts the following command line syntax:');
      WriteLn;
      WriteLn('  MAPMEM [Options]');
      WriteLn;
      WriteLn('Options may be preceded by either / or -. Valid options are as follows:');
      WriteLn('     /H     do not use WATCH information.');
      WriteLn('     /V     verbose report.');
      WriteLn('     /?     write this help screen.');
      Halt(1);
    end;

  begin
    {Initialize defaults}
    Verbose := False;
    UseHook := False;
    WroteHeader := False;

    i := 1;
    while i <= ParamCount do begin
      arg := ParamStr(i);
      if (arg[1] = '?') then
        WriteHelp
      else if (arg[1] = '-') or (arg[1] = '/') then
        case arglen of
          1 : Abort('Missing command option following '+arg);
          2 : case UpCase(arg[2]) of
                '?' : WriteHelp;
                'H' : UseHook := True;
                'V' : Verbose := True;
              else
                Abort(unknop+arg);
              end;
        else
          Abort(unknop+arg);
        end
      else
        Abort(unknop+arg);
      i := Succ(i);
    end;

  end;

begin
  GetOptions;
  FindTheBlocks;
  WatchBlock := FindMark(WatchID, WatchOffset);
  UseWatch := UseWatch and not(UseHook);
  ShowTheBlocks;
  ShowTheEMSblocks;
  ShowTheExtendedMemory;
end.
