6. SwiftForth Assembler

SwiftForth operates in 32-bit protected mode on the IA-32 (Intel Architecture, 32-bit) processors. This class of processors is referred to here as “i386”, which encompasses Intel and AMD derivatives.

Throughout this book, we assume you understand the hardware and functional characteristics of the i386 as described in Intel IA-32 documentation (available for download at www.intel.com and from the SwiftForth page on www.forth.com). We also assume you are familiar with the basic principles of Forth.

This section supplements, but does not replace, Intel’s manuals. Departures from the manufacturer’s usage are noted here; nonetheless, you should use the Intel’s manuals for a detailed description of the instruction set and addressing modes.

Where boldface type is used here, it distinguishes Forth words (such as register names) from Intel names. Usually, these are the same; for example, MOV can be used as a Forth word and as an Intel mnemonic. Where boldface is not used, the name refers to the manufacturer’s usage or to hardware issues that are not particular to SwiftForth or Forth.

6.1 SwiftForth Assembler Principles

Assembly routines are used to implement the Forth kernel, to perform low-level CPU-specific operations, and to optimize time-critical functions.

SwiftForth provides an assembler for the i386. The mnemonics for the 386 opcodes have been defined as Forth words which, when executed, assemble the corresponding opcode at the next location in code space. The SwiftForth kernel is itself implemented with this assembler, so there are plenty of examples available in the kernel source.

Most mnemonics, addressing modes, and other mode specifiers use MASM names, but postfix notation and Forth’s data stack are used to specify operands. Words that specify registers, addressing modes, memory references, literal values, etc. precede the mnemonic.

SwiftForth constructs conditional jumps by using a condition code specifier followed by IF, UNTIL, or WHILE, as described in Section 6.8. Table 19 summarizes the relationship between SwiftForth condition codes used with one of these words and the corresponding Intel mnemonic.

6.2 Code Definitions

Code definitions normally have the following syntax:

CODE <name>   <assembler instructions>   RET   END-CODE

Or this:

CODE <name>   <assembler instructions>   RET   END-CODE

For example:

CODE DEPTH ( -- n )
   PUSH(EBX)            \ Save TOS
   S0 [ESI] EBX MOV     \ Get S0 from user area
   EBP EBX SUB          \ Stack depth in bytes
   EBX SAR   EBX SAR    \ Convert to cells
   RET   END-CODE

All code definitions must be terminated by the command END-CODE.

The differences between words defined by ICODE and CODE are small, but crucial.

ICODE begins a word which, when referenced inside a colon definition, will expand in-line code at that point in the definition. It may be called from another assembler routine, but it may not call any external function, nor may it exit any way except via the RET at the end.

CODE begins a word which can only be called as an external function. When a CODE word is referenced inside a colon definition, a CALL to it will be assembled. A CODE word may call other words, and may have multiple exits.

You may name a code fragment or subroutine using the form:

LABEL <name>   <assembler instructions>  END-CODE

This creates a definition that returns the address of the next code space location, in effect naming it. You may use such a location as the destination of a branch or call, for example.

If you reference a CODE definition inside a colon definition, SwiftForth will assemble a call to it; if you invoke it interpretively, it will be executed.

Reference to a LABEL under any circumstance will return the address of the labeled code.

The defining words LABEL, CODE, and ICODE may not be used to define entry points to any part of a code routine except the beginning.

Within code definitions, the words defined in the following sections may be used to construct machine instructions.

CODE <name>
( — )

Start a new assembler definition for the word name. If name is referenced inside a colon definition, it will be called; if referenced interpretively, it will be executed.

ICODE <name>
( — )

Start a new assembler definition for the word name. If name is referenced inside a colon definition, its code will be expanded in-line; if referenced interpretively, it will be executed. A word defined by ICODE may not contain any external references (calls or branches).

LABEL <name>
( — )

Start an assembler code fragment for the word name. If the definition is referenced, either inside a definition or interpretively, the address of its code will be returned on the stack.

END-CODE
( — )

Complete an assembler sequence started by CODE, ICODE, or LABEL.

6.3 Registers

The processor contains sixteen registers, of which eight 32-bit general registers can be used by an application programmer. Some have 8-bit and 16-bit, as well as 32-bit forms, as shown in Figure 19.

