The MDL Programming Environment
Chapter 7. The Assembler
It is occasionally necessary to write MDL routines in assembly language, usually to interface with a feature of the operating system not available in the interpreter. The MDL assembler (which is also used by the MDL compiler) provides this ability.
7.1. The Assembler
The MDL assembler provides the MDL user with a means of writing RSUBRs directly in machine language. The assembler is also used as the object language of the compiler. This section is a description of the assembler, its use, and some of its pseudo-operations.
7.1.1. General Organization
The MDL assembler is written in MDL to produce code that runs in the MDL environment. It takes arguments in the following form:
<FILE-ASSEMBLE input-file:string
output-file:string
quick:boolean>
The arguments are an input-file containing MDL assembly code (possibly for several RSUBRs), an optional output-file in which to put the binary output (by default the same file as input but with second file name "NBIN"), and an optional third argument which tells whether to use NBIN format output, and which under normal circumstances should always be T. There are four other optional arguments which are the same as the second through fifth arguments of ASSEMBLE.
<ASSEMBLE body
locals
messages
list
symbols>
(All the arguments are optional with the exception of body.)
body may be a CHANNEL, in which case all instructions in the file associated with the CHANNEL are assembled, or it may be a structured object, in which case all instructions in the object are assembled.
locals specifies the OBLIST to use for local symbol lookup when the body is a CHANNEL. The default is <1 .OBLIST> when the assembler is called.
messages is a CHANNEL to receive error messages, etc. It defaults to .MESSAGE-CHANNEL.
list is a CHANNEL to receive an assembly listing. If list is not supplied, no listing is generated. If list is a non-FALSE non-CHANNEL, and messages is a CHANNEL, then the messages CHANNEL will receive the address of each label. If list is a FALSE, then no listing is produced. The default is .LINE-CHANNEL (Initially LINE-CHANNEL is FALSE.)
symbols indicates if true that a DDT symbol table of all the labels for use with "RDB" (see section 7.2) will be generated. The default is .MAKE-SYM-TABLE (Initially MAKE-SYM-TABLE is FALSE.)
7.1.2. The Assembler as a Program
The assembler also exists as a program called ASSEM, which encapsulates FILE-ASSEMBLE.
7.1.3. Format of Assembler's Source
The MDL assembler's equivalent of a line of code is a FORM. It assembles FORMs into instructions in much the same way that a typical assembler treats lines of source code. ATOMs at the top level (i.e. not in FORMs) are treated as labels. The FORMs are assembled based on the TYPE of the GVAL of the first ATOM in the FORM. The GVALs of ATOMs whose PNAMEs are the PDP-10 instructions are of TYPE OPCODE (PRIMTYPE WORD); the 'value word' has the 36 bit value of the instruction. For example, in
<MOVE A* 1 (B)>
the value of MOVE (in the OP OBLIST) is #OPCODE 200000000000. This FORM is assembled directly into an instruction. If the GVAL of the first ATOM in a FORM is something applicable (SUBR, FUNCTION, RSUBR etc.) the FORM is EVALed and the resulting SPLICE of FORMs is assembled. This is how macros and pseudo-ops are implemented. Notice that a pseudo-op or macro may produce no code by returning an empty SPLICE.
7.1.4. Instruction Assembly
Having determined that a FORM is going to assemble into an instruction, the assembler basically adds up the values of all the items in the FORM. In the case of items of TYPE OPCODE, a full 36 bit add is performed. Items of TYPE ADDRESS refer to labels in the program. Since the code is all location insensitive and will move around during garbage collection, references to labels must be indexed by accumulator M, the base register. Therefore, label symbols include an M in the left half and must also be added in with a full-word add. Items of PRIMTYPE WORD other than OPCODEs and ADDRESSes are ANDBed with 777777 before being added, and the carry from right half to left half is suppressed. When ATOMs are found in FORMs that are being assembled into instructions, special lookup rules are in effect. If the ATOM has a global value, that value is used. If the ATOM does not have a global value but has a local value, it is used. If the ATOM has neither a local nor global value, it is assumed to be a local symbol for this assembly. In this case the symbol value is used if it has already been defined, otherwise it is added to a list of as yet undefined symbols.
Objects other than ATOMs or PRIMTYPE WORDs cause the assembler to take special action.
- LISTs are used to indicate swapping left and right halves. For example
<MOVE (1)>would put the 1 in the index field of the MOVE instruction (similar to MIDAS). - A VECTOR indicates a constant. The VECTOR may contain any number of FORMs to be assembled
at the end of the program. For example:
<PUSH TP* [<1 (1)>]>pushes a constant containing 1 in the right and left halves. - A FORM is simply EVALed and the value returned is used.
7.1.5. Initial Symbols
The OBLIST structure in effect during assembly is
(op mud! DEFAULT local root)
The OBLIST op is named OP and contains the PDP-10 opcodes, the MDL accumulator definitions (in both
accumulator and address fields), and the pseudo-ops. The OBLIST mud! is named MUDDLE and contains values
of many labels in the interpreter. This enables programs to do things like
As stated earlier, every accumulator has two symbols associated with it, one for the address field and one for the accumulator field. This is because there is no syntax to specify which field is intended. The address symbol is simply the accumulator's name, and the accumulator symbol is the name with an asterisk () appended to it; e.g. A versus A.
7.1.6. Macro Writing
Whenever an element or subelement of an instruction is a FORM and the first element of the FORM has an APPLICABLE GVAL, the FORM is evaluated and the result (unless it is a SPLICE) is re-evaluated as if it were in place of the FORM. This feature constitutes the assembler's macro facility. For compatibility between 'top-level' macros, which generate whole instructions, and macros which generate parts of an instruction, top-level macros may wish to return several instructions. To indicate that what is returned is several instructions, it is necessary to return an object of type SPLICE (PRIMTYPE LIST). The elements of the SPLICE are treated as individual instructions. An empty SPLICE may be returned from a macro which is part of an instruction, and the effect is as if a 0 were returned. This is the only SPLICE which may be returned from a macro which is a part of an instruction.
7.1.7. Pseudo Operations
The next part of this document will describe pseudo-ops available in the MDL assembler. There is no difference between a pseudo-op and a macro in the assembler except that the pseudo-operations are supplied by the system.
<TITLE name:string>
This is about the only required pseudo-op. It must be the first instruction to be assembled. It takes one argument, the name of the RSUBR being assembled. If additional TITLEs are found in a file being assembled, they are assumed to both end the previous RSUBR and begin the next. The assembler prints each TITLE on the messages CHANNEL as it is encountered.
<SUB-ENTRY entry:atom decl>
This pseudo-op is used to define additional RSUBR-ENTRYs for the RSUBR being assembled. The entry argument is the name of the RSUBR-ENTRY and the optional decl argument is a DECL for the entry.
<INTERNAL-ENTRY entry:atom nargs:fix>
is used to create an INTERNAL-ENTRY for a GLUEable RSUBR. Its arguments are the name of the INTERNAL-ENTRY and the number of arguments that will have been pushed on the stack for it when it is called. See also section 7.1.9 for details on writing GLUEable RSUBRs.
<DECLARE ("VALUE" decl decl decl ....)>
is used to supply declarations for the RSUBR named in the TITLE. It must occur before any code-generating instructions. DECLARE takes a LIST as its one argument. The format of the LIST is as described in [3]. The string "VALUE" is optional; if supplied it causes the first decl to declare the TYPE of the value of the RSUBR. Each additional decl is associated with one argument. Special STRINGs may also appear in the LIST with the following meanings:
-
"QUOTE" The next argument is QUOTEd (not EVALed).
-
"OPTIONAL" The rest of the arguments are optional (the RSUBR must supply any defaults for these).
-
"CALL" If this appears, it must be directly after the "VALUE" decl. It says there is one argument and it is the FORM generating the call (see "CALL" for FUNCTIONs in [3]).
-
"ARGS" This must be the last STRING. It says treat the rest of the arguments in the FORM as a LIST and pass it as the argument (see "ARGS" for FUNCTIONs in [3]).
-
"TUPLE" EVAL the rest of the arguments and pass them.
indicates the end of an RSUBR or group of RSUBRs. Only the text between TITLE and END pseudo-ops will be processed by the assembler. This makes it possible to intermix assembler source code and normal MDL source code in the same file (although assembly must be done before compilation in such cases).
<TYPE-CODE type:atom>
allows references to the internal TYPE codes for both system and user defined TYPEs. It takes one argument, the MDL TYPE name. For example:
<MOVSI A* <TYPE-CODE FIX>>
puts the TYPE code for FIX into the left half of accumulator A.
<TYPE-WORD type:atom any ...>
generates a reference to a word containing the TYPE code for type in the left half and possibly other junk in the right half. The first argument is the TYPE name and the rest of the arguments are optional but if supplied are added into the right half. If the TYPE is an initial TYPE and no right half is generated, a reference to the 'ST type' location in the interpreter is generated. For example,
<PUSH TP* <TYPE-WORD FIX>>
<PUSH TP* [0]>
would push a FIX 0 on the stack.
<GETYP ac type:atom>
has the same form as a PDP-10 instruction. It gets the TYPE code for type into the right half of its accumulator from its address. This is done by generating an appropriate LDB (load byte) instruction.
<MQUOTE object:any>
allows the RSUBR to reference garbage collected space. It adds its argument to the RVECTOR (if it isn't already there) and evaluates to an address of the form offset(R), pointing to the value word for object.
<PQUOTE object:any>
is identical to <
<IQUOTE object:any label:atom>
is like PQUOTE except that this will add a new element to the reference VECTOR each time called. The optional label if given defines the ATOM to be a label referring to that element. This is the only way to refer to that element again.
<PSEUDO arg:any>
evaluates its argument for its side effects and assembles no code.
<SIXBIT string>
makes SIXBIT of the legal characters of string.
<SQUOZE string sqbits:word>
makes SQUOZE of the legal characters of string and sticks the low-order four bits of the optional sqbits in the high-order four bits of the value. See the MIDAS Manual [4] for an explanation of the SQUOZE code.
<BYTE bound:fix byte-size:fix location>
Example: <BYTE 1 36 (C)> is like <(*014300*) (C)>.
<ARG argnum:fix>
is like <(AB) <* 2 <- .argnum 1>>>. ARG should not be used in GLUEable code.
<STACK sym1:atom sym2:atom sym3:atom ...>
makes sym1 a symbol for <(TB) 0>, sym2 a symbol for <(TB) 2>, sym3 a symbol for <(TB) 4>, etc.
STACK should not be used in GLUEable code.
<DPUSH ac args>
<DPOP ac args>
<DMOVE ac args>
<DMOVEM ac args>
are the double-word PDP-10 instructions. For example,
<DPUSH ac args>
expands into
#SPLICE (<PUSH ac args> <PUSH ac args 1>)
<UNDEF? symbol:atom>
evaluates to true only if the symbol has previously in the code been used as a symbol, but has not been defined.
<IF-NEEDED symbol:atom instructions ...>
If <UNDEF? symbol> evaluates to true, then all the instructions are inserted at the current location, otherwise they are not.
<*INSERT file-spec:string>
takes a file and reads instructions from it and inserts the instructions read at the current place.
7.1.8. The Type RSUBR
An RSUBR is a MDL object of PRIMTYPE VECTOR. The first element of an RSUBR is always of TYPE CODE (or PCODE). CODE is of PRIMTYPE UVECTOR, consisting of words or instructions. The second element of an RSUBR is an ATOM which is the RSUBR's name. If the RSUBR has declarations they are the third element. The rest of the RSUBR contains MDL objects which must be referenced by the code.
An RSUBR-ENTRY is a VECTOR of two or three items. The first item is either an RSUBR or an ATOM whose GVAL is an RSUBR, the second is an ATOM which is the entry's name and the third is a DECL for the entry. The difference between an RSUBR and an RSUBR-ENTRY is that an RSUBR always starts running at the beginning of the code when it is called while an RSUBR-ENTRY usually starts running somewhere in the middle of the code.
7.1.9. Writing Gluable RSUBRs
Certain conventions must be followed when writing hand coded RSUBRs in order to get the most benefit
from GLUEing. If the RSUBR (or RSUBR-ENTRY) has "TUPLE" in its DECL, it is already in the best shape
possible. In all other cases, the code after the TITLE or SUB-ENTRY pseudo-operation should simply push
the arguments onto the TP stack and PUSHJ P** to one of the internal entries based on the number of items
on the stack. After the PUSHJ it should do a
7.2. Debugging Binary Code
Binary code produced by the MDL assembler or the MDL compiler may be debugged with DDT, like any other binary code. However, an interface between that code and the DDT environment must exist. That interface is the "RDB" PACKAGE. It is obtained by
<USE "RDB">
The symbol table optionally produced by the assembler can be passed to DDT and at the same time the RSUBR frozen (moved out of normal garbage-collected space) by:
<RFREEZE name-of-rsubr:atom>
Note that name-of-rsubr may also refer to an RSUBR-ENTRY.
<RBREAK name-of-rsubr:atom>
is similar, but in addition causes DDT to put a breakpoint at the first instruction of the RSUBR. If there is no symbol table, RFREEZE and RBREAK merely freeze the RSUBR and pass up symbols for the RSUBR name and any sub-entries. In all cases the symbols passed up are made up of the legal SQUOZE characters (letters, digits, $, %, .) of the name, up to six characters. For example the ATOM FOO-BLECH becomes the symbol FOOBLE.
<ADR object:any>
returns the address of object as a FIX. For example,
<RUNBREAK name-of-rsubr:atom>
clears the breakpoint(s) at the beginning of the RSUBR and of any of its sub-entries.
7.3. Unassembling Binary Code
Converting compiled or assembled binary code back into something resembling the original assembler source code is an operation that is performed primarily in one situation: tracking down a MDL compiler bug. It is, however, almost invaluable in that situation. The PACKAGE containing the unassembler is "UNASSM". The main entry is
<UNASSEMBLE code:rsubr-or-group
output:channel-or-string
glue?:boolean>
code is the object being unassembled. It is either an RSUBR (not an RSUBR-ENTRY, note), or an ATOM whose LVAL is a group (as created by GROUP-LOAD).
output is where to put the output. If it is a STRING, then the output is put in a file with that name. If output is a CHANNEL, then output is done on that CHANNEL. The file is "code UNASSM" by default.
glue? (by default T) tells whether there are glue bits for the code loaded. If there are none, this argument should be given as a FALSE.
The output produced by UNASSEMBLE is like the MDL compiler's assembler input, with the addition of comments which give code and stack offsets for stack slots referenced. This information is useful in tracing exactly what is going on in the code, but it is not always accurate, since the compiler's stack model is sometimes too complex for the unassembler to understand.