.\" 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.