Figure 19. General registers

Neither the segment registers nor the status and control registers (not shown here) may be used directly under Windows.

The general registers EAX, EBX, etc., hold operands for logical and arithmetic operations. They also can hold operands for address calculations (except the ESP register cannot be used as an index operand). The names of these registers are derived from the names of the general registers on the 8086 processor, the AX, BX, CX, DX, BP, SP, SI, and DI registers. As Figure 19 shows, the low 16 bits of the general registers can be referenced using these names.

Each byte of the 16-bit registers AX, BX, CX, and DX also has another name. The byte registers are named AH, BH, CH, and DH (high bytes) and AL, BL, CL, and DL (low bytes).

All of the general-purpose registers are available for address calculations and for the results of most arithmetic and logical operations; however, a few instructions assign specific registers to hold operands. For example, string instructions use the contents of the ECX, ESI, and EDI registers as operands. By assigning specific registers for these functions, the instruction set can be encoded more compactly. The instructions that use specific registers include: double-precision multiply and divide, I/O, strings, move, loop, variable shift and rotate, and stack operations.

SwiftForth has assigned certain registers special functions for the Forth virtual machine, as follows:

EBX is the top of stack
ESI is the user area pointer
EDI contains the origin of SwiftForth’s memory window
EBP is the data stack pointer
ESP is the return stack pointer

These registers may not be used for any other purpose unless you save and restore them.

6.4 Instruction Components

An instruction may have a number of components, which are listed below. The only required component is the opcode itself. In SwiftForth, as in many Forth assemblers, an opcode is represented by a Forth word which takes arguments specifying the other components, as desired, and assembles the completed instruction. For this reason, the opcode generally comes last, preceded by other notation (i.e., the syntax is ). Except for this ordering, SwiftForth assembler notation follows Intel notation.

  • The components of an instruction may include:
  • Prefixes: one or more bytes that modify the operation of the instruction.
  • Register specifier: an instruction can specify one or two register operands. Register specifiers occur either in the same byte as the opcode or in the same byte as the addressing-mode specifier.
  • Addressing-mode specifier: when present, specifies whether an operand is a register or memory location; if in memory, specifies whether a displacement, a base register, an index register, and scaling are to be used.
  • SIB (scale, index, base) byte: when the addressing-mode specifier indicates the use of an index register to calculate the address of an operand, an SIB byte is included in the instruction to encode the base register, the index register, and a scaling factor.
  • Displacement: when the addressing-mode specifier indicates a displacement will be used to compute the address of an operand, the displacement is encoded in the instruction. A displacement is a signed integer of 32, 16, or 8 bits. The 8-bit form is used in the common case when the displacement is sufficiently small. The processor extends an 8-bit displacement to 16 or 32 bits, taking into account the sign.
  • Opcode: specifies the operation performed by the instruction. Some operations have several different opcodes, each specifying a different form of the operation.

These components make up the instruction operands discussed in the next section.

6.5 Instruction Operands

Everything except the opcode may be considered an operand. SwiftForth notation for various kinds of operands is similar to Intel’s, except for order. In general, where there are two operands, the order is {source} {destination} .

6.5.1 Implicit Operands

An operand is implicit if the instruction itself specifies it. Some examples include:

CLD Clear direction flag to zero
RET Return from subroutine

6.5.2 Register Operands

A register operand specifies one or two registers, by giving their names. Some examples include:

EAX INC         Increment EAX
EBX ECX MOV     Move EBX (top of stack) to ECX

6.5.3 Immediate Operands

An immediate operand specifies a number as part of the instruction. SwiftForth indicates immediate operands by following the number with a # character. Numbers may be specified in any base; if your base is decimal and you want to specify a single number in hex, you can use the $ prefix (see Section 4.3). Some examples of immediate operands include:

8 # EBP ADD     Add 8 to EBP (the Forth data stack pointer)
2 # EBX SUB     Subtract 2 from EBX (the top stack item)
32 # AL XOR     Exclusive-or the character in AL by 32

6.5.5 Memory Reference Operands

This is a large class of operands, because of the many ways to address memory.

6.5.5.1 Direct Addressing

This is the simplest form of memory addressing, in which you simply specify an address and SwiftForth assembles a reference to it.

