596 lines
29 KiB
Plaintext
596 lines
29 KiB
Plaintext
|
.\" Implementation details
|
||
|
.\"
|
||
|
.\" $Header$
|
||
|
.bp
|
||
|
.NH
|
||
|
IMPLEMENTATION DETAILS.
|
||
|
.PP
|
||
|
The pertinent issues are addressed below, in arbitrary order.
|
||
|
.NH 2
|
||
|
Stack manipulation and start-up
|
||
|
.PP
|
||
|
It is not at all easy to start the EM machine with the stack in a reasonable
|
||
|
and consistent state. One reason is the anomalous value of the ML register
|
||
|
and another is the absence of a proper RSB. It may be argued that the initial
|
||
|
stack does not have to be in a consistent state, since the first instruction
|
||
|
proper is only executed after \fIargc\fP, \fIargv\fP and \fIenviron\fP
|
||
|
have been stacked (which takes care of the empty stack) and the initial
|
||
|
procedure has been called (which creates a RSB). We would, however, like to
|
||
|
preform the stacking of these values and the calling of the initial procedure
|
||
|
using the normal stack and call routines, which again require the stack to be
|
||
|
in an acceptable state.
|
||
|
.NH 3
|
||
|
The anomalous value of the ML register
|
||
|
.PP
|
||
|
All registers in the EM machine point to word boundaries, and all of them,
|
||
|
except ML, address the even-numbered byte at the boundary.
|
||
|
The exception has a good reason: the even numbered byte at the ML boundary does
|
||
|
not exist.
|
||
|
This problem is not particular to EM but is inherent in the number system: the
|
||
|
number of N-digit numbers can itself not be expressed in an N-digit number, and
|
||
|
the number of addresses in an N-bit machine will itself not fit in an N-bit
|
||
|
address. The problem is solved in the interpreter by having ML point to the
|
||
|
highest word boundary that has bytes on either side; this makes ML+1
|
||
|
expressible.
|
||
|
.NH 3
|
||
|
The absence of an initial Return Status Block
|
||
|
.PP
|
||
|
When the stack is empty, there is no legal value for AB, since there are no
|
||
|
actuals; LB can be set naturally to ML+1. This is all right when the
|
||
|
interpreter starts with a call of the initial routine which stores the value
|
||
|
of LB in the first RSB, but causes problems when finally this call returns. We
|
||
|
want this call to return completely before stopping the interpreter, to check
|
||
|
the integrity of the last RSB; restoring information from it will, however,
|
||
|
cause illegal values to be stored in LB and AB (ML+1 and ML+1+rsbsize, resp.).
|
||
|
On top of this, the initial (illegal) Procedure Identifier of the running
|
||
|
procedure will be restored; then, upon restoring the likewise illegal PC will
|
||
|
cause a check to see if it still is inside the running procedure. After a few
|
||
|
attempts at writing special cases, we have decided that it is possible, but not
|
||
|
worth the effort; the final (= initial) RSB will not be unstacked.
|
||
|
.NH 2
|
||
|
Floating point numbers.
|
||
|
.PP
|
||
|
The interpreter is capable of working with 4- and 8-byte floating point (FP)
|
||
|
numbers.
|
||
|
In C-terms, this corresponds to objects of type float and double respectively.
|
||
|
Both types fit in a C-double so the obvious way to manipulate these entities
|
||
|
internally is in doubles.
|
||
|
Pushing a 8-byte FP, all bytes of the C-double are pushed.
|
||
|
Pushing a 4-byte FP causes the 4 bytes representing the smallest fraction
|
||
|
to be discarded.
|
||
|
.PP
|
||
|
In EM, floats can be obtained in two different ways: via conversion
|
||
|
of another type, or via initialization in the loadfile.
|
||
|
Initialized floats are represented in the loadfile by an ASCII string in
|
||
|
the syntax of a Pascal real (signed \fPUnsignedReal\fP).
|
||
|
I.e. a float looks like:
|
||
|
.DS
|
||
|
[ \fISign\fP ] \fIDigit\fP+ [ . \fIDigit\fP+ ] [ \fIExp\fP [ \fISign\fP ] \fIDigit\fP+ ] (G1)
|
||
|
.DE
|
||
|
followed by a null byte.
|
||
|
Here \fISign\fP = {+, \-}; \fIDigit\fP = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||
|
\fIExp\fP = {e, E}; [ \fIAnything\fP ] means that \fIAnything\fP is optional;
|
||
|
and a + means one or more times.
|
||
|
To accommodate some loose code generators, the actual grammar accepted is:
|
||
|
.DS
|
||
|
[ \fISign\fP ] \fIDigit\fP\(** [ . \fIDigit\fP\(** ] [ \fIExp\fP [ \fISign\fP ] \fIDigit\fP+ ] (G2)
|
||
|
.DE
|
||
|
followed by a null byte. Here \(** means zero or more times. A floating
|
||
|
denotation which is in G2 but not in G1 draws a warning, one that is not even
|
||
|
in G2 causes a fatal error.
|
||
|
.LP
|
||
|
A string, representing a float which does not fit in a double causes a
|
||
|
warning to be given.
|
||
|
In that case, the returned value will be the double 0.0.
|
||
|
.LP
|
||
|
Floating point arithmetic is handled by some simple routines, checking for
|
||
|
over/underflow, and returning appropriate values in case of an ignored error.
|
||
|
.PP
|
||
|
Since not all C compilers provide floating point operations, there is a
|
||
|
compile time flag NOFLOAT, which, if defined, suppresses the use of all
|
||
|
fp operations in the interpreter. The resulting interpreter will still load
|
||
|
EM files with floats in the global data area (and ignore them) but will give a
|
||
|
fatal error upon attempt to execute a floating point instruction; consequently
|
||
|
code involving floating point operations can be run as long as the actual
|
||
|
instructions are avoided.
|
||
|
.NH 2
|
||
|
Pointers.
|
||
|
.PP
|
||
|
The following sub-sections both deal with problems concerning pointers.
|
||
|
First, something is said about pointer arithmetic in general.
|
||
|
Then, the null-pointer problem is dealt with.
|
||
|
.NH 3
|
||
|
Pointer arithmetic.
|
||
|
.PP
|
||
|
Strictly speaking, pointer arithmetic is defined only within a \fBfragment\fP.
|
||
|
From the explanation of the term fragment however (as given in [1], page 3),
|
||
|
it is not quite clear what a fragment should look like
|
||
|
from an interpreter's point of view.
|
||
|
For this reason we introduced the term \fBsegment\fP,
|
||
|
bordering the various areas within which pointer arithmetic is allowed.
|
||
|
Every stack-frame is a segment, and so are the global data area (GDA) and
|
||
|
the heap area.
|
||
|
Thus, the number of segments varies over time, and at some point in time is
|
||
|
given by the number of currently active stack-frames
|
||
|
(#CAL + #CAI \- #RET \- #RTT) plus 2 (gda, heap).
|
||
|
Pointers in the area between heap and stack (which is inaccessible by
|
||
|
definition), are assumed to be in the heap segment.
|
||
|
.PP
|
||
|
The interpreter, while building a new stack-frame (i.e. segment), stores the
|
||
|
value of the last ActualBase in a pointer-array (\fIAB_list[\ ]\fP).
|
||
|
When a pointer (say \fIP\fP) is available for arithmetic, the number
|
||
|
of the segment where it points (say \fIS\d\s-2P\s+2\u\fP),
|
||
|
is determined first.
|
||
|
Next, the arithmetic is performed, followed by a check on the number
|
||
|
of the segment where the resulting pointer \fIR\fP points
|
||
|
(say \fIS\d\s-2R\s+2\u\fP).
|
||
|
Now, if \fIS\d\s-2P\s+2\u != S\d\s-2R\s+2\u\fP, a warning is given:
|
||
|
\fBPointer arithmetic yields pointer to bad segment\fP.
|
||
|
.br
|
||
|
It may also be clear now, why the illegal area between heap and stack
|
||
|
was joined with the heap segment.
|
||
|
When calculating a new heap pointer (\fIHP\fP), one will obtain intermediate
|
||
|
results being pointers in this area just before it is made legal.
|
||
|
We do not want error messages all of the time, just because someone is
|
||
|
allocating space in the heap.
|
||
|
.LP
|
||
|
A similar treatment is given to the pointers in the SBS instruction; they have
|
||
|
to point into the same fragment for subtraction to be meaningful.
|
||
|
.LP
|
||
|
The length of the \fIAB_list[\ ]\fP is initially 100,
|
||
|
and it is reallocated in the same way the dynamically growing partitions
|
||
|
are (see 1.1).
|
||
|
.NH 3
|
||
|
Null pointer.
|
||
|
.PP
|
||
|
Because the EM language lacks an instruction for loading a null pointer,
|
||
|
most programs solve this problem by loading a pointer-sized integer of
|
||
|
value zero, and using this as a null pointer (this is also proposed in [1]).
|
||
|
\fBInt\fP allows this, and will not complain.
|
||
|
A warning is given however, when an attempt is made to add something to a
|
||
|
null pointer (i.e. the pointer-sized integer zero).
|
||
|
.LP
|
||
|
Since many programming languages use a pointer to location 0 as an illegal
|
||
|
value, it is desirable to detect its use.
|
||
|
The big problem is though that 0 is a perfectly legal EM address;
|
||
|
address 0 holds the current line number in the source file. It may be freely
|
||
|
read but is written only by means of the LIN instruction. This allows us to
|
||
|
declare the area consisting of the line number and the file name pointer to be
|
||
|
read-only memory. Thus a store will be caught (and result in a warning) but a
|
||
|
read will succeed (and yield the EM information stored there).
|
||
|
.NH 2
|
||
|
Function Return Area (FRA).
|
||
|
.PP
|
||
|
The Function Return Area (\fIFRA[\ ]\fP) has a default size of 8 bytes;
|
||
|
this default can
|
||
|
be overridden through the use of the \fB\-r\fP-option, but cannot be
|
||
|
made smaller than the size of two pointers, in accordance with the
|
||
|
remark on page 5 of [1].
|
||
|
The global variable \fIFRASize\fP keeps track of how many bytes were
|
||
|
stored in the FRA, the last time a RET instruction was executed.
|
||
|
The LFR instruction only works when its argument is equal to this size.
|
||
|
If not, the FRA contents are loaded anyhow, but one of the following warnings
|
||
|
is given:
|
||
|
\fBReturned function result too large\fP (\fIFRASize\fP > LFR size) or
|
||
|
\fBReturned function result too small\fP (\fIFRASize\fP < LFR size).
|
||
|
.LP
|
||
|
Note that a C-program, falling through the end of its code without doing
|
||
|
a proper \fIreturn\fP or \fIexit()\fP, will generate this warning.
|
||
|
.PP
|
||
|
The only instructions that do not disturb the contents of the FRA are
|
||
|
GTO, BRA, ASP and RET.
|
||
|
This is expressed in the program by setting \fIFRA_def\fP to "undefined"
|
||
|
in any instruction except these four.
|
||
|
We realize this is a useless action most of the time, but a more
|
||
|
efficient solution does not seem to be at hand.
|
||
|
If a result is loaded when \fIFRA_def\fP is "undefined", the warning:
|
||
|
\fBReturned function result may be garbled\fP is generated.
|
||
|
.LP
|
||
|
Note that the FRA needs a shadow-FRA in order to store the shadow
|
||
|
information when performing a LFR instruction.
|
||
|
.NH 2
|
||
|
Environment interaction.
|
||
|
.PP
|
||
|
The EM machine represented by \fBint\fP can communicate with
|
||
|
the environment in three different ways.
|
||
|
A first possibility is by means of (UNIX) interrupts;
|
||
|
the second by executing (relatively) high level system calls (called
|
||
|
monitor calls).
|
||
|
A third means of interaction, especially interesting for the debugging
|
||
|
programmer, is via internal variables set on the command line.
|
||
|
The former two techniques, and the way they are implemented will be described
|
||
|
in this section.
|
||
|
The latter has been allotted a separate section (3).
|
||
|
.NH 3
|
||
|
Traps and interrupts.
|
||
|
.PP
|
||
|
Simple user programs will generally not mess around with UNIX-signals.
|
||
|
In interpreting these programs, the default actions will be taken
|
||
|
when a signal is received by the program: it gives a message and
|
||
|
stops running.
|
||
|
.LP
|
||
|
There are programs however, which try to handle certain signals
|
||
|
themselves.
|
||
|
In C, this is achieved by the system call \fIsignal(\ sig_no,\ catch\ )\fP,
|
||
|
which calls the handling routine \fIcatch()\fP, as soon as signal
|
||
|
\fBsig_no\fP occurs.
|
||
|
EM does not provide this call; instead, the \fIsigtrp()\fP monitor call
|
||
|
is available for mapping UNIX signals onto EM traps.
|
||
|
This implies that a \fIsignal()\fP call in a C-program
|
||
|
must be translated by the EM library routine to a \fIsigtrp()\fP call in EM.
|
||
|
.PP
|
||
|
The interpreter keeps an administration of the mapping of UNIX-signals
|
||
|
onto EM traps in the array \fIsig_map[NSIG]\fP.
|
||
|
Initially, the signals all have their default values.
|
||
|
Now assume a \fIsigtrp()\fP occurs, telling to map signal \fBsig_no\fP onto
|
||
|
trap \fBtrap_no\fP.
|
||
|
This results in:
|
||
|
.IP 1.
|
||
|
setting the relevant array element
|
||
|
\fIsig_map[sig_no]\fP to \fBtrap_no\fP (after saving the old value),
|
||
|
.IP 2.
|
||
|
catching the next to come \fBsig_no\fP signal with the handling routine
|
||
|
\fIHndlEMSig\fP (by a plain UNIX \fIsignal()\fP of course), and
|
||
|
.IP 3.
|
||
|
returning the saved map-value on the stack so the user can know the previous
|
||
|
trap value onto which \fBsig_no\fP was mapped.
|
||
|
.LP
|
||
|
On an incoming signal,
|
||
|
the handling routine for signal \fBsig_no\fP arms the
|
||
|
correct EM trap by calling the routine \fIarm_trap()\fP with argument
|
||
|
\fIsig_map[sig_no]\fP.
|
||
|
At the end of the EM instruction the proper call of \fItrap()\fP is done.
|
||
|
\fITrap()\fP on its turn examines the value of the \fIHaltOnTrap\fP variable;
|
||
|
if it is set, the interpreter will stop with a message. In the normal case of
|
||
|
controlled trap handling this bit is not on and the interpreter examines
|
||
|
the value of the \fITrapPI\fP variable,
|
||
|
which contains the procedure identifier of the EM trap handling routine.
|
||
|
It then initiates a call to this routine and performs a \fIlongjmp()\fP
|
||
|
to the main
|
||
|
loop to bypass all further processing of the instruction that caused the trap.
|
||
|
\fITrapPI\fP should be set properly by the library routines, through the
|
||
|
SIG instruction.
|
||
|
.LP
|
||
|
In short:
|
||
|
.IP 1.
|
||
|
A UNIX interrupt is caught by the interpreter.
|
||
|
.IP 2.
|
||
|
A handling routine is called which generates the corresponding EM trap
|
||
|
(according to the mapping).
|
||
|
.IP 3.
|
||
|
The trap handler calls the corresponding EM routine which emulates a UNIX
|
||
|
interrupt for the benefit of the interpreted program.
|
||
|
.PP
|
||
|
When considering UNIX signals, it is important to notice that some of them
|
||
|
are real signals, i.e., messages coming from outside the program, like DEL
|
||
|
and QUIT, but some are actually program-caused synchronous traps, like Illegal
|
||
|
Instruction. The latter, if they happen, are incurred by the interpreter
|
||
|
itself and consequently are of no concern to the interpreted program: it
|
||
|
cannot catch them. The present code assumes that the UNIX signals between
|
||
|
SIGILL (4) and SIGSYS (12) are really traps; \fIdo_sigtrp()\fP
|
||
|
will fail on them.
|
||
|
.LP
|
||
|
To avoid losing the last line(s) of output files, the interpreter should
|
||
|
always do a proper close-down, even in the presence of signals. To this end,
|
||
|
all non-ignored genuine signals are initially caught by the interpreter,
|
||
|
through the routine \fIHndlIntSig\fP, which gives a message and preforms a
|
||
|
proper close-down.
|
||
|
Synchronous trap can only be caused by the interpreter itself; they are never
|
||
|
caught, and consequently the UNIX default action prevails. Generally they
|
||
|
cause a core dump.
|
||
|
Signals requested by the interpreted program are caught by the routine
|
||
|
\fIHndlEMSig\fP, as explained above.
|
||
|
.NH 3
|
||
|
Monitor calls.
|
||
|
.PP
|
||
|
For the convenience of the programmer, as many monitor calls as possible
|
||
|
have been implemented.
|
||
|
The list of monitor calls given in [1] pages 20/21, has been implemented
|
||
|
completely, except for \fIptrace()\fP, \fIprofil()\fP and \fImpxcall()\fP.
|
||
|
The semantics of \fIptrace()\fP and \fIprofil()\fP from an interpreted program
|
||
|
is unclear; the data structure passed to \fImpxcall()\fP is non-trivial
|
||
|
and the system call has low portability and applicability.
|
||
|
For these calls, on invocation a warning is generated, and the arguments which
|
||
|
were meant for the call are popped properly, so the program can continue
|
||
|
without the stack being messed up.
|
||
|
The errorcode 5 (IOERROR) is pushed onto the stack (twice), in order to
|
||
|
fake an unsuccessful monitor call.
|
||
|
No other \- more meaningful \- errorcode is available in the errno-list.
|
||
|
.LP
|
||
|
Now for the implemented monitor calls.
|
||
|
The returned value is zero for a successful call.
|
||
|
When something goes wrong, the value of the external \fIerrno\fP variable
|
||
|
is pushed, thus enabling the user to find out what the reason of failure was.
|
||
|
The implementation of the majority of the monitor calls is straightforward.
|
||
|
Those working with a special format buffer, (e.g. \fIioctl()\fP,
|
||
|
\fItime()\fP and \fIstat()\fP variants), need some extra attention.
|
||
|
This is due to the fact that working with varying word/pointer size
|
||
|
combinations may cause alignment problems.
|
||
|
.LP
|
||
|
The data structure returned by the UNIX system call results from
|
||
|
C code that has been translated with the regular C compiler, which,
|
||
|
on the VAX, happens to be a 4-4 compiler.
|
||
|
The data structure expected by the interpreted program conforms
|
||
|
to the translation by \fBack\fP of the pertinent include file.
|
||
|
Depending on the exact call of \fBack\fP, sizes and alignment may differ.
|
||
|
.LP
|
||
|
An example is in order. The EM MON 18 instruction in the interpreted program
|
||
|
leads to a UNIX \fIstat()\fP system call by the interpreter.
|
||
|
This call fills the given struct with stat information, the contents
|
||
|
and alignments of which are determined by the version of UNIX and the
|
||
|
used C compiler, resp.
|
||
|
The interpreter, like any program wishing to do system calls that fill
|
||
|
structs, has to be translated by a C compiler that uses the
|
||
|
appropriate struct definition and alignments, so that it can use, e.g.,
|
||
|
\fIstab.st_mtime\fP and expect to obtain the right field.
|
||
|
This struct cannot be copied directly to the EM memory to fulfill the
|
||
|
MON instruction.
|
||
|
First, the struct may contain extraneous, system-dependent fields,
|
||
|
pertaining, e.g., to symbolic links, sockets, etc.
|
||
|
Second, it may contain holes, due to alignment requirements.
|
||
|
The EM program runs on an EM machine, knows nothing about these
|
||
|
requirements and expects UNIX Version 7 fields, with offsets as
|
||
|
determined by the em22, em24 or em44 compiler, resp.
|
||
|
To do the conversion, the interpreter has a built-in table of the
|
||
|
offsets of all the fields in the structs that are filled by the MON
|
||
|
instruction.
|
||
|
The appropriate fields from the result of the UNIX \fIstat()\fP are copied
|
||
|
one by one to the appropriate positions in the EM memory to be filled
|
||
|
by MON 18.
|
||
|
.PP
|
||
|
The \fIioctl()\fP call (MON 54) poses additional problems. Not only does it
|
||
|
have a second argument which is a pointer to a struct, the type of
|
||
|
which is dynamically determined, but its first argument is an opcode
|
||
|
that varies considerably between the versions of UNIX.
|
||
|
To solve the first problem, the interpreter examines the opcode (request) and
|
||
|
treats the second argument accordingly. The second problem can be solved by
|
||
|
translating the UNIX Version 7 \fIioctl()\fP request codes to their proper
|
||
|
values on the various systems. This is, however, not always useful, since
|
||
|
some EM run-time systems use the local request codes. There is a compile-time
|
||
|
flag, V7IOCTL, which, if defined, will restrict the \fIioctl()\fP call to the
|
||
|
version 7 request codes and emulate them on the local system; otherwise the
|
||
|
request codes of the local system will be used (as far as implemented).
|
||
|
.PP
|
||
|
Minor problems also showed up with the implementation of \fIexecve()\fP
|
||
|
and \fIfork()\fP.
|
||
|
\fIExecve()\fP expects three pointers on the stack.
|
||
|
The first points to the name of the program to be executed,
|
||
|
the second and third are the beginnings of the \fBargv\fP and \fBenvp\fP
|
||
|
pointer arrays respectively.
|
||
|
We cannot pass these pointers to the system call however, because
|
||
|
the EM addresses to which they point do not correspond with UNIX
|
||
|
addresses.
|
||
|
Moreover, (it is not very likely to happen but) what if someone constructs
|
||
|
a program holding the contents for one of these pointers in the stack?
|
||
|
The stack is implemented upside down, so passing the pointer to
|
||
|
\fIexecve()\fP causes trouble for this reason too.
|
||
|
The only solution was to copy the pointer contents completely
|
||
|
to fresh UNIX memory, constructing vectors which can be passed to the
|
||
|
system call.
|
||
|
Any impending memory fault while making these copies results in failure of the
|
||
|
system call, with \fIerrno\fP set to EFAULT.
|
||
|
.PP
|
||
|
The implementation of the \fIfork()\fP call faced us with problems
|
||
|
concerning IO-channels.
|
||
|
Checking messages (as well as logging) must be divided over different files.
|
||
|
Otherwise, these messages will coincide.
|
||
|
This problem was solved by post-fixing the default message file
|
||
|
\fBint.mess\fP (as well as the logging file \fBint.log\fP) with an
|
||
|
automatically leveled number for every new forked process.
|
||
|
Children of the original process do their diagnostics
|
||
|
in files with postfix 1,2,3 etc.
|
||
|
Second generation processes are assigned files numbered 11, 12, 21 etc.
|
||
|
When 6 generations of processes exist at one moment, the seventh will
|
||
|
get the same message file as the sixth, for the length of the filename
|
||
|
will become too long.
|
||
|
.PP
|
||
|
Some of the monitor calls receive pointers (addresses) from to program, to be
|
||
|
passed to the kernel; examples are the struct stat for \fIstat()\fP, the area
|
||
|
to be filled for \fIread()\fP, etc. If the address is wrong, the kernel does
|
||
|
not generate a trap, but rather the system call returns with failure, while
|
||
|
\fIerrno\fP is set to EFAULT. This is implemented by consistent checking of
|
||
|
all pointers in the MON instruction.
|
||
|
.NH 2
|
||
|
Internal arithmetic.
|
||
|
.PP
|
||
|
Doing arithmetic on signed integers, the smallest negative integer
|
||
|
(\fIminsint\fP) is considered a legal value.
|
||
|
This is in contradiction with the EM Manual [1], page 14, which proposes using
|
||
|
\fIminsint\fP for uninitialized integers.
|
||
|
The shadow bytes already check for uninitialized integers however,
|
||
|
so we do not need this special illegal value.
|
||
|
Although the EM Manual provides two traps, for undefined integers and floats,
|
||
|
undefined objects occur so frequently (e.g. in block copying partially
|
||
|
initialized areas) that the interpreter just gives a warning.
|
||
|
.LP
|
||
|
Except for arithmetic on unsigneds, all arithmetic checks for overflow.
|
||
|
The value that is pushed on the stack after an overflow occurs depends
|
||
|
on the UNIX behavior with regard to that particular calculation.
|
||
|
If UNIX would not accept the calculation (e.g. division by zero), a zero
|
||
|
is pushed as a convention.
|
||
|
Illegal computations which UNIX does accept in silence (e.g. one's
|
||
|
complement of \fIminsint\fP), simply push the UNIX-result after giving a
|
||
|
trap message.
|
||
|
.NH 2
|
||
|
Shadow bytes implementation.
|
||
|
.PP
|
||
|
A great deal of run-time checking is performed by the interpreter (except if
|
||
|
used in the fast version).
|
||
|
This section gives all details about the shadow bytes.
|
||
|
In order to keep track of information about the contents of D-space (stack
|
||
|
and global data area), there is one shadow-byte for each byte in these spaces.
|
||
|
Each bit in a shadow-byte represents some piece
|
||
|
of information about the contents of its corresponding 'sun-byte'.
|
||
|
All bits off indicates an undefined sun-byte.
|
||
|
One or more bits on always guarantees a well-defined sun-byte.
|
||
|
The bits have the following meaning:
|
||
|
.IP "\(bu bit 0:" 8
|
||
|
indicates that the sun-byte is (a part of) an integer.
|
||
|
.IP "\(bu bit 1:" 8
|
||
|
the sun-byte is a part of a floating point number.
|
||
|
.IP "\(bu bit 2:" 8
|
||
|
the sun-byte is a part of a pointer in dataspace.
|
||
|
.IP "\(bu bit 3:" 8
|
||
|
the sun-byte is a part of a pointer in the instruction space.
|
||
|
According to [1] (paragraph 6.4), there are two types pointers which
|
||
|
must be distinguishable.
|
||
|
Conversion between these two types is impossible.
|
||
|
The shadow-bytes make the distinction here.
|
||
|
.IP "\(bu bit 4:" 8
|
||
|
protection bit.
|
||
|
Indicates that the sun-byte is part of a protected piece of memory.
|
||
|
There is a protected area in the stack, the Return Status Block.
|
||
|
The EM machine language has no possibility to declare protected
|
||
|
memory, as is possible in EM assembly (the ROM instruction). The protection
|
||
|
bit is, however, set for the line number and filename pointer area near
|
||
|
location 0, to aid in catching references to location 0.
|
||
|
.IP "\(bu bit 5/6/7:" 8
|
||
|
free for later use.
|
||
|
.LP
|
||
|
The shadow bytes are managed by the routines declared in \fIshadow.h\fP.
|
||
|
The warnings originating from checking these shadow-bytes during
|
||
|
run-time are various.
|
||
|
A list of them is given in appendix A, together with suggestions
|
||
|
(primarily for the C-programmer) where to look for the trouble maker(s).
|
||
|
.LP
|
||
|
A point to notice is, that once a warning is generated, it may be repeated
|
||
|
thousands of times.
|
||
|
Since repetitive warnings carry little information, but consume much
|
||
|
file space, the interpreter keeps track of the number of times a given warning
|
||
|
has been produced from a given line in a given file.
|
||
|
The warning message will
|
||
|
be printed only if the corresponding counter is a power of four (starting at
|
||
|
1). In this way, a logarithmic back-off in warning generation is established.
|
||
|
.LP
|
||
|
It might be argued that the counter should be kept for each (warning, PC
|
||
|
value) pair rather than for each (warning, file position) pair. Suppose,
|
||
|
however, that two instruction in a given line would cause the same message
|
||
|
regularly; this would produce two intertwined streams of identical messages,
|
||
|
with their counters jumping up and down. This does not seem desirable.
|
||
|
.NH 2
|
||
|
Return Status Block (RSB)
|
||
|
.PP
|
||
|
According to the description in [1], at least the return address and the
|
||
|
base address of the previous RSB have to be pushed when performing a call.
|
||
|
Besides these two pointers, other information can be stored in the RSB
|
||
|
also.
|
||
|
The interpreter pushes the following items:
|
||
|
.IP \-
|
||
|
a pointer to the current filename,
|
||
|
.IP \-
|
||
|
the current line number (always four bytes),
|
||
|
.IP \-
|
||
|
the Local Base,
|
||
|
.IP \-
|
||
|
the return address (Program Counter),
|
||
|
.IP \-
|
||
|
the current procedure identifier
|
||
|
.IP \-
|
||
|
the RSB code, which distinguishes between initial start-up, normal call,
|
||
|
returnable trap and non-returnable trap (a word-size integer).
|
||
|
.LP
|
||
|
Consequently, the size of the RSB varies, depending on
|
||
|
word size and pointer size; its value is available as \fIrsbsize\fP.
|
||
|
When the RSB is removed from the stack (by a RET or RTT) the RSB code is under
|
||
|
the Stack Pointer for immediate checking. It is not clear what should be done
|
||
|
if RSB code and return instruction do not match; at present we give a message
|
||
|
and continue, for what it is worth.
|
||
|
.PP
|
||
|
The reason for pushing filename and line number is that some front-ends tend
|
||
|
to forget the LIN and FIL instructions after returning from a function.
|
||
|
This may result in error messages in wrong source files and/or line numbers.
|
||
|
.PP
|
||
|
The procedure identifier is kept and restored to check that the PC will not
|
||
|
move out of the running procedure. The PI is an index in the proctab, which
|
||
|
tells the limits in the text segment of the running procedure.
|
||
|
.PP
|
||
|
If the Return Status Block is generated as a result of a trap, more is
|
||
|
stacked. Before stacking the normal RSB, the trap function pushes the
|
||
|
following items:
|
||
|
.IP \-
|
||
|
the contents of the entire Function Return Area,
|
||
|
.IP \-
|
||
|
the number of bytes significant in the above (a word-size integer),
|
||
|
.IP \-
|
||
|
a word-size flag indicating if the contents of the FRA are valid,
|
||
|
.IP \-
|
||
|
the trap number (a word-size integer).
|
||
|
.LP
|
||
|
The latter is followed directly by the RSB, and consequently acts as the only
|
||
|
parameter to the trap handler.
|
||
|
.NH 2
|
||
|
Operand access.
|
||
|
.PP
|
||
|
The EM Manual mentions two ways to access the operands of an instruction. It
|
||
|
should be noticed that the operand in EM is often not the direct operand of the
|
||
|
operation; the operand of the ADI instruction, e.g., is the width of the
|
||
|
integers to be added, not one of the integers themselves. The various operand
|
||
|
types are described in [1]. Each opcode in the text segment identifies an
|
||
|
instruction with a particular operand type; these relations are described in
|
||
|
computer-readable format in a file in the EM tree, \fIip_spec.t\fP.
|
||
|
.PP
|
||
|
The interpreter uses a variant of the second method. Several other approaches
|
||
|
can be designed, with increasing efficiency and equally increasing complexity.
|
||
|
They are briefly treated below.
|
||
|
.NH 3
|
||
|
The Dispatch Table, Method 1.
|
||
|
.PP
|
||
|
When the interpreter starts, it reads the ip_spec.t file and constructs from it
|
||
|
a dispatch table. This table (of which there are actually three,
|
||
|
for primary, secondary
|
||
|
and tertiary opcodes) has 256 entries, each describing an instruction with
|
||
|
indications on how to decode the operand. For each instruction executed, the
|
||
|
interpreter finds the entry in the dispatch table, finds information there on
|
||
|
how to access the operand, constructs the operand and calls the appropriate
|
||
|
routine with the operand as calculated. There is one routine for each
|
||
|
instruction, which is called with the ready-made operand. Method 1 is easy to
|
||
|
program but requires constant interpretation of the dispatch table.
|
||
|
.NH 3
|
||
|
Intelligent Routines, Method 2.
|
||
|
.PP
|
||
|
For each opcode there is a separate routine, and since an opcode uniquely
|
||
|
defines the instruction and the operand format, the routine knows how to get
|
||
|
the operand; this knowledge is built into the routine. Preferably the heading
|
||
|
of the routine is generated automatically from the ip_spec.t file. Operand
|
||
|
decoding is immediate, and no dispatch table is needed. Generation of the
|
||
|
469 required routines is, however, far from simple. Either a generated array
|
||
|
of routine names or a generated switch statement is used to map the opcode onto
|
||
|
the correct routine. The switch approach has the advantage that parameters can
|
||
|
be passed to the routines.
|
||
|
.LP
|
||
|
The interpreter uses a variant of the switch statement scheme. Numerical
|
||
|
information that can be deduced from the opcode is passed as parameters to the
|
||
|
routine; this includes the argument of minis, the high order byte of shorties,
|
||
|
and the fact that the result is to be multiplied by the word size. This
|
||
|
reduces the number of required routines to 338.
|
||
|
.NH 3
|
||
|
Intelligent Calls.
|
||
|
.PP
|
||
|
The call in the switch statement does full operand construction, and the
|
||
|
resulting operand is passed to the routine. This reduces the number of
|
||
|
routines to 133, the number of EM instructions. Generation of the switch
|
||
|
statement from ip_spec.t will be complicated, but the routine space will be
|
||
|
much cleaner. This will not give any speed-up since the same actions are still
|
||
|
required; they are just performed in a different place.
|
||
|
.NH 3
|
||
|
Static Evaluation.
|
||
|
.PP
|
||
|
It can be observed that the evaluation of the operand of a given instruction in
|
||
|
the text segment will always give the same result. It is therefore possible to
|
||
|
preprocess the text segment, decomposing the instructions into structs which
|
||
|
contain the address, the instruction code and the operand. No operand decoding
|
||
|
will be necessary at run-time: all operands have been precalculated. This will
|
||
|
probably give a considerable speed-up. Jumps, especially GTO jumps, will,
|
||
|
however, require more attention.
|
||
|
.NH 2
|
||
|
Disassembly.
|
||
|
.PP
|
||
|
A disassembly facility is available, which gives a readable but not
|
||
|
letter-perfect disassembly of the EM object. The procedure structure is
|
||
|
indicated by placing the indication \fBP[n]\fP at the entry point of each
|
||
|
procedure, where \fBn\fP is the procedure identifier. The number of locals is
|
||
|
given in a comment.
|
||
|
.LP
|
||
|
The disassembler was generated by the software in the directory \fIswitch\fP
|
||
|
and then further processed by hand.
|