[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: Re-engineered: Wizardry III, Legacy of Llylgamyn
Thank you Vlad, Eric, R, michael, and Geoff for the encouragement to
continue.
Starting with this post, I will begin to explain how to re-engineer
(or reverse engineer) Pascal P-code (bytecodes) into Pascal source
code. Compared to my previous posts, these will likely not be as
organized. I've had a difficult time trying to determine how much (or
little) to write. It is difficult to determine how much my audience
knows (or should know) and what they do not know (or need to learn).
The organization of information does not easily fit into an "A, B, C"
order of learning. I guess I'm saying that you need to know almost
everything in order to understand anything.
Since this is an Apple forum, and I've only worked with Apple Pascal
(UCSD Pascal 1.1), that is the basis for my writing. I've used an
AppleII+ and an Apple IIe and performed much of my work using an Apple
emulator (AppleWin) on a Windows XP computer. If you have a different
computer or different level of Pascal, you will need to adjust
accordingly.
I assume the reader knows everything about standard Pascal:
Variable types (BOOLEAN, INTEGER, REAL, CHAR)
Statements (Assignment, WHILE, REPEAT-UNTIL, FOR, IF-THEN, IF-THEN-
ELSE, CASE, GOTO)
Boolean expressions and operators
Procedures and Functions
Global variables and local variables
Scope of variables and procedures (lexical level)
Passing parameters by reference or by value
Scalar and subrange types
Structured types
Record types (including use of the WITH statement)
Arrays and Records (including variant records)
Pointer types
Dynamic memory allocation (NEW and DISPOSE)
File types (Textfiles, INPUT, OUTPUT)
Standard procedures (GET, PUT, RESET, NEW, DISPOSE, PACK, UNPACK,
ORD, CHR, etc.)
READ, READLN, WRITE, WRITELN
Since I am discussing Apple Pascal, I will also assume that you know
the differences between it and standard Pascal:
STRING variable type
INTERACTIVE file type
PACKED variable differences
LONG INTEGER
MARK and RELEASE (replacement for DISPOSE)
SEGMENT procedures and functions
UNITS
EXTERNAL procedures and functions (assembly language - 6502 code)
Built-in functions (LENGTH, POS, CONCAT, COPY, DELETE, INSERT, STR)
Input and Output functions (REWRITE, RESET, CLOSE, UNITREAD,
BLOCKREAD, etc.)
Miscellaneous functions (e.g., EXIT)
Byte-Oriented functions (SIZEOF, SCAN, MOVELEFT, FILLCHAR)
Pascal compiler options
(*$I+*)
(*$I-*)
(*$I filename *)
(*$L+*)
(*$R+*)
(*$S++*)
(*$V-*)
Libraries and UNITs (Regular and Intrinsic)
Chaining programs (SETCHAIN, SETCVAL, GETCVAL, SWAPON, SWAPOFF)
SYSTEM.SWAPDISK
I found "Apple Pascal, Operating System Reference Manual" to be
extremely valuable in understanding not just the Pascal OS, but the P-
Machine:
Appendix A: "Architecture of the P-Machine"
Appendix B: "Operation of the P-Machine"
Appendix C: "File Formats" (including CODE files).
The reference manual is very technically written, and in my opinion
not very well, but it does contain almost 100% of the information
needed to decompile an Apple Pascal program. The appendices clearly
formed the basis for a Pascal P-Code decompiler (disassembler?) that I
wrote using Applesoft Basic. Its current name is WIZ4 that you might
read about later.
Note: "Decompiler" seems like the wrong term, since the output is not
Pascal code!?
If I had dozens of programs the size of Wizardry III that I wanted to
re-engineer, I would consider writing a real Pascal decompiler -- one
that would generate Pascal code directly from the P-code.
A Pascal P-code decompiler is the centerpiece in the decompiling
process.
I will make an assumption here that you understand the difference
between a compiler that generates native machine code (6502 code) and
a Pascal compiler that generates P-code.
Now, how well do you need to know the information in Appendices A, B,
C regarding the P-machine? If I do my job right in these posts, you
might not need to know more than the general concepts. Much of the
work for decompiling uses the text output from the decompiler. You
never need to know exactly what an individual P-code (bytecode) value
does as the decompiler shows it at a higher conceptual level.
The Pascal system on Wizardry III is the 48K runtime version (there
are others) that software developers distribute with their product.
A Pascal program can have:
Multiple files starting with STARTUP.CODE (turnkey system)
Chaining to other ".CODE" files.
Each file with at least one segment (Wizardry.CODE and WizUtil.CODE
contain many).
A segment containing other segments.
Each segment containing one or more procedures (or functions).
Each procedure with at least one Pascal statement.
Each Pascal statement consisting of 1 or more bytes.
Regarding the Apple computer, what do you need to know? Lots(?)
Some of my discussions will likely contain references to the AppleII
"monitor" program (hint: it has nothing to do with a computer screen
or terminal). You should know what "CALL -151", "800G", and "800L"
do. You should be able to read and understand assembly language
code. You should know the general way that the 6502 CPU operates. In
general you should know what a memory map displays.
Apple memory map:
Zero page
Stack page
$3F0-$3F4 BRK and Powerup bytes
Low-res memory map screen
High-res graphics page 1 memory
High-res graphics page 2 memory
Addresses from $C000-$C070 for hardware
PROM space for slot devices ($C100-$C7FF)
Monitor rom
Some of my side discussions (for example: copy protection schemes)
will likely require a lot of knowlege about internal disk formatting.
You might need to know about Track and Sector formatting and RWTS.
There are differences between Pascal "block" numbering and DOS T/S
numbering.
You need lots of TIME and lots of PATIENCE.
To whet your appetite for what is to come, I will show you an example
of decompiled P-code (from Wizardry III, file WizUtil.code, segment
UTILS, procedure 10) using my WIZ4 decompiler. The program solicits
for "Process all?", "FILE NUMBER", "SEGMENT NUMBER", "START ADDRESS",
and "PROCEDURE NUMBER". The rest of this listing is output from the
decompiler (WIZ4).
]PR#1
]REM LEGACY OF LLYLGAMYN BOOT DISK IN S6 D2
]RUN WIZ4
PROCESS ALL?N
FILES: 9
1 WIZARDRY.CODE
2 SYSTEM.CHARSET
3 SYSTEM.STARTUP
4 PICTURE.BITS
5 WIZUTIL.CODE
6 SYSTEM.LIBRARY
7 RTSTRP.APPLE
8 SYSTEM.PASCAL
9 SYSTEM.MISCINFO
TYPE A FILE NUMBER (OR -1) 5
SEGMENT T/S = 24/0 --- PASCAL T/S
SEGMENT T/S = 24/0 --- DOS 3.3 T/S
0 WIZBOOT
1 UTILS
2 MAKESCEN
3 OPTIONS
TYPE A SEGMENT NUMBER (OR -1) 1
T/S FOR SEGMENT 24/2 --- PASCAL
T/S FOR SEGMENT 24/13 --- DOS
SEGMENT'S START ADDR($5000):
THERE ARE 26 PROCEDURES.
ENTER PROCEDURE # (OR -1) 10
JTAB FOR SUBROUTINE 10:
DATA SIZE: 8
PARAM SIZE: 2
EXIT AT: 538C
ENTER AT: 5340
PROC NUMBER: 10
LEXICAL LEVEL: 2
5340 00 SLDC. PUSH #0000
5341 CC 04 STL. MP.04 := (TOS)
5343 AE 04 CIP. CALL INTERMEDIATE PROCEDURE: 04
5345 EA SLDO. PUSH BASE.03
5346 0D SLDC. PUSH #000D
5347 CB NEQI. PUSH ((TOS-1) <> (TOS))
5348 A1 24 UJP. IF NOT (TOS) THEN JUMP TO 536E
534A B6 03 03 LOD. PUSH ACTREC(-03).03
534D 08 SLDC. PUSH #0008
534E 00 SLDC. PUSH #0000
534F CD 00 11 CXP. CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00
5352 00 SLDC. PUSH #0000
5353 CC 03 STL. MP.03 := (TOS)
5355 EC SLDO. PUSH BASE.05
5356 02 SLDC. PUSH #0002
5357 8E MODI. PUSH ((TOS-1) MOD (TOS))
5358 CC 05 STL. MP.05 := (TOS)
535A DA SLDL. PUSH MP.03
535B DC SLDL. PUSH MP.05
535C C8 LEQI. PUSH ((TOS-1) <= (TOS))
535D A1 0F UJP. IF NOT (TOS) THEN JUMP TO 536E
535F B6 03 03 LOD. PUSH ACTREC(-03).03
5362 58 SLDC. PUSH #0058
5363 00 SLDC. PUSH #0000
5364 CD 00 11 CXP. CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00
5367 DA SLDL. PUSH MP.03
5368 01 SLDC. PUSH #0001
5369 82 ADI. PUSH ((TOS) + (TOS-1))
536A CC 03 STL. MP.03 := (TOS)
536C B9 F6 UJP. JUMP TO 535A
536E DB SLDL. PUSH MP.04
536F 01 SLDC. PUSH #0001
5370 82 ADI. PUSH ((TOS) + (TOS-1))
5371 CC 04 STL. MP.04 := (TOS)
5373 D8 SLDL. PUSH MP.01
5374 DB SLDL. PUSH MP.04
5375 EA SLDO. PUSH BASE.03
5376 BF STB. (TOS-2)^.(TOS-1) := TOS BYTE
5377 EA SLDO. PUSH BASE.03
5378 0D SLDC. PUSH #000D
5379 C3 EQUI. PUSH ((TOS-1) = (TOS))
537A DB SLDL. PUSH MP.04
537B 0F SLDC. PUSH #000F
537C C3 EQUI. PUSH ((TOS-1) = (TOS))
537D 8D LOR. PUSH ((TOS-1) OR (TOS))
537E A1 F4 UJP. IF NOT (TOS) THEN JUMP TO 5343
5380 B6 03 03 LOD. PUSH ACTREC(-03).03
5383 CD 00 16 CXP. CALL EXTERNAL PROCEDURE: 16 IN SEGMENT: 00
5386 D8 SLDL. PUSH MP.01
5387 00 SLDC. PUSH #0000
5388 DB SLDL. PUSH MP.04
5389 01 SLDC. PUSH #0001
538A 95 SBI. PUSH ((TOS-1) - (TOS))
538B BF STB. (TOS-2)^.(TOS-1) := TOS BYTE
538C AD 00 RNP. RETURN FROM NON-BASE PROCEDURE.
538E 4B SLDC. PUSH #004B
538F 00 SLDC. PUSH #0000
5390 36 SLDC. PUSH #0036
5391 00 SLDC. PUSH #0000
ENTER PROCEDURE # (OR -1) -1
============================
Description of lising (e.g., instruction at $534F):
534F CD 00 11 CXP. CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00
The first number is an Apple memory address ($534F) determined by
the segment load address.
Next are the P-code bytes (CD 00 11).
Following that is the P-code mnemonic for CD (CXP.).
Following that is the interpretation of the parameters for the CXP
instruction
(CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00).
The JTAB header says there are 2 bytes of data as parameters and 8
bytes of data for local variables in this routine. Pascal generally
works on word boundaries with 2 bytes of data representing one piece
of data such as an INTEGER. The JTAB indicates this procedure is at
lexical level 2 (UTILS is lexical level 1, so this is a direct child
to it). The RNP instruction at 538C is the exit point from this
routine, and since the parameter for this instruction (RNP.) is 00,
that means this is a procedure and not a function.
Procedures and functions are inserted into the final CODE file based
on the order in which the "END;" instruction for the procedure is
encountered while compiling. They are given a "number" based on the
number of PROCEDURE (and FUNCTION) headings encountered. (There is an
exception to this rule when FORWARD statements are used.)
The P-code machine makes heavy use of the stack to push results and
pull results. For example, to assign a value to a variable takes 2
steps:
1. PUSH <value>
2. ASSIGN (from TOS) (TOS == Top of Stack).
To add 2 values, and then assign the result to a variable takes 4
steps:
1. PUSH <value1>
2. PUSH <value2>
3. ADD <TOS> + <TOS-1> (Pull 2 values from stack, add them, and
push result)
4. ASSIGN (from TOS)
So already we can start programming:
PROGRAM WIZBOOT; (* <-- Note the name from
decompiler. *)
SEGMENT PROCEDURE UTILS; (* <-- Note "SEGMENT" and name
from decompiler. *)
PROCEDURE P050102;
BEGIN
END;
PROCEDURE P050103;
BEGIN
END;
PROCEDURE P050104;
BEGIN
END;
PROCEDURE P050105;
BEGIN
END;
PROCEDURE P050106;
BEGIN
END;
PROCEDURE P050107;
BEGIN
END;
PROCEDURE P050108;
BEGIN
END;
PROCEDURE P050109;
BEGIN
END;
PROCEDURE P05010A( MP01: INTEGER); (* <-- lexical level 2
(subroutine to UTILS) *)
VAR
MP02: INTEGER;
MP03: INTEGER;
MP04: INTEGER;
MP05: INTEGER;
BEGIN
END;
BEGIN (* UTILS *)
END;
BEGIN (* WIZBOOT *)
END.
I use a simple naming convention for unknown procedures and
functions. The letter "P", followed by the file's cardinal location
in the directory (05), followed by the segment number (01), followed
by the procedure number (I'm inconsistent as I use hexidecimal here,
but use decimal in WIZ4). Note, the values in the procedure name are
the same as the "input" to the decompiler (WIZ4).
I use MPxx to represent parameters and local variables, and BASExx to
represent global variables.
At this point I do not know if the variables are INTEGER or something
else, and I do not know if the parameter is passed by value or passed
by reference (VAR).
If you printed my p-code listing on paper, you should now start to
annotate it with your pencil (of course these days you might want to
just put it into an editor and work on it). Under each instruction
that has ":=" you should draw a line to signify that this completes an
assignment instruction. You can also draw a line under any "CALL"
instruction as that oftentimes indicates the end of a Pascal
instruction.
If you see a jump or branch instruction in a forward direction
(towards the bottom of the page), then draw an arrow from the left
side of the page from there to the address. If you see a jump or
branch to an instruction in a backward direction, draw an arrow from
the right side to the destination.
So now you listing should look like the following:
5340 00 SLDC. PUSH #0000
5341 CC 04 STL. MP.04 := (TOS)
--------------------------------------
5343 AE 04 CIP. CALL INTERMEDIATE PROCEDURE: 04 <-+
-------------------------------------- |
5345 EA SLDO. PUSH BASE.03 |
5346 0D SLDC. PUSH #000D |
5347 CB NEQI. PUSH ((TOS-1) <> (TOS)) |
5348 A1 24 +<-UJP. IF NOT (TOS) THEN JUMP TO 536E |
534A B6 03 03 | LOD. PUSH ACTREC(-03).03 |
534D 08 | SLDC. PUSH #0008 |
534E 00 | SLDC. PUSH #0000 |
534F CD 00 11 | CXP. CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00
--------------|----------------------- |
5352 00 | SLDC. PUSH #0000 |
5353 CC 03 | STL. MP.03 := (TOS) |
--------------|----------------------- |
5355 EC | SLDO. PUSH BASE.05 |
5356 02 | SLDC. PUSH #0002 |
5357 8E | MODI. PUSH ((TOS-1) MOD (TOS)) |
5358 CC 05 | STL. MP.05 := (TOS) |
--------------|----------------------- |
535A DA | SLDL. PUSH MP.03 <--+ |
535B DC | SLDL. PUSH MP.05 | |
535C C8 | LEQI. PUSH ((TOS-1) <= (TOS)) |
535D A1 0F +<-UJP. IF NOT (TOS) THEN JUMP TO 536E |
535F B6 03 03 | LOD. PUSH ACTREC(-03).03 |
5362 58 | SLDC. PUSH #0058 | |
5363 00 | SLDC. PUSH #0000 | |
5364 CD 00 11 | CXP. CALL EXTERNAL PROCEDURE: 11 IN SEGMENT: 00
--------------|----------------------- | |
5367 DA | SLDL. PUSH MP.03 | |
5368 01 | SLDC. PUSH #0001 | |
5369 82 | ADI. PUSH ((TOS) + (TOS-1)) |
536A CC 03 | STL. MP.03 := (TOS) | |
--------------|----------------------- | |
536C B9 F6 | UJP. JUMP TO 535A ->+ |
536E DB +->SLDL. PUSH MP.04 |
536F 01 SLDC. PUSH #0001 |
5370 82 ADI. PUSH ((TOS) + (TOS-1)) |
5371 CC 04 STL. MP.04 := (TOS) |
-------------------------------------- |
5373 D8 SLDL. PUSH MP.01 |
5374 DB SLDL. PUSH MP.04 |
5375 EA SLDO. PUSH BASE.03 |
5376 BF STB. (TOS-2)^.(TOS-1) := TOS BYTE |
-------------------------------------- |
5377 EA SLDO. PUSH BASE.03 |
5378 0D SLDC. PUSH #000D |
5379 C3 EQUI. PUSH ((TOS-1) = (TOS)) |
537A DB SLDL. PUSH MP.04 |
537B 0F SLDC. PUSH #000F |
537C C3 EQUI. PUSH ((TOS-1) = (TOS)) |
537D 8D LOR. PUSH ((TOS-1) OR (TOS)) |
537E A1 F4 UJP. IF NOT (TOS) THEN JUMP TO 5343 -->+
5380 B6 03 03 LOD. PUSH ACTREC(-03).03
5383 CD 00 16 CXP. CALL EXTERNAL PROCEDURE: 16 IN SEGMENT: 00
--------------------------------------
5386 D8 SLDL. PUSH MP.01
5387 00 SLDC. PUSH #0000
5388 DB SLDL. PUSH MP.04
5389 01 SLDC. PUSH #0001
538A 95 SBI. PUSH ((TOS-1) - (TOS))
538B BF STB. (TOS-2)^.(TOS-1) := TOS BYTE
538C AD 00 RNP. RETURN FROM NON-BASE PROCEDURE.------>
--------------------------------------
538E 4B SLDC. PUSH #004B
538F 00 SLDC. PUSH #0000
5390 36 SLDC. PUSH #0036
5391 00 SLDC. PUSH #0000
You might start to recognize some Pascal structure now.
This construct is an assignment statement:
PUSH #0000
MP.04 := (TOS)
This construct is a REPEAT UNTIL loop:
A <-----+
B |
C |
test -->+ (jump backward)
This construct is an IF THEN statement:
+<--test (jump forward)
| A
| B
| C
+-->D
This construct is a FOR (or WHILE) loop:
+<---test <---+ (jump forward)
| A |
| B |
| C |
| jump --->+ (unconditional jump backward)
+--->D
Whenever you see "MP.xx" it refers to parameters in the procedure
heading, or to local variables. The "MP" refers to Markstack Pointer
or simply Markstack. Whenever you see "BASE.xx" that refers to GLOBAL
variables.
Whenever you see "ACTREC(-x)" it is referring to a Pascal Activation
Record. The value in the parenthesis after ACTREC indicates the
number of levels backwards to traverse.
With "IF THEN" instructions the code usually sets up the condition and
then tests "IF NOT <condition> JUMP" which means when the condition is
TRUE it falls into the code following the test as we would expect.
There are several calls to EXTERNAL procedures and the ACTREC(-3)
means we are going back several activation records until we are at the
-1 level (+2 - 3). The ".3" refers to the standard Pascal OUTPUT
file. There is always a parameter of 0 pushed before an external
procedure call. This means the code is executing a standard Pascal
procedure. Here is a table I created to identify the external
procedures:
READ() Procedure: 10
EOLN() Procedure: 0B
WRITE() Procedure: 11 WRITE( CHR( 13))
WRITE() Procedure: 13 WRITE( Astr : 20);
WRITELN() Procedure: 13 and 16
WRITELN; Procedure: 16
WRITE() Procedure: 0D WRITE( Anum);
GOTOXY() Procedure: 1D
COPY() Procedure: 19
CONCAT() Procedure: 17
Ok, get out your coding pencil again! It's time to code!
If I don't know how to convert something right away, I'll just leave
the original P-code in the file for now.
PROGRAM WIZBOOT;
VAR
(* First 2 global variables are implicitly defined. INPUT/
OUTPUT (?) *)
BASE03 : INTEGER; (* see P05010A *)
BASE04 : INTEGER;
BASE05 : INTEGER; (* see P05010A *)
SEGMENT PROCEDURE UTILS;
PROCEDURE P05010A( MP01: INTEGER);
VAR
MP02: INTEGER;
MP03: INTEGER;
MP04: INTEGER;
MP05: INTEGER;
BEGIN
MP04 := 0;
REPEAT
P050104;
IF BASE03 <> 13 THEN
BEGIN
WRITE( CHR( 8));
MP03 := 0;
MP05 := BASE05 MOD 2;
WHILE MP03 <= MP05 DO
BEGIN
WRITE( CHR( 88));
MP03 := MP03 + 1
END;
END;
MP04 := MP04 + 1;
5373 D8 SLDL. PUSH MP.01 |
5374 DB SLDL. PUSH MP.04 |
5375 EA SLDO. PUSH BASE.03 |
5376 BF STB. (TOS-2)^.(TOS-1) := TOS BYTE |
UNTIL (BASE03 = 13) OR (MP04 = 15);
WRITELN;
5386 D8 SLDL. PUSH MP.01
5387 00 SLDC. PUSH #0000
5388 DB SLDL. PUSH MP.04
5389 01 SLDC. PUSH #0001
538A 95 SBI. PUSH ((TOS-1) - (TOS))
538B BF STB. (TOS-2)^.(TOS-1) := TOS BYTE
END;
Ok, time to clean up MP01, the parameter to P05010A. The STB
instruction at $5376 indicates we are using MP01 as a pointer that is
indexed by MP04 (MP.04) and storing BASE03. The instruction at $538B
is likewise using MP01 as a pointer.
By looking at the calling procedure we can determine how it uses the
parameter that is passed to P05010A. The variable in the calling
procedure is also called MP.01. Here is some code from the calling
routine:
LLA. PUSH #MP.01
LLA. PUSH #MP.4B
NEQ. PUSH ((TOS-1) <> (TOS))--STRINGS
Therefore we know that the type of parameter passed to P05010A must be
STRING, and since it is altered, it is passed by reference (VAR).
There is one other problem with the code. If you look at BASE03, you
will see in one line that it seems to be used as an INTEGER, yet on
another line it is being used as a BYTE. The proper place to make the
correction here is the definition for BASE03...it must be type CHAR.
And the instruction that is comparing it to "13" as an integer can be
fixed by using the "CHR" function as "CHR( 13)".
So this version of the code now looks like:
PROGRAM WIZBOOT;
VAR
(* First 2 global variables are implicitly defined. INPUT/
OUTPUT (?) *)
BASE03 : CHAR; (* see P05010A *)
BASE04 : INTEGER;
BASE05 : INTEGER; (* see P05010A *)
PROCEDURE P05010A( VAR MP01: STRING);
VAR
MP02: INTEGER;
MP03: INTEGER;
MP04: INTEGER;
MP05: INTEGER;
BEGIN
MP04 := 0;
REPEAT
P050104;
IF BASE03 <> CHR( 13) THEN
BEGIN
WRITE( CHR( 8));
MP03 := 0;
MP05 := BASE05 MOD 2;
WHILE MP03 <= MP05 DO
BEGIN
WRITE( CHR( 88));
MP03 := MP03 + 1
END;
END;
MP04 := MP04 + 1;
MP01[ MP04] := BASE03
UNTIL (BASE03 = CHR( 13)) OR (MP04 = 15);
WRITELN;
MP01[ 0] := MP04 - 1
END;
==============================
There is something I need to mention about this code. There are 2
ways to write the "WHILE" loop. It can also be written as a FOR loop:
1. Remove MP05 definition from the VAR declaration,
2. Rewrite the loop:
FOR MP03 := 0 to BASE05 MOD 2 DO
WRITE( CHR( 88));
The compiler will actually generate a temporary variable that was
previously defined as MP05. The compiler will then generate EXACTLY
the same P-code. Note that I did not say it would produce code that
when executed would produce identical results, the P-code itself is
identical. You might think this is an insignificant topic, but not
knowing this distinction caused me to spend countless HOURS trying to
solve a problem in the code.
Note that this is just one case where writing 2 different looking
sequence of Pascal statements will generate exactly the same P-code.
It really doesn't matter (usually) which way you write it in your re-
engineering because the resultant code will execute exactly as the
original code.
If you want to, you can also change CHR( 88) to 'X' (with the single
quotes).
Ok, so now you compile this code and find that it still does not match
the generated code (even if I haven't included any typos in this
description). You may find that you need to disable the "IO CHECK" (*
$I-*) and turn off "RANGE CHECK" (*$R-*).
I think this was a very good example to start with, as it showed
parameter passing, local variables, global variables, a compiler
generated local variable, a call to a local procedure, calls to
external standard procedures, assignment statements, repeat loop, if-
then, and a while (or for) loop.
Enjoy!
--Tommy