Addresses used as destinations for a JMP or CALL are assembled as offsets from the location of the instruction, within the memory space allocated to SwiftForth by Windows. To get an address of a defined word to be used as a destination for a JMP or CALL use the form:

' <name> >CODE

The word >CODE converts the execution token supplied by ‘ (“tick”) to a suitable address.

VARIABLEs and other data structures present some particular problems as a consequence of the position-independent implementation strategy used in SwiftForth. The address returned by such words (to be precise, any word defined using CREATE that returns a data space address) is the absolute address in the machine. SwiftForth and executable programs generated from SwiftForth are always instantiated in the same virtual address space, but DLLs may be instantiated in different places at different times. This means that code that might ever be used in a DLL must avoid compiling references to absolute addresses. If you are writing code that may ever be used inside a DLL, we recommend that you test your code using the optional PROTECTION option described in Section 8.2.2.

In order to obtain a generic address, special actions are required. SwiftForth provides an assembler macro ADDR that will place a suitable data address in a designated register, used in the form:

<addr> <reg> ADDR

where addr is the data address returned by a VARIABLE or any word defined using CREATE, and reg is the desired register. The following example shows how to add a value to the contents of a VARIABLE:

VARIABLE DATA

CODE +DATA ( n -- )     \ Add n to the contents of DATA
   DATA EAX ADDR        \ Address of DATA to EAX
   EBX 0 [EAX] ADD      \ Add top-of-stack to DATA
   POP(EBX)             \ Pop stack
   RET   END-CODE

Another example is:

' THROW >CODE JMP

Jump to THROW (on an error condition). When using this method to abort from code, you must provide the throw code value in EBX (top-of-stack).

ADDR
( addr reg — )

Convert the data address addr from an absolute address to an address relative to the start of SwiftForth’s memory, and place it in register reg.

6.5.5.2 Addressing with an Offset

Offset addressing combines parameters that are added to a base or index register, with other parameters, as shown in Figure 20.

Figure 20. Offset (or effective address) computation

The SwiftForth assembler syntax is:

disp [base] [index*scale] opcode

The displacement must be given, even if it is zero. The assembler will use the correct instruction form, depending upon the size of the displacement (usually omitting it if zero).

You may specify a base register, an index register, or both. The default scale of an index register is 1; the form of reference is the same as for a base register, e.g., [EBX]. For other scale factors, the form of reference is:

ScaleFormExample
2[reg*2][EAX*2]
4[reg*4][EAX*4]
8[reg*8][EAX*8]

For example:

0 [EBP] EAX MOV

Moves the second stack item to EAX

CELL [EBP] EAX MOV

Moves the third stack item to EAX

-1 [ECX] [EDI] EAX LEA

Loads the effective address (ECX+EDI) -1 into EAX.

0 [EDI] [EDX*4] EAX CMP

Adds the number of cells (4 bytes each) in EDX to the base address in EDI, and compare the referenced item to EAX.

6.5.5.3 Stack-based Addressing

The hardware stack, controlled by ESP, is used for subroutine calls, which makes it a natural choice for the Forth return stack pointer. The stack is affected implicitly by CALL and RET instructions, but is directly manipulated using PUSH and POP. These are single-operand instructions, where the operand may be an immediate value, register, or memory location.

The hardware stack is a convenient place for temporarily saving a register’s contents while you use that register for something else.

SwiftForth uses EBP as its data stack pointer. However, this stack must be managed more directly, because PUSH and POP are specifically tied to ESP. Both stacks are considered to grow towards low memory. That is, if you do a PUSH, ESP will be decremented; if you do a POP, it will be incremented. Correspondingly, to push something on the data stack, decrement EBP; to pop something, increment EBP. Data stack management is slightly complicated (but its performance is improved) by SwiftForth’s practice of keeping the top-of-stack item in EBX.

To facilitate use of the data stack pointer, SwiftForth provides the assembler macros PUSH(EBX) and POP(EBX) that push and pop the top stack item in EBX.

Here are some examples of data stack management:

0 [EBX] EBX MOV

Replaces the top item with the contents of its address (the action of @).

CELL # EBP SUB
EBX 0 [EBP] MOV

Pushes the contents of EBX onto the data stack (the action of DUP).

