{---------------------------------------------------------}
{  Project : Call Stack Reporter                          }
{  Auteur  : Ir. G.W. van der Vegt                        }
{            Hondsbroek 57                                }
{            6121 XB Born                                 }
{---------------------------------------------------------}
{  Datum .tijd  Revisie                                   }
{  920713.2100  Creatie.                                  }
{  920715.2330  Trace at normal exit (exitcode=0) removed.}
{  920805.2230  Path removed from filename in trace       }
{  920806.2200  Blanks filled in, RunTime Library routines}
{               now traced to.                            }
{  921026.2000  Textmode(lastmode) added to default       }
{               Csr_report. Objects & overlay tracing     }
{               tested.                                   }
{  921118.1400  Exitcode doesn't trigger trace anymore    }
{  931114.1430  Keyboard flush in exitprocedure           }
{  940201.2200  Made independed of Routines.              }
{---------------------------------------------------------}
{  To do        Trace Virtual Methode Table (VMT)         }
{---------------------------------------------------------}

{$D+}
{$L+}

{---------------------------------------------------------}
{----This unit gives the line numbers & filenames at error}
{    The result is a list of the call stack as produced by}
{    the Turbo Pascal IDE.                                }
{                                                         }
{    The internal text mode report function can be        }
{    replaced by another one located in your program.     }
{    This could be a graphics mode or printer version. It }
{    must be compiled far (so use $F+ & $F- around it.    }
{    It's called once for each call level.                }
{                                                         }
{    This program parses the MAP file to obtain the       }
{    line numbers. It searches for the MAP file in the    }
{    programs startup directory as obtained by            }
{    PARAMSTR(0).                                         }
{---------------------------------------------------------}
{    To obtain all possible info compile with the         }
{    following setting :                                  }
{                                                         }
{    OPTIONS/LINKER/MAP FILE      = DETAILED              }
{    OPTION/COMPILE/DEBUG INFO    = ON                    }
{                                                         }
{    The last can also be forced by the $D+ compiler      }
{    directive .                                          }
{                                                         }
{    This version traces procedures, functions through    }
{    the main program and it's (overlayed) units. It also }
{    traces static methodes but not virtual methodes.     }
{    When tracing static methodes a phantom entry with    }
{    an call address located oon the heap is generated.   }
{    The trace is stopped at the first call to a virtual  }
{    methode. In a future version VMT tracing will be     }
{    added as soon as I start using virtual methodes.     }
{---------------------------------------------------------}

UNIT CSR_01;

INTERFACE

{---------------------------------------------------------}
{----TYPES                                                }
{---------------------------------------------------------}

TYPE
  Csr_repfunc  = PROCEDURE(level : Word;csr : STRING);

{---------------------------------------------------------}
{----VARIABLES                                            }
{---------------------------------------------------------}

VAR
  Csr_reporter : Csr_repfunc;

{---------------------------------------------------------}
{----PROCEDURES/FUNCTIONS                                 }
{---------------------------------------------------------}

PROCEDURE Csr_report(level : Word;csr : STRING);

{---------------------------------------------------------}

IMPLEMENTATION

Uses
  CRT,
  DOS;

VAR
  ext     : extstr;
  dir     : dirstr;
  nam     : namestr;
  mapfile : BOOLEAN;
  map     : Text;
  ft      : BOOLEAN;

CONST
  space   = #32;

{---------------------------------------------------------}
{----SUPPORT PROCEDURES & FUNCTIONS                       }
{---------------------------------------------------------}

FUNCTION Istr(i,n : INTEGER;pad : CHAR) : STRING;

VAR
  s : STRING;

BEGIN
  Str(i:n,s);
  IF (pad<>space)
    THEN
      WHILE (Pos(space,s)>0) DO
        s[Pos(space,s)]:=pad;
  Istr:=s;
END; {of Istr}

{---------------------------------------------------------}

FUNCTION  Wstr(w : WORD;n : INTEGER) : STRING;

VAR
  s : STRING;

BEGIN
  Str(w:n,s);
  Wstr:=s;
END; {of Wstr}

{---------------------------------------------------------}

FUNCTION  Sstr(s : STRING;n : INTEGER) : STRING;

VAR
  tmp : STRING;

BEGIN
  tmp:=s;
  IF n>=0
    THEN WHILE (Length(tmp)<+n) DO Insert(space,tmp,1)
    ELSE WHILE (Length(tmp)<-n) DO tmp:=tmp+space;
  Sstr:=tmp;
END; {of Sstr}

{---------------------------------------------------------}

PROCEDURE Beep;

BEGIN
  Sound(500);
  Delay(20);
  Nosound;
END; {of Beep}

{---------------------------------------------------------}

FUNCTION Word2Hex(w : Word) : STRING;

const
  hexChars : array [0..$F] of Char = '0123456789ABCDEF';

begin
  Word2Hex :=hexChars[Hi(w) shr 4]+hexChars[Hi(w) and $F]+
             hexChars[Lo(w) shr 4]+hexChars[Lo(w) and $F];
end; {of Word2Hex}

{---------------------------------------------------------}

Function Hex2Word(h : String) : word;

const
  hexChars : String[16] = '0123456789ABCDEF';

var
  f : word;

begin
  f := 0;
  while length(h) > 0 do
     begin
       if pos(Copy(h,1,1),HexChars) = 0
         then f := 0
         Else f := (f*16)+pos(H[1],Hexchars)-1;
       delete(h,1,1);
     end;
  Hex2Word:= f;
end; {of Hex2Word}

{---------------------------------------------------------}

FUNCTION Ptr2Hex(p : POINTER) : STRING;

BEGIN
  IF (p=nil)
    THEN Ptr2Hex := '   NIL   '
    else Ptr2Hex := Word2hex(Seg(P^))+':'+Word2hex(Ofs(P^));
END; {of Ptr2Hex}

{---------------------------------------------------------}

Procedure FlushKbd;

Begin
  MemW[$40:$1C]:=MemW[$40:$1A];
End; {of Fluskkbd}

{---------------------------------------------------------}
{----STACK TRACE ROUTINES START HERE                      }
{---------------------------------------------------------}

FUNCTION BPreg : WORD;

INLINE($55/$58); {Push BP, Pop AX}

{---------------------------------------------------------}

Procedure Findlineno(first,near : BOOLEAN;dep : Word;p : Pointer);

VAR
  tmp     : String[80];

  line    : Integer;
  adr     : String[9];
  ch      : Char;

  fn      : STRING[80];
  un      : STRING[80];

  errseg,
  errofs  : Word;

  s,
  lastun,
  lastpr,
  lastfn  : STRING[80];
  lastnr  : Word;
  call    : STRING[4];

BEGIN
  IF near
    THEN call:='near'
    ELSE call:='far ';

  errseg:=Hex2word(Copy(Ptr2hex(p),1,4));
  errofs:=Hex2word(Copy(Ptr2hex(p),6,4));

  lastnr:=0;
  lastfn:='';
  lastpr:='';
  lastun:='';

  Assign(map,dir+nam+'.MAP');
  {$I-} Reset(map); {$I+}
  IF (IOResult=0)
    THEN
      BEGIN
      {----Fist try on unit/program name}
        s:='';
{
 00000H 00096H 00097H VALTOREN           CODE

  Address         Publics by Value
}
        WHILE NOT(Eof(map) OR
                  (Pos('Publics by Value',s)>0) OR
                  (Pos('Line numbers'   ,s)>0)) DO
          BEGIN
            Readln(map,s);
            IF (Length(s)>=45) AND (s[7]='H')
              THEN
                BEGIN
                  IF (Errseg=Hex2Word(Copy(s,2,4))) {AND
                     (Copy(s,42,4)='CODE')}
                    THEN lastun:=Copy(s,23,18);
                END;
          END;

      {----Strip Trailing Blanks}
        WHILE (Length(lastun)>0) AND
              (lastun[Length(lastun)]=#32) DO
          Delete(lastun,Length(lastun),1);

      {----Second Try to find procedure name}
        s:='';
{
  Address         Publics by Value

 0000:0000       @
 000A:00CB       MENU_INIT
}
        WHILE NOT(Eof(map) OR
                  (Pos('Line numbers',s)>0)) DO
          BEGIN
            Readln(map,s);
            IF (Length(s)>=18) AND (s[6]=':')
              THEN
                BEGIN
                  IF (Errseg=Hex2Word(Copy(s,2,4)))
                    THEN
                      BEGIN
                        IF (lastpr='')
                          THEN lastpr:=Copy(s,18,Length(s)-17)
                          ELSE
                            IF (Errofs>=Hex2Word(Copy(s,7,4)))
                              THEN lastpr:=Copy(s,18,Length(s)-17);
                      END;
                END;
          END;

      {----Strip Trailing Blanks}
        WHILE (Length(lastpr)>0) AND
              (lastpr[Length(lastpr)]=#32) DO
          Delete(lastpr,Length(lastpr),1);

      {----Third try on line numbers & sourcefile names}
        REPEAT
{
  Line numbers for TEST_ERROR(TEST_ERR.PAS) segment TEST_ERROR
}
          IF (Pos('Line numbers',s)>0)
            THEN
              BEGIN
                Delete(s,1,17);
                un:=Copy(s,1,Pos('(',s)-1);
                Delete(s,1,Pos('(',s));
                fn:=Copy(s,1,Pos(')',s)-1);

                While Pos('\',fn)>0 DO Delete (fn,1,Pos('\',fn));

                Readln(map);
                REPEAT
{
  15 0000:0008    16 0000:0017    18 0000:00C4    28 0000:00D2
}
                  Read(map,line);
                  Read(map,ch);
                  Read(map,adr);
                  IF (Errseg=Hex2Word(Copy(adr,1,4)))
                    THEN
                      BEGIN
                        lastfn:=fn;
                        IF (Errofs>=Hex2Word(Copy(adr,6,4)))
                          THEN lastnr:=line;
                      END;

                  If Eoln(map)
                    Then Readln(map);

                UNTIL Eoln(map);
              END;

            IF NOT(eof(map))
              THEN Readln(map,s);

          UNTIL Eof(map) OR ((lastnr<>0) OR (lastfn<>''));

        Close(map);

        Beep;

        IF (lastfn<>'') AND ((errseg<>0) OR (errofs<>0))
          THEN
          {----Report Line Number & Source File}
            BEGIN
              WHILE (length(lastfn)<12) DO Insert(#32,lastfn,1);
              If first
                THEN
                  Csr_reporter(dep,'Runtime error '+Istr(exitcode,3,'0')+
                                                  ' in line '+Wstr(lastnr,4)+
                                                  ' of '+lastfn+
                                                  ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.')
                ELSE
                  Csr_reporter(dep,'    Called '+call+' from line '+Wstr(lastnr,4)+
                                                      ' of '+lastfn+
                                                      ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.');
            END
          ELSE
            BEGIN
              IF (lastun<>'') OR (lastpr<>'')
                THEN
                {----Report Unit/Program Name & Procedure name}
                  BEGIN
                    IF (Pos('@',lastpr)>0)
                      THEN s:=lastun+'.MAIN'
                      ELSE s:=lastun+'.'+lastpr;

                    WHILE (Length(s)>25) DO
                      Delete(s,Length(s),1);

                    If first
                      THEN
                        Csr_reporter(dep,'Runtime error '+Istr(exitcode,3,'0')+
                                                        ' in '+Sstr(s,25)+
                                                        ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.')
                      ELSE
                        Csr_reporter(dep,'    Called '+call+' from '+Sstr(s,25)+
                                                            ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.');
                  END
                ELSE
                {----Report Error Address Only}
                  BEGIN
                    If first
                      THEN
                        Csr_reporter(dep,'Runtime error '+Istr(exitcode,3,'0')+
                                                        '             '+
                                                        '                '+
                                                        ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.')
                      ELSE
                        Csr_reporter(dep,'    Called '+call+' from line     '+
                                                           '                '+
                                                           ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.');
                  END;
            END;
      END
    ELSE
    {----Report Error Addres Only}
      Csr_reporter(dep,'Runtime error '+Istr(exitcode,0,'0')+
                                      ' at '+Word2hex(errseg)+':'+Word2Hex(errofs)+'.')
END; {of Findlineno}

{---------------------------------------------------------}
{$F+}

VAR
  exitsave : POINTER;

PROCEDURE Myexit;

VAR
  ch  : Char;
  cdiv,
  csmin,
  cs,
  sp,
  ss  : WORD;
  p   : Pointer;
  dep : WORD;
  j   : INTEGER;

BEGIN
  Flushkbd;

  Exitproc:=exitsave;

  IF (exitcode=0) OR (erroraddr=NIL) THEN Exit;

  sp:=BPreg;
  ss:=SSeg;

{----Calculate calling depth}
  dep:=0;
  p:=Ptr(ss,sp);
  WHILE MemW[ss:Ofs(p^)]<>0 DO
    BEGIN
      IF (Mem[cs:MemW[ss:Ofs(p^)+2]-3]<>$E8)
        THEN cs:=MemW[ss:Ofs(p^)+4];

      p:=Ptr(ss,MemW[ss:Ofs(p^)]);
      Inc(dep);
    END;

  p:=Ptr(ss,sp);
  cdiv :=Cseg-cs;
  csmin:=cs;
  cs   :=Cseg;

{----Report Runtime address}
  Findlineno(true,true,dep,erroraddr);
  Dec(dep);

{----Calculate cseg at runtime error}
  cs:=csmin+Seg(erroraddr^);

{----Prevent Turbo Pascal from reporting}
  Erroraddr:=NIL;

  If NOT(mapfile) THEN Exit;

{----Skip Runtime error handler entry}
  IF (MemW[ss:Ofs(p^)]<>0)
    THEN p:=Ptr(ss,MemW[ss:Ofs(p^)]);

{----Report Call Stack}
  WHILE MemW[ss:Ofs(p^)]<>0 DO
    BEGIN
    {----Test for near call instruction 3 bytes before return address}
      IF (Mem[cs:MemW[ss:Ofs(p^)+2]-3]=$E8)
      {----Trace a near call}
        THEN Findlineno(false,true,dep,Ptr(WORD(Cs+Cdiv-Cseg),MemW[ss:Ofs(p^)+2]-3))
        ELSE
        {----Trace a far call}
          BEGIN
            Cs:=MemW[ss:Ofs(p^)+4];
            Findlineno(false,false,dep,Ptr(WORD(Cs+Cdiv-Cseg),MemW[ss:Ofs(p^)+2]-3));
          END;

    {----Increment stackpointer}
      p:=Ptr(ss,MemW[ss:Ofs(p^)]);
      Dec(dep);
    END;

END; {of Myexit}

{---------------------------------------------------------}

PROCEDURE Csr_report(level : Word;csr : STRING);

BEGIN
  IF ft
    THEN
      BEGIN
        Textmode(lastmode);
        ft:=false;
      END;
  Writeln(csr+' (',level,')');
END; {of Csr_report}
{$F-}
{---------------------------------------------------------}

BEGIN
  exitsave:=Exitproc;
  exitproc:=@Myexit;
  csr_reporter:=Csr_report;

  Fsplit(Paramstr(0),dir,nam,ext);
  Assign(map,dir+nam+'.MAP');
  {$I-} Reset(map); {$I+}
  IF (IOResult=0)
    THEN
      BEGIN
        mapfile:=true;
        Close(map);
      END
    ELSE mapfile:=false;

  ft:=true;
END.

{  STACK UNIT NEEDED FOR CRS_01}

UNIT Stack1;

INTERFACE

PROCEDURE test2(VAR i : Integer);

IMPLEMENTATION

VAR
  i : INTEGER;

{---------------------------------------------------------}

PROCEDURE test2(VAR i : Integer);

PROCEDURE test4(i : INTEGER);

VAR
  tmp : Integer;

BEGIN
  tmp:=0;
  i:=1 div tmp;
END;

BEGIN
  test4(i);
END;

{---------------------------------------------------------}

BEGIN
  i:=1;
END.


{ -------------------------------   DEMO ------------------------}
{---------------------------------------------------------}
PROGRAM Csrtst;

USES
  CRT,
  Csr_01,
  Stack1;

{---------------------------------------------------------}

PROCEDURE test3;

VAR
  i : INTEGER;

BEGIN
  test2(i);
END;

{---------------------------------------------------------}

PROCEDURE test4;

BEGIN
  test3
END;

{---------------------------------------------------------}

BEGIN
  clrscr;
  test4;
END.