PUSH(EBX) 
CELL [EBP] EBX MOV

Pushes a copy of the second stack item on top of the stack (the action of OVER).

CELL [EBP] EBX MOV 
2 CELLS # EBP ADD

Discards the top two stack items (the action of 2DROP).

6.5.5.4 User Variable Addressing

ESI contains the address of the start of the user area. User variables are defined in terms of an offset from this area. When a user variable is executed, an absolute address is returned which is the address of the start of the current task’s user area plus the offset to this user variable.

The assembler line:

BASE [ESI] EBX MOV

would load EBX from the address userarea + userarea + offset, which is wrong. But the phrase

BASE STATUS - [ESI] EBX MOV

would load the real contents of BASE into EBX, subtracting off the extraneous userarea.

This is rather cumbersome, so SwiftForth provides the following shorthand notation:

: [U] ( useraddr -- offset ) STATUS - [ESI] ;

which is available as an assembler addressing mode. So, the appropriate form of an instruction to load EBX from BASE becomes:

BASE [U] EBX MOV
[U]
( addr — offs reg )

Provides the appropriate addressing mode for accessing a user variable at addr by converting addr to an offset and index register [ESI].

Mode specifiers, including instruction prefixes, modify the action of instructions. These include:

Size specifiers control whether the instruction affects data elements whose size is eight bits, 16 bits, etc.

Repeat prefixes control string instructions, causing them to act on whole strings instead of on single data elements.

Segment register overrides control what memory space is affected (not used in Windows applications.)

6.6.1 Size Specifiers

The size specifiers defined in SwiftForth are listed below. These words must be used after a memory reference operand, and will modify its size attribute.

SpecifierDescription
BYTE8-bit operand
WORD16-bit integer
DWORD32-bit integer or floating-point
QWORD64-bit integer or floating-point
TBYTEBCD or 80-bit internal floatin-point

For example:

$40 # 5 [EBX] BYTE TEST

Tests the bit masked by $40 in the byte at EBX+5.

0 [EAX] TBYTE FLD

Pushes the floating-point value pointed to by EAX onto the numeric stack.

6.6.2 Repeat Prefixes

The assembler provides a group of string operators that use ESI as a pointer to a source string, EDI as a pointer to a destination string, and ECX as a count register. These instructions can do a variety of things, including move, compare, and search the strings. The string instructions can operate on single data elements or can process the entire string.

In the default mode, they operate on a single item, and automatically adjust the registers to prepare for the next item. If, however, a string instruction is preceded by one of the repeat prefixes, it will repeat until one of the terminating conditions is encountered.

PrefixDescription
REPRepeat, decrementing ECX until ECX=0
REPE, REPZRepeat until ECX=0 or ZF=0
REPNE, REPNZRepeat until ECX=0 or ZF=1

6.7 Direct Branches

In Forth, most direct branches are performed using structures (such as those described above) and code endings (described below). Good Forth programming style involves many short, self-contained definitions (either code or high level), without the unstructured branching and long code sequences that are characteristic of conventional assembly language. The Forth approach is also consistent with principles of structured programming, which favor small, simple modules with one entry point, one exit point, and simple internal structures.

However, direct transfers are useful at times, particularly when compactness of the compiled code or extreme performance requirements override other criteria. The SwiftForth assembler supports JMP, CALL, and all forms of conditional branches, although most conditional branching is done using assembler structures.

A CALL is automatically generated when a word defined by CODE is referenced inside a colon definition, but you may also use CALL in assembly code. The argument to CALL must be the address of the code field of a subroutine that ends with an RET.

The normal way to define a piece of code intended to be referenced from other code routines is to use LABEL (described below). To get a suitable address for a word defined by CODE or ICODE, you must use the form:

' <name> >CODE CALL

The word >CODE transforms the execution token returned by ‘ to a suitable code field address.

LABEL is used in the form described above. Invoking name returns the address identified by the label, which may be used as a destination for either a JMP or a CALL.

For example, this code fragment is used by code that constructs a temporary data buffer to provide a substitute return address:

LABEL R-GO-ON
   EAX JMP      \ jump to address in EAX
   END-CODE

6.8 Assembler Structures

In conventional assembly language programming, control structures (loops and conditionals) are handled with explicit branches to labeled locations. This is contrary to principles of structured programming, and is less readable and maintainable than high-level language structures.

Forth assemblers in general, and SwiftForth in particular, address this problem by providing a set of program-flow macros, listed in the glossary at the end of this section. These macros provide for loops and conditional transfers in a structured manner, and work like their high-level counterparts. However, whereas high-level Forth structure words such as IF, WHILE, and UNTIL test the top of the stack, their assembler counterparts test the processor condition codes.

The structures supported in the SwiftForth assembler are:

BEGIN   <code to be repeated>   AGAIN

BEGIN   <code to be repeated>   <cc> UNTIL

BEGIN   <code>   <cc> WHILE   <more code>   REPEAT

<cc> IF   <true case code>   ELSE   <false case code>   THEN

In the sequences above, cc represents condition codes, which are listed in the glossary sections below. The combination of a condition code and a structure word (UNTIL, WHILE, IF) assembles a conditional branch instruction Bcc, where cc is the condition code. The other components of the structures — BEGIN, REPEAT, ELSE, and THEN — enable the assembler to provide an appropriate destination address for the branch.

In addition, the Intel instructions LOOP, LOOPE, and LOOPNE may be used with BEGIN to make a loop. For example:

   BEGIN
      LODSB             \ read a char
      BL AL CMP         \ check for control
      U< IF             \ if control
      CHAR ^ # AL MOV   \ replace with caret
      THEN
      STOSB             \ write the char
   LOOP                 \ and repeat

All conditional branches use the results of the previous operation which affected the necessary condition bits. Thus:

EBX EAX CMP   < IF

executes the true branch of the IF structure if the contents of EAX is less than the contents of EBX.

The word NOT following a condition code inverts its sense. The name NOT is also the name of an opcode mnemonic, so the SwiftForth assembler will examine the stack, and if a valid condition code is present, it will invert it; otherwise, it will assemble a NOT instruction.

In high-level Forth words, control structures must be complete within a single definition. In assembler, this is relaxed; the assembler will automatically assemble a short or long form for all relative jump, call, and loop instructions. Control structures that span routines are not recommended as they make the source code harder to understand and harder to modify.

The following table shows the instructions generated by SwiftForth condition codes in combination with words such as IF or UNTIL. See the glossary below for details. Refer to your processor manual for details about the condition flags.

ConditionInstructionDescriptionForth cc
OF=1JOJump if overflowOV NOT
OF=0JNOJump if not overflowOV
CF=1JB
JC
Jump if below
Jump if carry
U< NOT
CC, CS NOT
CF=0JNB
JNC
Jump if not below
Jump if not carry
U<
CS
ZF=1JE
JZ
Jump if equal
Jump if zero
0=NOT, 0<>
ZF=0JNE
JNZ
Jump if not equal
Jump if not zero
0=
CF=1 or ZF=1JBE
JNA
Jump if below or equal
Jump if not above
U>
CF=0 and ZF=0JNBE
JA
Jump if not below or equal
Jump if above
U> NOT
SF=1JSJump if sign 0< NOT
SF=0JNSJump if not sign0<
PF=1JP
JPE
Jump if parity
Jump if parity even
PO
PF=0JNP
JPO
Jump if not parity
Jump if parity odd
PE
SF≠OFJL
JNGE
Jump if less
Jump if not greater or equal
< NOT
SF=OFJNL
JGE
Jump if not less
Jump if greater or equal
<
ZF=1 or SF≠OFJLE
JNG
Jump if less or equal
Jump if not greater
0>
ZF=0 and SF=OFJNLE
JG
Jump if not less or equal
Jump if greater
> NOT
ECX=0JECXZJump if ECX is zeroECXNZ
N/AJMPUnconditional jumNEVER

Note that the standard Forth syntax for sequences such as 0< IF implies no branch in the true case. Therefore, the combination of the condition code and branch instruction assembled by IF, etc., branch on the opposite condition (i.e., ≥ 0 in this case).

These constructs provide a level of logical control that is unusual in assembler-level code. Although they may be intermeshed, care is necessary in stack management, because REPEAT, UNTIL, AGAIN, ELSE, and THEN use the addresses on the stack.In the glossary entries below, the stack notation cc refers to a condition code.

Branch Macros

BEGIN
( — addr )

Leave the current address addr on the stack, to serve as a destination for a branch. Doesn’t assemble anything.

AGAIN
( addr — )

Assemble an unconditional branch to addr.

UNTIL
( addr cc — )

Assemble a conditional branch to addr. UNTIL must be preceded by one of the condition codes listed below.

WHILE
( addr1 cc — addr2 addr1 )

Assemble a conditional branch whose destination address is left empty, and leave the address of the branch addr on the stack. WHILE must be preceded by one of the condition codes listed below.

IF
( cc — addr )

Assemble a conditional branch whose destination address is not given, and leave the address of the branch on the stack. IF must be preceded by one of the condition codes listed below.

ELSE
( addr1 — addr2 )

Set the destination address addr1 of the preceding IF to the next word, and assemble an unconditional branch (with unspecified destination) whose address addr2 is left on the stack.

THEN
( addr — )

Set the destination address of a branch at addr (left by IF or ELSE) to point to the next location in code space.

Condition Codes

Each of these words returns a condition code used by UNTIL, WHILE, and IF.

0<
( — cc )

Branch on positive.

0=
( — cc )

Branch on non-zero.

0>
( — cc )

Branch on 0 or negative.

0
( — cc )

Branch on zero.

0>=
( — cc )

Branch on negative.

U<
( — cc )

Branch on unsigned greater than or equal.

U>
( — cc )

Branch on unsigned less than or equal.

U>=
( — cc )

Branch on unsigned less than.

U<=
( — cc )

Branch on unsigned greater than.

<
( — cc )

Branch on greater than or equal.

>
( — cc )

Branch on less than or equal.

>=
( — cc )

Branch on less than.

<=
( — cc )

Branch on greater than.

CS
( — cc )

Branch on carry clear. Same as U<.

CC
( — cc )

Branch on carry set.

PE
( — cc )

Branch on parity odd.

PO
( — cc )

Branch on parity even.

OV
( — cc )

Branch on overflow clear.

ECXNZ
( — cc )

Branch on ECX = 0.

NEVER
( — cc )

Unconditional branch.

NOT
( cc1 — cc2 )

Invert condition code cc1 to give cc2.

Note that the sense of phrases such as < IF and > IF is parallel to that in high-level Forth; < IF generates a branch on greater than or equal, so the code following IF will be executed if the test fails.

For example:

CODE TRY  ( n1 -- n2 )
   10 # EBX CMP   < IF   0 # EBX MOV   THEN
   RET   END-CODE

When this is executed, one gets the result:

12 TRY . 12 ok
8 TRY . 0 ok

6.9 Writing Assembler Macros

The important thing to remember when considering assembler macros is that the various elements in SwiftForth assembler instructions (register names, addressing mode specifiers, mnemonics, etc.) are Forth words that are executed to create machine language instructions. Given that this is the case, if you include such words in a colon definition, they will be executed when that definition is executed, and will construct machine language instructions at that time, i.e., expanding the macro.

An assembly language macro in SwiftForth is a colon definition whose contents include assembler commands.

The only complication lies in the fact that SwiftForth assembler commands are not normally “visible” in the search order (see the discussion of search orders in Section 5.5.2). This is necessary because there are assembler versions IF, WHILE, and other words that have very different meanings in high-level Forth. When you use CODE, ICODE, or LABEL to start a code definition, those words automatically select the assembler search order, and END-CODE restores the previous search order. However, to make macros, you will need to manipulate search orders more directly.

Relevant commands for manipulating vocabularies for assembler macros are given in the glossary at the end of this section. Here are a few simple examples.

Example 1: Drop top stack item

: POP(EBX) ( -- )
   0 [EBP] EBX MOV              \ Move second cell on data stack to top
   CELL [EBP] EBP LEA  ;        \ Adjust data stack pointer

Example 2: Push top stack item

: PUSH(EBX) ( -- )
   -1 CELLS [EBP] EBP LEA       \ Allocate one cell on the data stack
   EBX 0 [EBP] MOV ;            \ Move top of stack to that cell

Example 3: Data space address reference

: ADDR ( addr reg -- )
   DUP R32? 0 = THROW >R        \ Must be R32
   ORIGIN - [EDI] R> LEA ;      \ Calculate displacement in data space, use for LEA