Extending a Generic Hardware Simulator with a

Multi-threaded Programming Language for Embedded System Software Debugging

by

Kenneth James Duda

Submitted to the Department of Electrical Engineering and Computer Science in partial fulfillment of the requirements for the degrees of Bachelor of Science and Master of Science in Computer Science and Engineering at the MASSACHUSETTS INSTITUTE OF TECHNOLOGY May 1993

\begin{matrix}c \\ ◯ \end{matrix} Massachusetts Institute of Technology 1993. All rights reserved.

Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Department of Electrical Engineering and Computer Science

May 18, 1993

Certified by. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Gill Pratt

Research Associate Thesis Supervisor

Certified by. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Wilfred Wong Company Supervisor

Accepted by . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Campbell L. Searle

Chairman, Departmental Committee on Graduate Students

Extending a Generic Hardware Simulator with a

Multi-threaded Programming Language for Embedded

System Software Debugging

by

Kenneth James Duda

Submitted to the Department of Electrical Engineering and Computer Science

on May 18, 1993, in partial fulfillment of the requirements for the degrees of Bachelor of Science and Master of Science in Computer Science and Engineering

Abstract

The cost of developing software for embedded systems is currently very high. This thesis discusses a tool called SOTEST, which can dramatically reduce embedded system development time (and, therefore, cost). SOTEST makes possible the debugging of software for an embedded system on a workstation, without any of the hardware for the embedded system. The system includes a microprocessor simulator for the Intel 80186, a symbolic debugger that understands Microsoft debugging information, and an interpreter for a unique hardware modeling language. The hardware modeling language is designed from a microprocessor-instruction point of view rather than from a signal point of view. The thesis discusses SOTEST’s design and implementation, and attempts to illuminate the key features that make SOTEST useful.

Extending a Generic Hardware Simulator with a

Multi-threaded Programming Language for Embedded

System Software Debugging

by

Kenneth James Duda

Submitted to the Department of Electrical Engineering and Computer Science

on May 18, 1993, in partial fulfillment of the requirements for the degrees of Bachelor of Science and Master of Science in Computer Science and Engineering

Abstract

The cost of developing software for embedded systems is currently very high. This thesis discusses a tool called SOTEST, which can dramatically reduce embedded system development time (and, therefore, cost). SOTEST makes possible the debugging of software for an embedded system on a workstation, without any of the hardware for the embedded system. The system includes a microprocessor simulator for the Intel 80186, a symbolic debugger that understands Microsoft debugging information, and an interpreter for a unique hardware modeling language. The hardware modeling language is designed from a microprocessor-instruction point of view rather than from a signal point of view. The thesis discusses SOTEST’s design and implementation, and attempts to illuminate the key features that make SOTEST useful.

Contents

1 Introduction 5

1.1 Current Development Strategies . . . . . . . . . . . . . . . . . . . . . 5 1.1.1 Laboratory-based Target Environment . . . . . . . . . . . . . 5 1.1.2 Workstation-based Target Environment . . . . . . . . . . . . . 6 1.1.3 Workstation-based Cross-environment . . . . . . . . . . . . . . 7 1.1.4 The Workstation-based Simulated Environment . . . . . . . . 8

2 SOTEST 11 2.1 80186 Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2 Symbolic Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.3 SOTL Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.4 Interactions between the 80186 Engine and SOTL . . . . . . . . . . . 12 2.5 Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

2.5.1 Scheduling Rules . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.5.2 Motivations for Scheduling Rules . . . . . . . . . . . . . . . . 16

3 Using SOTL 18 3.1 Writing Hardware Simulators . . . . . . . . . . . . . . . . . . . . . . 18

4 SOTEST Implementation 24

4.1 Event Dispatching . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.1.1 Breakpoint Events . . . . . . . . . . . . . . . . . . . . . . . . 24 4.1.2 Memory Read and Write Events . . . . . . . . . . . . . . . . . 25 4.1.3 Port Read and Write Events . . . . . . . . . . . . . . . . . . . 26

4.1.4 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.2 Idle Trapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

5 Conclusions 28

A SOTEST Reference Manual 29 A.1 SOTEST Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 A.2 Threads and Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . 30 A.3 SOTL Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

A.3.1 Primitive Types . . . . . . . . . . . . . . . . . . . . . . . . . . 31 A.3.2 Aggregate Types . . . . . . . . . . . . . . . . . . . . . . . . . 33 A.3.3 Booleans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

A.4 SOTL Identifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 A.5 SOTL Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 A.6 SOTL Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

A.6.1 SOTL Operators . . . . . . . . . . . . . . . . . . . . . . . . . 36 A.7 SOTL Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 A.8 SOTL Command Operators . . . . . . . . . . . . . . . . . . . . . . . 41 A.9 Events, Event Specifications, Event Predicates . . . . . . . . . . . . . 67

A.9.1 Event Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 A.9.2 Event Specifications . . . . . . . . . . . . . . . . . . . . . . . 70 A.9.3 Event Specification Constructors . . . . . . . . . . . . . . . . 71 A.9.4 Event Predicates . . . . . . . . . . . . . . . . . . . . . . . . . 73

A.10 UNIX System Calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

List of Figures

1-1 The Laboratory-based Target Environment . . . . . . . . . . . . . . . 6 1-2 The Workstation-based Target Environment . . . . . . . . . . . . . . 7 1-3 The Workstation-based Cross-environment . . . . . . . . . . . . . . . 8 1-4 The Workstation-Based Simulated Environment . . . . . . . . . . . . 9

2-1 SOTEST Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2-2 SOTEST Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

Chapter 1

Introduction

Developing software for embedded systems is a very common task in both industry and the military. Today, microprocessors are in everything from cars to televisions to kitchen appliances, and applications promise only to grow as hardware costs continue to fall. However, the cost of developing software for embedded system remains high because the embedded software development process is difficult.

1.1 Current Development Strategies

In a typical embedded software development project, each engineer has a workstation on which the software is written. There are also separate development environments in which the software is debugged and tested. The quality of these environments is directly linked to software engineer productivity. There are four basic types of environments: laboratory, workstation-based target, cross, and simulated.

1.1.1 Laboratory-based Target Environment

Because the target software must ultimately run on the target hardware, every project must set up an environment containing the actual target hardware. This environment, called the target environment, also must contain some facility for loading the target software into the target hardware. Depending on the nature of the project, other pieces of hardware with which the target interacts may be necessary. To make the

Figure 1-1: The Laboratory-based Target Environment

environment tolerable for the software developer, other tools may be added. One common and powerful tool is the in-circuit emulator, which plugs into the socket for the target’s microprocessor, emulates the microprocessor, and links to a workstation running a symbolic debugger. In addition, tools that allow inspection of external hardware devices or communication pathways can be useful. However, such tools are typically expensive to acquire and maintain. In general, the target environment suffers from two fundamental problems. The first is a lack of vision: it is very difficult to inspect the internal state of all of the various hardware components. The second is a lack of control: while it may be possible to suspend the microprocessor and single-step, other hardware components may not be so obliging, and may time out or otherwise behave differently than they would at full-speed. Similarly, while it may be possible to arbitrarily change the internal state of the microprocessor or its memory, it is generally impossible to arbitrarily change the internal state of external hardware components. These deficiencies can make it very difficult to isolate what are theoretically very simple bugs.

1.1.2 Workstation-based Target Environment

In an attempt to get a better handle on the embedded system, the workstation-based target environment requires that a project-specific hardware interface be built be-

Figure 1-2: The Workstation-based Target Environment

tween the target hardware and the workstation. Alongside an in-circuit emulator, this interface enables special software on the workstation to fully monitor and control the execution of the target, giving the developer control not only of the microprocessor but also of the various special-purpose hardware components on the embedded system. This environment has a higher startup cost than the laboratory environment — building the interface and the software to run it are not easy tasks. Furthermore, external hardware devices are every bit as hard to examine and control as before.

1.1.3 Workstation-based Cross-environment

In response to the difficulties of the target environments, a development team may set up a workstation-based cross-environment. In this environment, portions of the embedded software are compiled with the workstation’s native compiler, and are debugged using the workstation’s development tools. In this environment, engineers can quickly isolate bugs having to do with logic in high-level routines. The problems of control and vision are solved. However, two significant new limitations are introduced. The first is a reduced scope: code dealing directly with hardware, or written in a language not available on the workstation (usually assembly language), must be stubbed out. The second is reduced faithfulness: conditions having to do with timing, interrupts, hardware device handling, and so forth are so different than in the target

Figure 1-3: The Workstation-based Cross-environment

environment that there is a large class of bugs that appear in one environment but not the other. Nonetheless, without other alternatives, the gain in vision and control is worth the effort of getting the embedded software to run natively on the workstation.

1.1.4 The Workstation-based Simulated Environment

A third type of environment is the workstation-based simulated environment, based on a specialized program called a microprocessor simulator. The microprocessor simulator runs on the workstations, and can load the target software into workstation memory. It then simulates the target microprocessor, stepping instruction by instruction, decoding each instruction, and performing its effects. With a suitable user interface and symbolic debugging capabilities, the vision and control of the workstation-based cross-environment can be achieved. Because it simulates at a target-instruction level, the limited scope of the workstation-based cross-environment is overcome. Part of the faithfulness problem is solved as well; if care is taken when developing the simulator, it can simulate instruction timing and interrupt handling to sub-microsecond accuracy. However, a great problem for the simulated environment lies in handling external hardware. The target software must communicate with external devices; these devices are not connected to the workstation, and a generic microprocessor simulator knows nothing about them. There are two obvious ways to handle this

Figure 1-4: The Workstation-Based Simulated Environment

problem. First, two versions of the embedded software can be built: one for the target environment, and one meant to run under the microprocessor simulator. The latter would have low-level device-handling code stubbed out, replaced by routines that would feed the necessary results back to test other parts of the embedded software. In this case, the scope and faithfulness of the environment are reduced, as in the workstation-based simulated environment, and it is no longer clearly worth the effort to acquire the simulator. Another way to handle external hardware is to extend the generic microprocessor simulator with code to simulate external hardware components. This is a viable but difficult option. The source code for the simulator must be available. The specifications for the hardware are frequently unknown or change over the course of the project. The simulator is typically coded in a low-level language like “C” due to speed considerations, and “C” is not a particularly convenient language for implementing hardware simulation. Once the current project ends and the next begins, only the generic microprocessor portion of the simulator is still useful.

Another way to solve the problem of external hardware in a simulated environment is to equip the generic microprocessor simulator with a hardware modeling language. In addition to reading in the compiled embedded software, it could also read and interpret programs in a high-level language designed to describe the interaction between a microprocessor and external hardware. If this language is implemented well, it can be ported from simulator to simulator, surviving not only many projects but also many microprocessor types. A simulator equipped with such a language gives the vision and control of the workstation-based cross-environment, the full scope of the target environment, and, when programmed properly, can be made as faithful to the target environment as is desired. SOTEST is a successful example of such a system, a combination of a microprocessor simulator (with a symbolic debugger) and hardware modeling language.

SOTEST

SOTEST consists of three main components: a generic Intel 80186 microprocessor simulator, a symbolic debugger, and an interpreter for SOTL, a general Turingequivalent high-level language designed for hardware modeling. Of course the pieces must interface to one another in a variety of ways, but the boundaries are sufficiently well-defined to make it possible to change to a different microprocessor type with a reasonably low investment of effort.

2.1 80186 Engine

The 80186 interpreter works at an instruction level. It makes no attempt to simulate individual bus cycles, logic levels on 80186 pins, and so forth. Instead, it represents memory with a large array, remembers the values of registers and flags, and keeps track of the state of other internal components of the 80186 (including its built-in interrupt controller, timer block, and DMA controller). It reads instructions from its memory array, decodes them, and performs the operation required, updating its internal state appropriately. In addition, it knows how long each instruction takes to execute on a real 80186, and keeps a running total of the amount of time (in clock cycles) that a real 80186 would have spent executing the software.

2.2 Symbolic Debugger

The symbolic debugger contains facilities for loading Intel absolute object module files, interpreting Microsoft symbolic debugging information, setting breakpoints, displaying stack frames, displaying the values of variables, locating positions in source files, and so on. It has essentially the same facilities as any standard symbolic debugger, such as dbx or gdb. It is part of the 80186 interpreter in the sense that it is heavily 80186-dependent, but it is tied to the SOTL interpreter insofar as symbolic debugging commands can be issued by SOTL programs.

2.3 SOTL Interpreter

The other major component of SOTEST is the SOTL interpreter. SOTL is a blockstructured, high-level, multi-threaded, event-driven language designed for hardware modeling at an instruction (rather than signal) level. It is very rich, providing subroutines, local variables, automatic memory management, dynamic strings and arrays, regular expression matching, and access to certain host system calls, as well as operators that access and manipulate the 80186 simulation and access to the symbolic debugging facilities. A full reference manual for the language is included in the appendix.

2.4 Interactions between the 80186 Engine and

SOTL

The model chosen for interactions between the 80186 engine and SOTL threads is an event-driven model. The 80186 engine generates events, which can be received by interested SOTL threads. There is an event generated whenever an instruction is fetched (breakpoint event), the count of clock ticks reaches a certain value (breaktime event), a memory location is read or written, or an I/O port is read or written. Each SOTL thread specifies a set of events that it’s interested in. Typically, a thread Figure 2-1: SOTEST Architecture

14 is modeling an external device which is, for example, mapped into the processor’s memory at a certain range of addresses. The thread modeling this device would first subscribe to memory read and write events over that range of addresses, and then block (go to sleep). When a relevant event is received, the thread would wake up, examine the event, and act accordingly. Actions to take could include displaying messages, modifying internal state, or writing values into simulated memory. A return value can be provided in response to a memory read event, so that the thread can specify the value that the processor should see. The user may also subscribe to events, and will be notified when an event matching his subscription is received.

Events actually have sources other than the 80186 engine. A thread can ask for events when a UNIX file descriptor or message queue becomes readable, when a message is sent from another thread, or when another thread exits.

2.5 Scheduling

There are three things that SOTEST might be doing at any given time. It could be running 80186 instructions, running SOTL threads, or reading commands from the user. How to prioritize these duties is subtle and important. The rules governing scheduling of these jobs are somewhat complicated. I will first state them, and then explain their motivation.

2.5.1 Scheduling Rules

SOTEST has three states as diagrammed in Figure 2.5.1: waiting for a command from the user, running threads, and simulating 80186 instructions. SOTEST begins in the first state. When the user types a command, the command is parsed and run as a thread. This thread is special, and is called the foreground thread; all other threads are called background threads. SOTEST then changes into the state in which it runs the threads. The order in which individual threads are run is unpredictable and generally not relevant. It will stay in this state until all background threads are blocked, and the foreground thread is either blocks or exits (or is killed). If

Figure 2-2: SOTEST Scheduling

the foreground thread is gone, it resumes waiting for commands. If the foreground thread blocks, then SOTEST runs 80186 instructions. It does this until an event is generated which unblocks a thread, in which case it resumes running threads, or until the foreground thread spontaneously exits, in which case it resumes reading commands. The foreground thread spontaneously exits if the user presses control-C, or if the user receives an event.

It is possible that more than one thread will have an event in its event queue at the same time. In this case, the system must make a decision about which thread to run first. In SOTEST, events are not processed in any particular order; if events are waiting for more than one thread, the choice of which thread to run is arbitrary. While it is possible to construct a program that depends on the order in which events are received, we have been unable to come up with a useful program of this type. Of course, all events generated by a single instruction are processed before the next instruction is interpreted, so it will never be the case that an event for a given instruction is processed after an event for a subsequent instruction.

2.5.2 Motivations for Scheduling Rules

Clearly SOTL threads must have priority over the 80186 engine. When an 80186 instruction performs, for example, a memory read, which causes an event to be sent to a thread, the engine must wait for the thread to calculate the value that should be read before it can proceed. In general, therefore, if any thread is unblocked then it his priority over the 80186 interpreter. Thus, the first rule is that all threads must block or exit before any instructions will be interpreted.

Remaining scheduling semantics are motivated by the intended use of foreground and background threads. Background threads are intended to be doing hardware simulation. If they have computation to do, it is likely because the external hardware they are simulating needs to update its state. The thread blocks when this state is determined, and it is waiting for interaction with the microprocessor. If a thread needs to spread a computation over 80186 time, it can explicitly block until the clock advances a sufficient amount. Since the threads simulate relatively independent hardware units, the threads’ computations are generally independent as well, and so it does not matter which thread runs first if multiple threads have queued events.

The foreground thread, on the other hand, is treated specially because it is meant to be performing a simple action on behalf of the user; it represents the user’s intentions. The user probably wants this action to complete before entering another command. If this action causes other threads to run, then we probably want them to run to completion before allowing the user to enter another command. If this action blocks, then the user probably wants the 80186 interpreter to run. For example, the “go” command is implemented as an infinite block. It carries out the user’s intentions of relinquishing control and letting the simulation run.

Breakpoints

In SOTEST’s model of simulation, the users “sets a breakpoint” by subscribing to breakpoint events at the desired code location. When the code reaches the given location, the breakpoint event is sent to the user. Whenever the user receives an event, the event is displayed and the foreground thread is killed. Then, after all background threads block (they may have subscribed to the same breakpoint event), control returns to the user.

Control-C Interrupts

If the user presses control-C, the foreground thread is immediately killed. If SOTEST was simulating instructions at the time, control is returned immediately to the user. If SOTEST was running threads, the background threads continue to run until all block (or exit); only then is control returned to the user. The user must take care not to allow a background thread to get stuck in an infinite loop. If a background thread is under development, the user may place the thread in “debug mode”. When the user presses control-C, all threads in debug mode become “frozen” (blocked until granted permission to run by the user). When all other threads block, threads being debugged can have their state examined, and may be unfrozen or killed.

Using SOTL

The SOTL programming model, writing many relatively independent threads which are usually blocked waiting for events, turns out to be very powerful in a workstationbased simulated environment. In addition to providing convenient mechanisms for simulating common types of external hardware, the same model facilitates writing status monitors, test scripts, and interfaces to other host programs.

3.1 Writing Hardware Simulators

SOTL is very well-suited to hardware modeling. A typical external hardware device communicates to the microprocessor through memory-mapped addresses or I/O ports, can signal interrupts, can perform actions based on internal timers, and can directly access processor memory (DMA). All of these hardware mechanisms can be easily simulated through appropriate SOTL primitives and events. Further, the autonomy of threads in SOTL is analogous to the autonomy of hardware components in an embedded system. Specifically, it is typically very convenient to implement a simulator for a given piece of external hardware as a thread. If this piece of hardware is memory-mapped or port-mapped, the thread can subscribe to memory/port read/write events. If it has internal timers, the thread can subscribe to clock events. SOTL has primitives for interacting with the microprocessor in all the ways that external hardware components can: signalling interrupts, reading and writing memory, and manipulating the DMA controller and timer block.

As a simple example, here is a SOTL procedure which, when run as a thread,

implements an memory-mapped 7-segment LED. When the program writes a byte

to memory location 0xe000:0x100, the user is informed that the LED changed value.

Please refer to Appendix A for a detailed explanation of the language.

def $seven_segment_simulator ()
{

# loop forever, waiting for events

while (1) {

# wait for a write to location 0xe000:0x100

blockuntil memorywrite (0xe000:0x100);

# we’ve got one. Find out the value written, # and whether it was a byte or word write

if (memorywrite($addr, $value, $size)) {
if ($size == 2) {
print "Word writes to the LED not allowed!";
breaknow;
} else {

# If bit 7 is set, then the decimal point is lit

$dot = $value & 0x80 ? "." : " ";

# Figure out which digit it is from remaining bits

$val = $value & 0x7f;
if ($val == 0x7f) {
$digit = "8";
} elsif ($val == 0x77) {
$digit = "0";
} elsif ... {
...
} else {
$digit = "unknown";
}
print "LED changes to ", %b $value,
"(", $digit, $dot, ")";
}
}
}
}

As a more interesting example, consider this implementation of an asynchronous

64k-BPS serial port. The transmit and receive register is port-mapped at address

0x3f8. A status register at 0x3f9 indicates receive ready (bit 0), receive overrun (bit 1), transmit empty (bit 2), and transmit overrun (bit 3). Reading the status register

clears the overrun bits. It signals an interrupt when the transmit register becomes

empty or a byte is received. When the code under test sends a character, it is printed

on the screen. When the user wants to send a string of characters in, he calls the

fuction $serial port kick recv(“thestring...”) defined below.

Here is the SOTL code that implements the simulated serial port:

$serport_incoming = "";
$serport_next_xmit = 0;
$serport_next_recv = 0;
$serport_xmit_overrun = 0;
$serport_recv_overrun = 0;
$serport_data_taken = 0;

$serport_speed = 1000; # 1000 clock ticks per byte. This is a # rate of 16k per second, or 64 kilobaud, # for a 16 Mhz processor.

def $serport_simulator ()
{

# Loop forever, waiting for events

    while (1) {

# Wait for write or read to our registers, # or until transmit/receive time is over

        blockuntil portwrite(0x3f8, 0x3f9),
   portread(0x3f8, 0x3f9),
                   breaktime($serport_next_xmit),
                   breaktime($serport_next_recv);

# Handle event

        if (portwrite($port, $value, $size)) {

# Handle write to transmit port

            if ($size == 2 || $port == 0x3f9) {
                print "Bad write to serial port!  ", %w $port;
                breaknow;
            } else {
                if ($serport_next_xmit) {  # transmitting?
                    print "Serial port transmit overrun",
                        " transmitting ", %b $value;
                    $serport_xmit_overrun = 1;
                } else {
                    print "Serial port writes ", %b $value;
                }
                $serport_next_xmit = clock + $serport_speed;
            }
        } elsif (portread($port, $size)) {

if ($size == 2) {
print "Cannot read words from serial port!";
breaknow;
} elsif ($port == 0x3f8) {

# Handle read from receive port

if ($serport_data_taken) {
    print "Repeated serial port data read";
}
$serport_data_taken = 1;

# Make receieved character available

outbyte 0x3f8, $serport_incoming[0];
print "Serial port data read: ",
    %b $serport_incoming[0];
} elsif ($port == 0x3f9) {

# Handle read from status port

$status = 0;
if (length($serport_incoming) > 0 &&
    !$serport_data_taken)  { $status |= 1; }
if ($serport_recv_overrun) { $status |= 2; }
if (!$serport_next_xmit)   { $status |= 4; }
if ($serport_xmit_overrun) { $status |= 8; }

# Reading the port clears these bits

$serport_recv_overrun = 0;
$serport_xmit_overrun = 0;
outbyte 0x3f9, $status;
}
} elsif (breaktime($when)) {

# Handle timed event

if ($when >= $serport_next_xmit) {

# Finished sending character

$serport_next_xmit = 0;

# Send interrupt to processor

interrupt 12;
}
if ($when >= $serport_next_recv) {

# Finished receiving a character

$serport_incoming =
    substr($serport_incoming, 1);
if (length($serport_incoming) == 0) {
    $serport_next_recv = 0;
} else {

# Send interrupt to processor

    $serport_kick_recv(0);
}
}

               }
           }
       }
       def $serport_kick_recv($str)
       {
           if ($str) { $serport_incoming = $str; }
           interrupt 12;
           if (!$serport_data_taken {
               $serport_recv_overrun = 1;
           }
           $serport_next_recv = clock + $serport_speed;
           $serport_data_taken = 0;
       }

As this example makes clear, a reasonably sophisticated piece of hardware can be

adequately simulated with very little effort.

Another useful feature of SOTL is the ease with which status monitors can be

written. A status monitor is a SOTL program, usually running as a single thread,

which gives the engineer a view of some aspect of the embedded software’s execution.

It might, for example, monitor whenever a certain procedure is called or when a

certain variable changes value. The event mechanism provides support for status

monitors. A status monitor can monitor what’s being executed by subscribing to

code location events, can poll system status with clock events, and can find out

when variables are changed (or accessed) with memory read/write events. Here is

an example SOTL command that will cause a message to be printed every time a

variable named “x” changes:

       fork {
           while (1) {

# wait for memory write event at x’s address

               blockuntil memorywrite(&x);

# now write the value through to x

               memorywrite ($addr, $value, $size);

# and notify the user

               print "X changed to ", $value;
           }
       };

SOTL actually provides syntactic sugar that makes this sort of device more con-

venient to type. The following code is equivalent to the above code.

       whenever memorywrite(&x) { memorywrite ($addr, $value, $size);
                                 print "X changed to ", $value; }

If the user merely wants a breakpoint when “x” is written, the following is the easiest way:

       break memorywrite(&x)

SOTL is also good for writing test scripts. Test scripts need to provide some sort of stimulation to the embedded software and then decide whether or not the software did the right thing. A script runs well as a single thread, blocking either until it’s time to do the next step of the script or until some indication is received that the embedded software has reached a certain state.

SOTL also interfaces conveniently to other host programs. A thread responsible for communicating with another UNIX process, for example, can subscribe to file descriptor or message queue events. This ability to connect to external processes is more useful than it first seems. Applications include interfacing to regression test programs and interfacing to other types of simulators.

Regression test programs are written as standalone programs so they can run in the target environment or the simulated environment. In the laboratory environment, they communicate with various pieces of specialized hardware that stimulates the whole embedded system and verifies that its response is correct. In the workstation environment, regression test programs communicate with the SOTL interface, which in turn stimulates the embedded software in an analogous way. This way the same tests can be run in both environments; if a test fails in the target environment, the cause of the problem can often be tracked down in the simulated environment.

SOTEST Implementation

General techniques for implementing each of SOTEST’s major components, namely, the hardware modeling language, the microprocessor simulator, and the symbolic debugger, are well understood. One interesting aspect of SOTEST’s implementation is efficient event dispatching; another is efficient handling of time periods when the simulated machine has nothing to do (idle trapping.)

4.1 Event Dispatching

For each thread, SOTEST maintains a list of event specifiers that describe the class of events that the thread is interested in. Clearly the system would be terribly inefficient if it searched each thread’s event specifier list every time anything happened that can generate an event (there are typically two or three such occurrences for each simulated instruction: an instruction fetch and a memory read or write). SOTEST employs different strategies for different event types, in an ad-hoc attempt to squeeze as much performance out of the system as possible.

4.1.1 Breakpoint Events

SOTEST implements breakpoints without performance degradation of the 80186 interpreter. The 80x86 instruction set contains breakpoint (INT3) instruction, which causes a trap when executed. When the user of a native 80x86 debugger requests a breakpoint, the debugger actually stores the instruction into the code segment of the target program at the location indicated by the user. Then, when the program is executed, when it reaches that point, the 80x86 traps to the debugger. SOTEST uses a very similar strategy. When a thread requests a breakpoint event, the request is recorded in a list of breakpoints and the program text is modified. The interpreter’s main loop contains a jump table based on the opcode of the instruction; the jump table entry for the INT3 is a breakpoint handler, which scans the list of breakpoints generating the appropriate events. The threads are allowed to run, and, when the thread subsystem is quiescent, the instruction originally stored at the breakpoint address is executed.

One drawback of this technique is that the program code modifications need to be temporarily undone when the user, for example, unassembles the program near the breakpoint; in addition, attempts to set breakpoints at locations where self-modifying programs write instructions will cause unpredictable behavior. However, the ability to support breakpoints without slowing down the 80186 interpreter’s general case was too attractive to pass up.

4.1.2 Memory Read and Write Events

It is assumed that for most applications, the number of subscriptions to memory read events is fairly small, and that these subscriptions are likely to be localized. This makes sense because system architects generally partition the 80186’s address space into a large region for RAM and ROM and a small region for memory-mapped external hardware. To attempt to implement memory read and write events efficiently, SOTEST divides the simulated one-megabyte address space into 4096 256byte “pages.” A bit array contains two bits for each page, one which is set if some thread might be subscribed to read events for some locations in the page, and one for write events. On each memory read or write, SOTEST checks the bit corresponding to the page, and, if set, scans the list of memory read (write) subscriptions. If no subscription is found for the page, the bit is cleared. For each subscription in the list which contains the location in question, an event is sent to the corresponding thread.

This strategy slows down memory accesses only very slightly for normal memory pages (i.e., pages on which no subscriptions are registered.)

4.1.3 Port Read and Write Events

While it may seem that port reads and writes should be handled exactly like memory reads and writes, SOTEST in fact uses a much simpler strategy. On every port read or write, the entire list of port subscriptions is scanned. The reason for this is that port reads and writes are meaningless unless some thread is subscribed to the port; therefore, it is assumed that the vast majority of port reads and writes will result in some thread receiving an event. In this case, the cost of processing the event is so much greater than the cost of scanning the list that we felt there was no need for a more complex mechanism.

4.1.4 Conclusions

Efficient event distribution is an important requirement for attaining reasonable performance in SOTEST. Different mechanisms are used for different event types because of the different characteristics of their usage patterns. While no detailed performance measurements have been taken, SOTEST, running on a Sun SparcStation II, runs 80186 programs at approximately 5% of the speed they would run on a real 16 megahertz 80186; that is, they take 20 times longer to run on a Sparc. A factor of 20 slowdown ends up being acceptable for many applications, since most embedded systems are I/O-bound, not CPU-bound, most of the time.

4.2 Idle Trapping

A real embedded system is idle much of the time. Most systems are constructed to have some sort of idle loop, where the machine is doing some boring task, such as calculating a checksum of itself, waiting for an interrupt (timer or device) to wake it up. In a typical SOTEST simulation, the simulated target is being stimulated by interrupts generated at appropriate times by threads. For example, suppose an embedded system is set up to poll for events each time a one-millisecond timer interrupt is received. The SOTL simulation code might be set up as follows:

       fork {
           while (1) {
               blockuntil breaktime(clock + 16000);
               print "Sending poll interrupt";
               interrupt 8;
           }
       };

When the user starts the simulation, the target software will run, receiving an interrupt every millisecond. Once initialization is complete, the target software will be idle most of the time. Unless the user wishes to test the idle loop itself, time spent by SOTEST simulating the instructions in the idle loop is wasted. It would be much better for SOTEST to send interrupts as fast as possible, until the target software decides it has something to do. This is implemented by the machineidle command. This SOTL command declares that the current machine has nothing to do, and should go into idle mode. When in idle mode, SOTEST does not actually interpret instructions; instead, it spins the machine’s clock until a thread subscribed to a breaktime event wakes up. If that thread does something that would bring the target software out of the idle loop, then the machine leaves idle mode. Suppose our machine has a label called _IDLE which contains the idle loop. Then the user may write

       fork {
           while (1) {
               blockuntil _IDLE;
               machineidle _IDLE;
           }
       };

This enhancement saves tremendous amounts of simulation time for programs which are I/O-bound; it collapses time spent in I/O-wait to almost nothing. It brings no improvement for programs that are CPU-bound, but they are not slowed either.

Conclusions

SOTEST has proven to be a very useful tool for developing software for complex embedded systems. With a rather modest investment in writing coarse-grained hardware simulators in the SOTEST language (SOTL), the user gains control over every aspect of the environment in which the software runs. External hardware modules that were once opaque and difficult to control are replaced by models which are transparent and easy to manipulate. Timing-critical regions of code that were once impossible to single-step can be run at any speed. SOTEST provides a software development model which is independent of host or target CPU architecture, although the current implementation is limited to debugging 80186 programs on UNIX-like machines.

The key to SOTEST’s success is the integration of a coarse-grained hardware modeling language and a microprocessor interpreter. By simulating at the granularity of an assembly-language instruction, rather than sub-clock-cycle granularity employed by traditional hardware simulators, it becomes practical to use the simulator to test and debug the software rather than the hardware itself. SOTEST represents a significant advance in embedded system development technology.

Appendix A

SOTEST Reference Manual

A.1 SOTEST Structure

SOTEST consists of three major pieces:

• 80186 (or 80188) engine

• SOTL (thread language) interpreter

• Command-line interface

The first piece, the 80186/8 engine, is capable of simulating one or more 80186/8based “machines.” Each machine is represented by an array of memory, an array of ports, registers, flags, a clock, an interrupt controller, a timer manager, and a DMA controller. The chip select unit is not simulated, but there are other ways to achieve its effects. The engine simulates at the instruction level rather than at the logic level; that is, no 80186 pins are simulated and no logic-level signals are generated.

The second piece is the SOTL interpreter. SOTL is the name of the debugging command language, a multi-threaded procedural language that can be used to write test scripts, simulate external hardware, monitor the execution of the target, or perform frequently-used combinations of top-level commands. SOTL supports all of the standard C operators, has procedures and local variables (distinct from the target’s procedures and local variables), provides operators to examine and control the simulated 80186, provides access to the target’s debugging information (target local variables and type information), and provides access to certain UNIX system calls (in particular, interprocess communication via System V msg___calls or Berkeley sockets). In addition, threads can send messages to one another.

The third piece is the command-line interpreter. It is very simple; it reads a SOTL statement from standard input, creates a thread for it, and starts that thread running. When that thread completes, the user is prompted for another command.

A.2 Threads and Scheduling

When the user types a command, a thread is created for it. This thread is the “foreground thread,” and until it dies, the user cannot execute any more commands. There may only be one foreground thread at a time, but the foreground thread can fork (create) other threads. The user may kill the foreground thread by pressing control-C.

Certain commands can cause the foreground thread (or any other thread) to block. Blocking means the thread says it has nothing more to do, for the time being at least. Receiving an event is the only thing that causes a thread to unblock. Before a thread blocks, it specifies which events it’s interested in; a read from a certain range of memory addresses, for example, can cause an event. There are several other ways events can be generated, which will be discussed later. The user may also subscribe to events; when the user receives an event, the event is displayed on the screen, and the foreground thread is killed.

When the foreground thread dies, either by exiting, when the user receives an event, or when the user presses control-C, all other threads (background threads, which may have been forked from the foreground thread) are then allowed to run until they block or exit. Finally, the user is allowed to type another command, starting a new foreground thread. If a background thread is misbehaving, and simply runs continuously, there is no way for the user to stop it. However, a thread can be put into debug mode. When the user presses control-C, all threads in debug mode are frozen, and the user may type commands. The threads are unfrozen either explicitly by the user (using the threadunfreeze command), when they are taken out of debug mode, or when the user uses the go command.

When the foreground thread is blocked, and all other threads are blocked or frozen, the engine starts interpreting machine instructions until an event is generated or the user presses control-C. When the machine generates an event for a thread, if the thread is not frozen, it will receive the event and unblock. If the thread is frozen, the event is entered onto an event queue for the thread, which will be processed when the thread is unfrozen. If the thread is in debug mode, a warning message is printed informing the user about the event.

Runnable threads have highest priority in the system. As long as any thread has something to do (i.e., it is neither blocked nor frozen), it runs, and all machines are effectively halted; registers and the clock hold their values. Further, user input is ignored. However, the user can always take control by pressing control-C. This interrupts whatever SOTEST is doing (executing 80186 instructions or running threads), kills the foreground thread, and marks threads in debug mode (whether blocked or running) as frozen. All threads have the same priority; if several become runnable at once (through being unfrozen or receiving events), which runs first is apparently random.

A.3 SOTL Types

A.3.1 Primitive Types

There are many primitive data types in SOTL. The simplest is the dword, which is an unsigned number from 0 to 2^{32} − 1. A dword constant is like an integer constant in C, except that Intel hexadecimal notation is permitted (e.g., 0d73h). Time is a primitive type, measured in ticks (clock cycles). A time value is created with the syntax minutes:seconds.ticks, where minutes, seconds, and ticks are integer constants. For example, 1:23.13000000 represents one minute, 23 seconds, and 13 million ticks.

The string is a primitive type; like in C, a string constant is an array of characters in double-quotes, e.g., "hello". Unlike in C, strings are not null-terminated; their length is recorded, and they may contain any characters. Strings can also be constructed from hex by using backquotes as in `1A2B3C4D`, in which whitespace is allowed. The string constant `41 42 43` creates the string "ABC". Strings can be accessed with array reference operators; "ABC"[1] returns 66 (the ASCII code for B).

There is also a primitive type that represents a block of target memory. This type is called a “block,” and is created with the syntax [address,size]. If the size is 1, 2, or 4, then the block can be used anywhere a dword can be used; when the dword is needed, memory at the address is read (generating memory read events.) The memoryfetch command creates a string whose characters are the contents of the block. Blocks can also be displayed conveniently with the print command. For example, x=[1234h:5678h,4] assigns to x a reference to 4 bytes at address 1234h:5678. Then x+10 reads a dword from that address and adds 10 to it, print %b x prints out the 4 bytes stored there, and y=memoryfetch x creates a string of length four containing those bytes and assigns it to y.

The pattern (or regular expression) is a primitive type. Strings can be tested to see if they can be see if they match a pattern; a pattern matches a set of strings. For example, you can write a pattern to match all strings that start with `03 04 05` and end with `A6`; see the =~ and !~ operators. The syntax for constructing patterns is described below.

Other primitive types include source code modules, machines (processors being simulated), user-defined procedures (called SOTL functions), and references and pointers to target data objects (using target debugging information). UNIX file descriptors and message queue ID’s are also primitive types. One final type is the null type; the only null value is nil, which is returned by commands that have no meaningful return value. It is also placed anywhere that would be uninitialized in C (such as in otherwise uninitialized arrays).

A.3.2 Aggregate Types

The array is the only aggregate type. The lower index of an array is always zero. Arrays grow on demand; accessing an element past the end of an array causes the array to grow. The untouched elements of the array are all initially nil. The elements of an array may be of any type; they need not be all the same type. Although multidimensional arrays are not directly supported, elements of arrays may be other arrays. However, if you set up a cyclical structure (for example, with x[0] = 0; x[2] = x;) then SOTL’s memory manager will never free the array(s) involved in the cycle.

Arrays need not be declared. If, before storing anything into a variable, you use the array reference operator on it, the variable becomes an array.

   Example: a[10] = 17; a[13][4] = 26; print a[3]; ==> nil

A.3.3 Booleans

SOTL does not have a boolean type. Similar to C, any non-zero dword is considered true, and 0 is considered false. A string of positive length is considered true, and the empty string is considered false. All values of other types are considered true besides nil, which is considered false.

A.4 SOTL Identifiers

Identifiers are like C identifiers, except that dollar-sign ($) is allowed within identifiers. It is recommended that you use a dollar-sign as the first character of all SOTL variables, in order to avoid accidental naming conflicts with target symbols.

The rules SOTL uses to find the values of identifiers are not simple. This is because SOTL has three namespaces, all of which are in effect simultaneously. There are SOTL variables and objects (global and local), debugging symbols (global and local), and public symbols and segment names. The procedure SOTL follows when looking up an identifier follows. First, locals for the current SOTL function are checked, and SOTL globals are checked. Then, debugging symbols for the current frame are searched. These are the only three context-dependent cases. There is a cache mapping identifiers to their context-independent values; this cache is checked next. If there is nothing in the cache, then the following lists are checked; if anything is found, it is stored in the cache. These lists, in order, are SOTL globals, usersupplied SOTL extension function names, machine names, account names, register names, public symbols, debugging globals (including module-global static variables), and segment names.

If an identifier is not found, and it is assigned to, then a SOTL variable is created. If the SOTL code is executing inside a SOTL function, the variable will be created locally to that function; otherwise, it is created globally. If you want to create a SOTL variable explicitly, you can use the local or global command operators. It is recommended that you always use these operators when creating variables. Here is an example of what can go wrong if you don’t:

        def $foo ($x) {
            $temp = $x;
            if ($x > 0) { foo ($x - 1); }
            print $temp;
        }
        $foo(2);  → 0  1  2
        $temp = 10;
        $foo(2);  → 0  0  0

Notice that the assignment to $temp on line 7 made $temp global, causing the recursive $foo to do something very different than intended. The problem is fixed by inserting local $temp; after the first line.

A.5 SOTL Patterns

Patterns are always found between pairs of forward-slashes (/). Inside the slashes, period (.) matches any byte, and dollar-sign ($) matches the end of the string. Other elements may be string constants or SOTL variables that hold strings. So, for example,

           `001122` =~ /`00 11 22`/ returns true,
           `001122` =~ /.  `1122` $/ returns true,
           `001122` =~ /`0011`/  returns true,
           `001122` =~ /`0011` $/  returns false, and
           `001122` =~ /.  .  .  ./ returns false.

Operators within regular expressions include question-mark (?), which makes the preceeding term optional; star (*), which allows the proceeding term to match 0 or more times; plus (+), which allows the proceeding term to match 1 or more times; and vertical-bar (|), which matches if either the term on its left or the term on its right matches. Parentheses (( and )) group terms together. Finally, a string in brackets ([ and ]) matches any single character which is in the string; if the string is prefixed with caret (^), it matches any single character not in the string. Thus,

           `00112233` =~ / (.*) `22` .  `66`?  $/ returns true,
           "aaad" =~ / "a"+ "b"* "c"?  "d" $/  returns true,
           "@q@q" =~ / (`40` "q")+ $/  returns true,
           "wxyz" =~ / "w" ("xy" | "yx") "z" /  returns true,
           "abccbd" =~ / "a" ["bc"]+ "d" /  returns true, and
           "abccbd" =~ / [^"bc"] "bccb" [^"bc"]  returns true.

After a successful match, substrings of the matched string can be extracted. The variable $0 holds the portion of the string that matched the regular expression, $1 holds the portion of the string that matched the first parenthesized subexpression in the regular expression, $2 holds the second, and so on. Thus, after the first of the above examples, $0 holds `00112233` and $1 holds `0011`. After the match

             "abcbcbcdddde" =  / ("a" ("bc" *)) ("d"+) /
returns true, $0 holds "abcbcbcdddd", $1 holds abcbcbc, $2 holds "bcbcbc", and $3

holds "dddd". This behavior is useful for parsing.

A.6 SOTL Expressions

SOTL expressions contain operators, identifiers, integer constants, string constants, and time constants. Operators include most of the C operators, and also a considerable set of keyword prefix operators, called “commands.” Each command can be abbreviated when it is the first thing on a line. A description of each command appears below.

A.6.1 SOTL Operators

SOTL supports most C operators, and uses the same precedence rules. However, some operators are overloaded and have different meanings in different contexts, for the convenience of the SOTL programmer. In the following section, x, y, and z represent expressions (which presumably contain no operators of lower precedence than the ones being demonstrated.) Precedence rules are the same as in C. The operators are presented here in order from low to high precedence. All standard C prefix operators actually have the same precedence, and all postfix operators all have the same (highest) precedence. There are a few infix operators that are higher precedence than the prefix operators.

Construction x = y

      x += y
      x -= y
      x *= y
      x /= y
      x %= y
      x >>= y
      x <<= y

Meaning y is evaluated. In the first case, the result is assigned to x; in the second, it is added to x; and so on.

Construction x ? y ! z

Meaning x is evaluated. If it is true, then y is evaluated and returned; otherwise, z is evaluated and returned. Note that ! is used instead of :. This is because : already has several other meanings, and overloading it further is not feasible.

Construction x || y

Meaning x is evaluated. If it is true, it is returned. Otherwise, y is evaluated and returned. Note that this is more like the Lisp meaning of the or operator than the C meaning; in C, the return value is always 0 or 1.

Construction x && y

Meaning x is evaluated. If it is false, it is returned. Otherwise, y is evaluated and returned.

Construction x | y

Meaning x and y are evaluated and their bitwise-or is returned.

Construction x ^y

Meaning x and y are evaluated and their bitwise-xor is returned.

Construction x & y

Meaning x and y are evaluated and their bitwise-and is returned.

Construction x == y x != y x =~ y x !~ y

Meaning x and y are evaluated. In the first two cases, == and !=, if they are dwords or time values, they are compared numerically. If they are strings, they are considered equal if they have the same length and each character is the same. For ==, if they are equal, then 1 is returned; otherwise, 0 is returned. Conversely for !=.

In the second two cases, x must be a string and y must be a pattern. For =~, true is returned if the string matches the pattern, and false otherwise. Conversely for !~.

Construction x <= y x >= y x > y x < y

Meaning x and y are evaluated and compared. They must be dwords or time values. If the comparison is true, then 1 is returned; otherwise, 0 is returned.

Construction {x + y \atop x - y}

Meaning x and y are evaluated and added (or subtracted). They must be dwords or time values. If only one is a time value, the result is another time value; when promoting a dword to a time value, the dword is taken to be a number of ticks.

Example  100 + 1:2.3  → 1:2.103
Construction x : y

Meaning If x is a dword and y is a dword, then another dword is returned using the formula (x << 16) + y (this is for segment:offset notation). If x is a dword and y is a time value (usually in the form a.b), then a time value x : a.b is created (this is for time value constants). If x is a source code module and y is a dword, then the address of line y of module x is returned (this is for line number references). If x is a machine and y is a symbol, then the address of symbol y in machine x is returned (making it possible to override the current machine when a symbol is being looked up).

Construction x * y x / y x % y

Meaning x and y are evaluated, and their product, quotient, or remained of division is returned.

Construction + x

Meaning An address x bytes from the top of the current function is returned.

Construction : x

Meaning The address of line x in the current module is returned.

Construction ~ x - x ! x

Meaning The binary, two’s complement, or boolean negation of x is returned.

Construction {& x \atop * x}

Meaning These are pointer operators as in C. Note that x must be a reference to a target data object in the first case, and a pointer to a target data object in the second. You may not take the address of a SOTL object; SOTL itself does not have a pointer type. These operators can only be used when target debugging information is available.

Construction {++ x \atop -- x}

Meaning x is evaluated; the result is incremented (or decremented), stored back in x, and returned.

Construction x -> y

Meaning x is evaluated, and must be a pointer to a target data object that is a structure. y must be a single indentifier, and is interpreted as a field of the structure. A reference to the field is returned.

Construction x . y

Meaning x is evaluated. If x is a reference to a target data object that is a structure, then y must be a single indentifier, and is interpreted as a field of the structure; a reference to the field is returned. If x is a string, then y must be a string, and their concatenation is returned. Otherwise, x and y must be dwords, in which case a time object with x seconds and y ticks (0:x.y) is returned.

Construction x [ y ]

Meaning x is evaluated, and must be either a SOTL array, a SOTL string, a reference to a target data object that is an array or a pointer, or a pointer to a target data object. y is evaluated, and must be a dword. If x is a SOTL array, a reference to the SOTL object stored an the array is returned. If x is a SOTL string, the byte stored in that position of the string (or -1 if y i out of range) is returned. If x is a target array, a target pointer, or a pointer to a target object, a reference to the appropriate target data object is returned.

Construction x ( arg1, arg2, ...  )

Meaning x is evaluated, and must be a SOTL function. The arguments are all evaluated, and the values are passed to x. The result of the final evaluation in x is returned.

Construction {x ++ \atop x --}

Meaning x is evaluated and its value is returned. Then, that value is incremented (decremented) and stored back in x.

Construction .

Meaning The current value of CS:IP is returned.

Construction _

Meaning The top of the current function is returned. This is the same as + 0.

Construction {[ x ] \atop [ x , y ]}

Meaning An object representing a block of target memory is created. The address is x, and the size is y if specified, and 4 if not. If the size is 1, 2, or 4, then this reference promotes automatically into a dword whenever a dword is needed. This promotion will generate a memory read event. Blocks are useful with the print command (see below) and can also be converted into strings.

Example  enterbyte 100, 123; x = [100, 1]; print x, " = ", x + 0;

→ [0000:0064 , 1] = 123

Construction ( x )

Meaning x is evaluated and returned. This construction exists to override operator precedence.

Construction { statement; statement; ...  }

Meaning The statements are executed and the result of the final statement is returned.

Example  x = { while (1) .....  };

A.7 SOTL Statements

There are very few statements in SOTL. As in C, most things that you might think of as statements (such as printing things, setting breakpoints, and so on) are actually expressions. There are, in fact, only five statements, and two are trivial.

Statement expression;

Meaning Evaluate the expression. If this is the last statement of a procedure, then its value is the return value for the procedure. Otherwise, the value is thrown away.

Statement ;

Meaning This is the empty statement, and has no effect.

Statement if expression { statement; ... } if expression { statement; ... } else { statement; ...} if expression { statement; ... } elsif expression ...

Meaning The expression is evaluated. If it is true, the first block is evaluated and its final value returned. Otherwise, there are three cases. If there is an else clause, then the else block is evaluated and its final value returned. If there is an elsif clause, it is treated just like an if. If there is no clause, then nil is returned. Unlike in C, the then-clause and else-clause must be grouped in curly-braces, even if there is only one statement.

Statement while expression { statement; ...  }

Meaning The expression is evaluated. If it is true, the block is evaluated and the expression is checked again. If it is false, its value is returned. Note that while statements always return false.

Statement def name ( arg_{1}, arg_{2}, ...  )  { statement; ...  }

Meaning This statement creates a SOTL function and assigns it to the given name. This statement always returns nil.

A.8 SOTL Command Operators

SOTL commands are actually prefix operators. Unlike the other (more typical) prefix operators like ++, which operate only on the term immediately on their right, SOTL prefix operators can grab several whole expressions separated by commas. They have the lowest precedence of any operator.

Many commands operate on a machine. While a few commands explicitly take a machine as an argument, most commands work on the “current machine.” Each thread has its own notion of what the “current machine” is; the current machine is preserved through procedure calls.

Many command operators can be abbreviated. These abbreviations are only recognized if the command is the first non-whitespace item on a new line. Abbreviations are meant to be used only at top-level, although they may be used in SOTL source files as well.

Many command operators take one or more arguments. These arguments may optionally be enclosed by parentheses. But beware: some examples should make clear the unexpected results this convenient syntax can sometimes cause.

Expression  Meaning
foo 2,3  Apply the foo operator to 2 and 3
foo (2,3)  Same as above

foo (2,3) + 4 Apply the foo operator to 2 and 3, and add four to the

result

(foo 2,3) + 4 Same as above

foo 2,3 + 4 Apply the foo operator to 2 and 7. This may not be

what you meant; you probably meant the above.

foo 1+2*3  Apply the foo operator to 7
foo (1+2*3)  Same as above
foo ((1+2)*3) Apply the foo operator to 9

foo (1+2)*3 Apply the foo operator to 3, and multiply the result by

3. This is certainly not what you meant!

As you can see, this is tricky. If you want to be safe, always parenthesize the intended arguments of command operators.

Command break Abbreviated b

Example  break _main, 0:10.23, memorywrite(_foo, _foo+20)

Description Allows the user to specify what events are of interest. When one of these events is received, it causes a “break.” A message describing the event is printed, the foreground thread is killed, all freezable threads are frozen, and control is returned to top-level as if the user pressed control-C. break takes a comma-separated list of event specifications as its argument; see the section on event specifications for more information.

Command breaklist

Abbreviated bl Description Lists the events specifications that will cause breaks. They are listed with

numbers that can be used to refer to them. This command takes no arguments.

Command breakdelete

Abbreviated bd

Example  breakdelete 2, 3

Description Takes a comma-separated list of numbers as its argument, and deletes the

event specifications with those numbers.

Command breaknow

Abbreviated bn Description Allows a thread to signal a break. When any thread executes this command,

a message is printed, the foreground thread is killed, all freezable threads are frozen, the user is returned to top level.

Command backtrace

Abbreviated bt Description Divides the stack of the current machine into its frames, and for each frame

prints the name of the procedure and the values of its arguments. Frame 0 is the innermost (deepest) frame; frame 1 is the frame of frame 0’s caller, frame 2 is frame 1’s caller, and so on. This command is only avaiable if the target has been compiled with debugging information. It takes no arguments.

Command blockuntil

Abbreviated bu

Example  blockuntil messagereceive, portread(0x400)

Description Has two functions. First, it subscribes to events the current thread is interested in receiving. Second, it blocks the thread. When an event matching one of the event specifications is received, the thread will unblock. Like break, its arguments are event specifications.

Command call Abbreviated c

Example  call _printf

Description Initiates a target procedure call. The registers and flags are saved, CS:IP is set to the given address, and the thread blocks until the call returns. When the target function returns, the thread unblocks, and the value of the call operator is the value of DX:AX. Then the registers and flags are restored. This operator is a hack. Do not use it if you can think of any way around it. If the thread dies before the call returns, chaos ensues, and SOTEST will probably crash. I will probably either fix this soon, or delete the call mechanism.

Command chdir Abbreviated cd

Example  chdir "/tmp"
Example  print chdir

Description With a string argument, it changes SOTEST’s notion of the current working directory. In any case, the current working directory is returned as a string; the second example above prints the current working directory.

Command clockclear

Abbreviated cc Description Clears the value of the clock for each machine. It also sets the record of the

amount of host CPU time used by SOTEST to 0.

Command clock Description Returns the value of the clock for the current machine. Command copy Abbreviated cp

Example  copy _my_data, _your_data, 100

Description Copies memory from the first address given to the second addres given; the third argument is the number of bytes to copy. Overlapping copies are supported. No memory write events are generated for the copy.

Command defined

Abbreviated df

Example  defined $my_symbol

Description Tests if the given symbol is defined. If it is defined in any way — as a SOTL variable, a target local symbol, a machine register, or the name of a code module, for example — the operator returns 1. Otherwise, it returns nil.

Command dmarequest

Abbreviated dr

Example  dmarequest $channel, 1

Description Sets the value of the DMA request line (DRQ0 or DRQ1) for the given channel, which must be 0 or 1. The second argument must be true or false. If a DMA channel is synchronized, then DMA is allowed only if this value is true. This value is not used if the DMA channel is unsynchronized. See documentation about the 80186 family of processors for more information on the DMA controller.

Command down Abbreviated dn Description Moves down one stack frame. This is available only if the module was compiled

with debugging information.

Command dump Abbreviated d

Example  dump es:0x1234

Description Displays 128 bytes of memory starting at the given address. If the address is omitted, the next 128 bytes are displayed. No memory read events will be generated for the memory displayed. If a memory location is under examination by one or more threads, a ? will be displayed after the value, meaning that the value displayed is not necessarily what an instruction reading the location would see. In this case, the value displayed is merely the last value written to the address.

Command enterbyte

Abbreviated eb

Example  enterbyte 0x1234:10, 1, 2, 3, 4

Description Enters bytes into memory. The first argument is the starting address; the remaining arguments are bytes to enter, each at the next address. Memory write events are generated for these writes, but will not be processed until the thread blocks.

Command eval Abbreviated ev

Example  eval ev = 10, pdq = 20

Description Evaluates a list of expressions. This takes the place of the C comma operator. Also, it works around the rare case when you want to begin an expression whose first term is a command abbreviation. Note that while the command eval g = 10 does what you want (sets variable g to 10), typing g = 10 alone does not. You can also work around this with parentheses, by typing (g =

           10).
Command enterword

Abbreviated ew Description Like enterbyte above, except it stores words in every other address starting

from the one specified.

Command fill Abbreviated f

Example  fill cs:0, cs:0xfffe, 123

Description Fills a range of memory with a single byte quickly, without generating memory write events for memory modified. The three arguments are the starting address, the ending address, and the byte to fill with. Note that the ending address is exclusive, i.e., fill 10, 10, 123 is a no-op, whereas fill 10, 11, 123 writes one byte.

Command flag Abbreviated fl

Example  flag if = 1
Example  $overflow = flag of

Description Returns the value of an 80186 flag. If used as an lvalue, it changes the value of a flag. Legal flags are:

cf Carry flag
pf Parity flag
af Auxiliary flag
zf Zero flag
sf Sign flag
tf Trace (single-step) flag
if Interrupt-enable flag
df Direction flag
of Overflow flag

Command fork Abbreviated fo

Example  fork my_function(1, 2, 3)
Example  fork { while (1) { ...  } }

Description Creates a new thread, which runs evaluating the expression given. The current values of SOTL local variables are copied for the new thread, so you can use locals variables inside the expression. The thread begins life runnable and unfrozen, and will start to run immediately; the current thread effectively “blocks”, but will run again once the forked thread blocks or exits. This operator returns the thread ID of the new thread.

Command format

Example  $mystr = format "X is ", %b x, " (decimal ", %d x, ")"

Description Formats SOTEST objects for output. It is exactly like print, except that instead of writing the result to standard output, it returns the result as a string. No newline is added to the string. See print below for more information.

Command getthreadid

Description Returns the thread ID of the current thread as a dword. Command go Abbreviated g

Example  go cs:ip+12

Description Unfreezes all frozen threads and then blocks until an event is received. At that point, it will print out the state of the current machines just like the machinelist operator. With no arguments, it will never return a value; the thread running it will block until it is killed. It is usually used from top-level, indicating that the user is ready to proceed with execution of background threads and the machines. It is not usually used from threads; blockuntil is more common, because it doesn’t have the side effects of unfreezing other threads and printing out machine state.

Command global Abbreviated gl Example global x, y Description Declares variables global without assigning them values. (Actually, if the

global variable doesn’t already exist, it creates it and assigns nil.) This is useful within SOTL functions, because within a function an assignment to an undefined variable causes the variable to be created local.

Command heapalloc

Abbreviated ha

Example  heapalloc 12

Description Allocates memory from the current machine’s heap, and returns its address. The argument to heapalloc specifies the size of the allocation. There is no way to free memory allocated in this way, other than redefining the heap.

Command heapdefine

Abbreviated hd

Example  heapdefine 0xf000:0, 0x1000

Description Defines a range of memory for use as a heap. Typically, this will be memory

that the target never uses.

Command historysize

Abbreviated hs

Example  historysize 40

Description Sets the number of history records SOTEST keeps, or, with an argument of 0, cancels history recording. If history recording is on, SOTEST keeps records of when memory is read or written, when ports are read or written, when instructions are fetched for execution, and when control is transferred from one point in the program to another. These records can be displayed with the printhistory command below.

Command inbyte Abbreviated ib

Example  inbyte 0x60

Description Reads a byte from a port. Although this command does generate a port read event, the thread subscribing to it will not receive it at least until after the inbyte operator finishes; thus, in general you will get the value last read from that port as opposed to any value that thread may supply. The value read is returned; thus, to see the value read from a port, do something like

              print %b inbyte 0x1234
Command interrupt

Abbreviated in

Example  interrupt 0x38

Description Generates an interrupt. If the vector type is not a hardware interrupt vector type, this blocks until the current machine’s interrupt flag is set and then generates an interrupt of the given vector type, just as if the machine had just executed an INT instruction. If the vector is a hardware vector, the interrupt is scheduled through the simulated interrupt controller, and the operator returns immediately.

Command inctimer

Abbreviated it

Example  inctimer 1

Description Advances the timer with the given number, as if it had received a strobe on its external clock line. This may generate an interrupt; if so, the interrupt is scheduled through the interrupt controller.

Command inword Abbreviated iw Description Just like inbyte, except it reads a word from the port.

Command length

Example  length "foo"

Description Returns the length of its argument. If the argument is a string, this is the number of characters in the string. If the argument is a block, this is the number of bytes in the block. If the argument is an array, this is the number of elements in the array (one more than the array’s highest index.)

Command load Abbreviated l

Example  load "test.abs"

Description Loads an absolute object file into the current machine, and processes its de-

bugging information.

Command local Abbreviated lo

Example  local x, y

Description Declares the named variables local. If the variables do not already exist locally, they are created and nil is assigned to them. Even though variables are created locally by default, it is still a good habit to always use local to declare variables. The reason is that if a variable exists globally, the first assignment to that variable in your function will not create the variable locally as you might expect, but rather will assign to the global variable.

Command machine

Abbreviated m

Example  machine Processor2

Description Sets the current function’s notion of the current machine to the machine given as its argument. Once this command is issued, operators which operate on machines (i.e., most operators, e.g., enterbyte and memoryfetch) will operate on the given machine. The old current machine is restored when the function exits.

Command machinecreate

Abbreviated mc Description Takes no arguments. It creates a new machine, allocates memory to simulate

target memory and ports, and sets the current machine to the new machine. Once a machine is created, there is no way to destroy it except by quitting SOTEST.

Command machineidle

Abbreviated mi Description Declares the current machine idle, sending events to threads waiting for the

machine to go idle. It takes an optional argument that identifies the machine’s idle point; the machine remains idle as long as CS:IP is equal to this idle point. If the idle point is omitted, then the current value of CS:IP is used. The typical way to use machineidle is to create an idle-detecting thread as follows:

   fork {
   while (1) {
   blockuntil _idle_task;
   machineidle _idle_task;
   }
   };

If you do not specify the idle point, it is possible that the idle-detecting thread will unblock at precisely the same time as a thread that generates an interrupt on the machine. In that case, you may inadvertently declare the machine idle at the start of the interrupt handler. Thus, it is a good habit to always explicitly specify the idle point.

SOTEST keeps track of the amount of time that elapses with the machine declared idle. You can use this to measure the loadware’s percent CPU utilization. The printclock operator will display the utilization.

Command machinelist

Abbreviated ml Description Prints the state of each machine: its name, the values of its registers and flags,

and whether or not the machine is idle.

Command machinename

Abbreviated mn

Example  machinename Processor1

Description Changes the name of the current machine to the name given. The name given

must not already be in use.

Command mapPCB Abbreviated mp

Example  mapPCB 0x8000:0

Description Memory-maps the 80186 peripheral control block (PCB) at the given addres.

Normally, the PCB is port-mapped at port 0xff00.

Command memoryfetch

Abbreviated mf

Example  $foo = memoryfetch [0x1234h:0x5678h, 10]

Description Takes a block as an argument. It reads the bytes from the block, generating memory read events, and assembles them into a string, which it returns. Note that this is not a particularly convenient way to view the contents of a block; you are better off using dump or print.

Command messagesend

Abbreviated ms

Example  messagesend $thread, "hello there"

Description Sends a message to a thread. The first argument is a thread ID number, usually the return value of fork. The second argument is the message, which may be any SOTL object; typically, dwords, strings, or arrays are sent. If the thread specified does not exist, or is not subscribed to messagereceive events, the message is dropped and a false value is returned; otherwise, a true value is returned.

Command microseconds
Example  microseconds 0:1.0  → 1000000

Description Converts a time value a dword, which is the number of microseconds (rounded down) that the time value represents. If its argument is a dword, it is interpreted as a number of ticks; i.e.,

              microseconds $foo

is equivalent to

              microseconds 0:0.$foo

The exact operation performed depends on how fast your processor is. For a 16 MHz processor, the operation is (more or less) a simple divide by 16.

Command outbyte

Abbreviated ob

Example  outbyte 0xff3e, 0

Description Sends a byte to a port, and generates a port write event.

Command outword

Abbreviated ow Description Sends a word to a port, and generates a port write event. Command pack Abbreviated pk

Example  pack 0x100:0, "CSL", 10, 1000, 10000000
Example  $packet = pack "nN", 1000, 10000000

Description Stores dwords in sophisticated ways. Pack has two forms. In the first form, the first argument is a dword, and it indicates a target address. The second argument, called the control string, is a string indicating how to store the remaining arguments, in the spirit of printf. The remaining arguments are dwords to be stored. The return value is the address where pack stopped; that is, the number of bytes written can be calculated as follows:

              $end = pack $addr, "v??V", 300, 123456789;
              $numpacked = $end - $addr;

Each character in the control string explains what to do with the next dword to be packed. For example, c means to interpret the dword as a signed char; one byte is stored at the current address, and the address is bumped up by one. N means to interpret the dword as an unsigned long, and pack it as four bytes in network (big-endian) byte order, most significant byte first.

c signed character
C unsigned character
s signed short (target byte order)

S unsigned short (target byte order)
i signed int
I unsigned int
l signed long (target byte order)
L unsigned long (target byte order)
v unsigned short, Vax (little-endian) byte order
V unsigned long, Vax (little-endian) byte order
n unsigned short, network byte order
N unsigned long, network byte order
x don’t take an argument; pack a null (0) byte
? don’t take an argument; skip forward a byte
X don’t take an argument; back up a byte
p pack the next argument according to target alignment constraints
P pack the next argument according to host alignment constraints

In these examples, address 0 is used with pack, and the results shown are the bytes stored at address 0 after the pack.

pack 0, "CCC", 1, 2, 3  → 01 02 03
pack 0, "S", 1234h  → 34 12
pack 0, "SL", 0102h, 010203h  → 02 01 03 02 01 00
pack 0, "cxi", 100, 1000  → 64 00 E8 03
pack 0, "cxv", 100, 1000  → 64 00 E8 03
pack 0, "cxn", 100, 1000  → 64 00 03 E8
pack 0, "cXc", 100, 7  → 07

In the second form of pack, the address is omitted, the first argument is the control string, and the remaining arguments are dwords to be stored. In this case, a string is created containing pack’s result and target memory is unchanged. Typical uses for this form include future storage with enterbyte, display with print, and message passing.

$mystring = pack "xxxx";  → string of 4 nulls
pack "CC", 104, 105  → "hi"

You may notice a redundancy: the first form of pack can be accomplished with the second form of pack and enterbyte. That is,

pack $addr, "...", ...

is equivalent to

enterbyte $addr, pack "...", ...

Command print Abbreviated p

Example  print "N is ", %b n, "!"

Description Displays SOTL and target data in a user-readable form. The arguments are SOTL expressions, optionally preceeded by a format specifier. The format specifiers are:

%d Format as signed decimal
%u Format as unsigned decimal
%b Format as array of hex bytes (chars)
%w Format as array of hex words (shorts)
%l Format as array of hex dwords (longs)
%p Format as array of pointers to target memory
%s Format as source code location

How an object displays depends on the type of the object and the format specifier. Dwords normally display as signed decimal, but can be displayed in hex by using %b, %w, or %l, or as a pointer with %p, or as a source code location with %s:

   print %b 100  → 64
   print %p 5ec2h:14  → 5EC2:000E _idle idle:168
   print %s 5ec2h:14  → 5EC2:000E _idle idle:168

The difference between the second and third example is that in the third example, in addition to printing the location, SOTEST sends a message to the SOTEST front end to display the source code at the given location.

By default, blocks display as [address, size], but can be displayed much more interestingly with format specifiers. In general, using a format specifier causes display of the contents of the memory to which the block refers. No memory read events will be generated when the memory is displayed. Examples should make this clear:

   enterbyte 1, 1, 2, 3, 4, 5, 6, 7, 8;
   x=[1,8]; print x  → [0000:0001, 8]
   print %b x  → 01 02 03 04 05 06 07 08
   print %w x  → 0201 0403 0605 0807
   print %l x  → 04030201 08070605

By default, references to target data object display symbolically; for example, a printing a reference to a structure displays its fields and their values.

You can control the number of array elements that will be displayed by setting the magic global variable $MaxArrayElems.

If you use %b, %w, or %l, the target object will be displayed as an array of hex bytes, words, or dwords; since SOTL knows the size of the target object, it knows how many numbers to print. No memory read events will be generated when target data objects are displayed.

By default, strings are printed in ASCII. However, strings can be formatted as arrays with %b, %w, or %l:

   print "foo ", %b "foo "  → foo 66 6F 6F 20

Other data types format very simply, and ignore their format specifier. For example:

   machinename FooMachine
   print FooMachine  → <Machine FooMachine>
Command printsegments

Description Dumps the segment definitions for the current machine. Segment names, classes, addresses, and sizes are displayed. This is basically the same information as you find in the linker’s map file.

Command printclock

Abbreviated pc Description Prints the value of the clock (i.e., the number of 80186 clock cycles since

SOTEST started), how much of that time was spent with the processor declared idle (see the machineidle operator), the amount of host CPU time used by SOTEST, and a comment about SOTEST’s performance.

Command printdma

Abbreviated pd Description Prints the state of the 80186 DMA controller. For each channel, it prints

the source and destination pointer, the synchronization type, the status of the DMA request lines (DRQ0 and DRQ1), and other miscellany about the channel. Use the dmarequest operator to manipulate the DMA request lines.

Command printframe

Abbreviated pf Description Prints the current stack frame. This is avaiable only if you have compiled

with debugging information. It displays the values of all parameters and local variables, and displays the return address and current execution address for the frame.

Command printhistory

Abbreviated ph

Example  printhistory 15

Description Prints recent history records. If an argument is specified, only that many are printed; otherwise, all records are printed. Records are printed from oldest to most recent. The historysize command must be issued first, to specify how many history records to keep.

Command printinterrupts

Abbreviated pi Description Prints the state of the 80186 interrupt controller. For each interrupt, it shows

the address of its interrupt procedure, whether the interrupt is masked, pending, or in service, and the interrupt’s priority.

Command printstack

Abbreviated ps Description Crawls the 80186 stack, hoping that every procedure is a far procedure. This

is available even without debugging information. It attempts to print the return addresses of procedures, their locals, and their parameters, but without type information and local variable information the rendition is ugly at best. However, if you’re stuck without debugging information, it’s better than nothing!

Command printtimers

Abbreviated pt Description Prints the state of the 80186 timer block. For each timer, it prints its value,

whether it is enabled, whether it is signalled internally or externally, and value of its thresholds.

Command processor

Abbreviated pr Description Configures the current machine’s microprocessor. The first argument is a

subcommand name; the remaining argument depends on the subcommand. Subcommands are:

I80186  The current machine is an 80186. No more arguments.
I80188  The current machine is an 80188. No more arguments.
clockspeed Sets the current machine’s clock speed to the given

value, in ticks per second.

waitstates Sets the number of wait states required for memory

access. Each wait state is one clock tick.

info  Displays the configuration of the current machine’s processor.

Examples:

              processor I80186;
              processor I80188;
              processor clockspeed, 16000000;  # 16 megaherz
              processor waitstates, 4;  # 4 wait states
              processor info;

Command random

Example  random 10

Description Returns a random number. If an argument is given, the number returned will be between 0 and its argument minus one; e.g, random 10 returns a number from 0 to 9. If not, the random number will be between 0 and 2^{32} − 1.

Command readline
Example  $line = readline $filedesc;
Example  $userinput = readline;

Description Returns a string read from a file. If a file descriptor is specified, the line is read from that file inefficiently, using lots of read(2) system calls, one character at a time. If a newline is hit, it terminates the line but is not included in the string read. If EOF is hit, it terminates a non-empty line. If EOF is hit at the beginning of the line, the value nil is returned.

If no file descriptor is given to readline, the line is read using stdio’s fgets(3) from SOTEST’s standard input (not the file being read by <). The string is returned, unless fgets(3) returns NULL, in which case the nil is returned.

Command reassigndebsym
Example  reassigndebsym 0x40e2, DFE_CODE
Example  reassigndebsym _dfe_intr_ul

Description This unfortunate command exists to work around a bug in SOTEST. SOTEST is not always able to correctly associate debugging information with its corresponding code segment. If there’s a public procedure foo which should, but doesn’t seem to, have debugging information, try this:

   print &foo;
   print _foo;

You will see two addresses. If they have the same offset but different segments, then the reason that foo doesn’t seem to have debugging information associated with it is because of SOTEST’s bug. To work around this bug, merely type

   reassigndebsym _foo;

and all should be well. If it isn’t, here’s another much more involved strategy. First, you will need to find the DEBSYM segment which actually contains the debugging information for foo (the printsegments and print %p search commands can help you). You also need to know the name of the segment in which foo is located (use printsegments and print %p _foo to find out). When you find the segment address of the DEBSYM segment, and the name of the code segment, do

   reassigndebsym debsym-segment-address, code-segment-name

as in

   reassigndebsym 0x44e2, MY_CODE

Then the test

   print &foo;
   print %p _foo;

should print the same exact address twice.

Command register

Abbreviated r

Example  r cs:ip = 0x1000:0x2000

Description Displays or sets registers on the current machine. With no argument, the registers and flags are displayed. The argument can be in the form reg = value or reg : reg = value. In the first case, the given register is set to the given value. In the second case, the first register is set to the segment portion (upper 16 bits) of the given value, and the second register is set to the offset portion.

Command return

Example  return "hello"

Description Exits the current SOTL function, returning the given value. Command search Abbreviated s

Example  search _start, _end, 10, 20, 30

Description Searches the given range of memory for the given sequence of bytes, returning the first address where the sequence was found. It does not generate read events for any memory read.

Command srandom
Example  srandom 123

Description Seeds the random number generator. With an argument, the argument is used

as the seed. Without an argument, the current system time is used as the seed.

Command string

Example  string 10, "hello", 0xff, 0, [_my_data, 20]

Description Constructs a string out of a list of arguments. The arguments may either be dwords, strings, or blocks. Dwords are converted to strings of length one, whose single byte is the least significant byte of the given dword. Blocks are converted to strings whose bytes are the bytes that make up the block (no memory read events are generated). Then the whole list is concatenated together.

              print %b string 10, "abc", 0  → 0A 61 62 63 00

   print string "hi", 32, "there"  → hi there
Command stringvalue
Example  stringvalue "A43", 16

Description Converts from a string to a dword. An optional second dword argument is the

base, which defaults to decimal. 0x... and ...h syntax are not recognized.

Command substr

Example  substr "hello there", 3, 2  → "lo"

Description Extracts a portion of a string. The second argument is the starting position (0 is the first character); the third argument is optional, and is the number of characters to extract. If omitted, characters up to the end of the string are extracted.

Command sourcedir

Abbreviated sd

Example  sourcedir "/build/mysources:/tmp:."

Description Specifies the directories that SOTEST will look in when trying to find source files. It takes a single argument which must be a string; it is a colon-separated list of pathnames similar to the PATH environment variable. When SOTEST locates a point in your source code, it will search the directories you specify here to find your source file and tell the SOTEST front end to display it. If you don’t use a front end, then your source is never displayed; see the section below on SOTEST front ends for more information.

The sourcedir operator returns the source directory list as a string, so the current source director list can be displayed with the command

   print sourcedir
Command threaddebug

Abbreviated td Description Puts a thread in debug mode. Such threads will be frozen when the user gains

control; they will continue to queue events, but they will be processed only when the user unfreezes the thread (either explicitly with threadunfreeze or by using the go command). A misbehaving (i.e., continuously-running) thread not in debug mode will keep the user from being able to do anything, but when in debug mode the user will be able to regain control with control-C. When a thread in debug mode receives an event, a warning message is printed.

Command threadexamine

Abbreviated te

Example  threadexamine getthreadid

Description Displays a considerable amount of information about the given thread. This

is often useful for debugging SOTL programs.

Command threadlist

Abbreviated tl Description Lists all threads, their thread ID, their state, whether or not they’re frozen,

whether or not they have events queued, their name, and where they were started from. You can get more information about a thread by with the

           command threadexamine.
Command threadcont

Abbreviated tc 12 Description Continues a thread that has been stopped by threadstop.

Command threadkill

Abbreviated tk

Example  threadkill 10, 11, getthreadid

Description Destroys a thread. Without an argument, destroys the current thread.

Command threadstop

Abbreviated ts

Example  threadstop $my_thread

Description Stops a thread. Without an argument, stops the current thread. Only a running thread can be stopped. When a thread is stopped, it cannot run and events for it will be queued. The only to resume a stopped thread is to issue the threadcont command.

Command threadunfreeze

Abbreviated tu Description Unfreezes the given thread. This is useful for debugging. The user may inter-

rupt the system, do something generating an event, inspect the state of the (presumably frozen) thread that is supposed to handle the event, and then manually unfreeze the thread while leaving other threads frozen, to prevent side effects. The user can then list the threads and find out which threads have events, and dump the relevant threads to find out what’s going on.

Command trace Abbreviated t

Example  trace 10

Description Steps through the target program until the current source line number changes. If an argument is given, it is used as a repeat count. If line number information is not available, it pretends the line number changes each instruction.

Command traceforward

Abbreviated tf Description Steps through the target program until the current source line number in-

creases — that is, it stops when CS:IP reaches a point in the same file but a larger line number. This is useful at the end of loops, to continue until the loop ends. However, it is fooled if the loop calls a procedure which is later in the same file. Its argument, if supplied, is used as a repeat count.

Command traceinstruction

Abbreviated ti Description Steps forward a single assembly language instruction. If an argument is given,

it is used as a repeat count.

Command traceprocedure

Abbreviated tp Description Steps forward until a procedure call or software interrupt call is found. Then

it sets a breakpoint at the point where the call returns, and blocks.

Command unassemble

Abbreviated u Example u _main Description Unassembles code at the given address Command unix Description Issue a UNIX system call. Several system calls are supported, and are de-

scribed individually below.

Command unpack Abbreviated un

Example  unpack ds:bx, "CSL", $foo, $bar, $baz
Example  unpack $packet, "nN", $code, $address

Description Performs the reverse of pack. It has two forms. The first form takes a memory address, a pack control string, and arguments (which must be lvalues). The bytes at the address are read from memory, interpreted according to the letters in the control string, and stored in the corresponding arguments. The return value is the address just following the last byte read by unpack. Memory read events are generated for locations read.

The second form takes a string as its first argument. It unpacks the bytes in the string, rather than bytes in target memory. It returns the number of bytes used, which is also the index into the string just past the last byte used.

Command up Description Performs the reverse of down. It climbs up one stack frame, to the current

frame’s caller, and displays the frame.

Command varlist

Abbreviated vl Description Lists all global variables, and displays their values.

Command warnings

Abbreviated w Description Lists the supported warnings, and whether they are enabled or disabled for

the current machine.

Command warncontrol

Abbreviated wc Description Toggles whether or not the user is warned when the current machine undergoes

a control transfer (a procedure call or return, and interrupt call or return, or a long jump.) This usually produces lots and lots of warnings, but used judiciously can be helpful to pinpoint where code runs foul. If an argument is specified, it is interpreted as a boolean; a true value enables the warning, and a false value disables it.

Command warndma

Abbreviated wd Description Toggles whether or not the user is warned when the current machine’s DMA

controller performs a DMA operation. If an argument is specified, it is interpreted as a boolean; a true value enables the warning, and a false value disables it.

Command warnevents

Abbreviated we Description Toggles whether or not the user is warned when a thread is sent an event. If

an argument is specified, it is interpreted as a boolean; a true value enables the warning, and a false value disables it. This warning is especially useful for debugging threads. Trace complete events are never displayed, even if this warning is enabled, because they are considered completely uninteresting.

Command warninterrupts

Abbreviated wi Description Toggles whether or not the user is warned when hardware interrupts are re-

quested and serviced. Like warncontrol, it accepts an optional boolean argument explicitly enabling or disabling the warning.

Command when

Example  when memorywrite(_myvar) { print "YOW!"; }

Description Takes an event specification list followed by a block of commands in curly braces. It forks a thread (returning its thread ID) that is blocked until an event matching the specification is received. At that point, the commands in the braces are executed and the thread exits. The command

   when 0:0.100000 { print "It's time"; }

is actually syntactic sugar for

   fork { blockuntil 0:0.100000; print "It's time"; }
Command whenever
Example  whenever memorywrite(_myvar) { print "YOW!"; }

Description Takes an event specification list followed by a block of commands in curly braces. It forks a thread (returning its thread ID) that is in an infinite loop, blocking until an event matching the specification is received. At that point, the commands in the braces are executed and the thread blocks again. The command

   whenever _my_proc { print "_my_proc called!"; }

is actually syntactic sugar for

   fork {
   while (1) {
   blockuntil _my_proc;
   print "_my_proc called!";
   }
   }

Note that any expressions you put in the event specification list are evaluated in the context of the new thread, and are re-evaluated every time around the loop. Thus, code like

   whenever .+2 { ... }

may not do what you expect (the breakpoint will be set in a different place each time around the loop.)

Command <

Example  < myfile.sot

Description This is not actually a command operator. It directs SOTEST to read commands from the named file in the spirit of #include. Note that the filename is a literal, not a string expression; the name is not in quotes, and string

operators are not allowed.

A.9 Events, Event Specifications, Event Predi-

cates

SOTEST supports 16 event types. Of these, five are used internally and are never seen by the user or SOTL code. There are 11 others that threads (or the user) may subscribe to, receive, display, and act on. These visible event types are target memory read, memory write, port read, port write, breakpoint, breaktime, idle machine, interthread message receive, thread exit, UNIX file descriptor ready, and UNIX message queue readable. The internal event types are command start, fork, target call return, trace complete, and interrupts allowed.

The user tells SOTEST what events are interesting by providing event specifications. The break operator subscribes the user to the given event specifications; when an event matching one of the specifications is received, the user is notified and simulation is suspended. The blockuntil operator deletes the thread’s old subscriptions, subscribes the thread to the given event specifications, and blocks the thread; when an event is received, the thread unblocks. The go operator is like blockuntil, but is meant for use by the foreground thread; in addition to subscribing the thread to the given specifications and blocking the thread, it unfreezes all frozen threads and prints machine state when an event is received.

When blockuntil (or go) returns, there is an event waiting for the thread. The thread can find out what sort of event it is with event predicates. Event predicates are actually like command operators, and can be used inside expressions just like command operators. There is one event predicate per event type. Each event predicate returns true if the event is of the given type, and false if it’s not. If the event is of the given type, details of the event will be written into the arguments given to the event predicate.

A.9.1 Event Types

The event types listed above are described here.

Type  memoryread

Details address, size Meaning This event is generated whenever a machine reads a memory location. It is also

generated by certain command operators, such as memoryfetch, as described above. The address of the memory location read and the size of the read (1 for a byte read, 2 for a word read) are included in a memoryread event.

Type  memorywrite

Details address, value, size Meaning This event is generated whenever a machine writes to target memory. It is

also generated by certain command operators, such as enterbyte, as described above. The address of the memory location written, the value written there, and the size of the write (1 for a byte write, 2 for a word write) are included

           in a memorywrite event.
Type  portread

Details port, size Meaning This event is generated whenever a machine reads a target I/O port. It is also

generated by certain command operators, such as inbyte, as described above. The address of the port read and the size of the read (1 for a byte read, 2 for a word read) are included in a portread event.

Type  portwrite

Details port, value, size Meaning This event is generated whenever a machine writes a target I/O port. It is

also generated by certain command operators, such as outbyte, as described above. The address of the port written and the size of the write (1 for a byte write, 2 for a word write) are included in a portwrite event.

Type  breakpoint

Details address Meaning This event is generated when the machine starts to execute an instruction.

The current value of CS:IP is included in the event.

Type  breaktime

Details time Meaning This event is generated when the machine’s clock goes past a certain value.

The current value of the clock is included in the event.

Type  idlemachine

Details machine Meaning This event is generated when a machine is declared idle by the machineidle

command. The machine declared idle is included in the event.

Type  messagereceive

Details sender, message Meaning This event is generated when a thread sends another thread a message. The

thread ID of the sender, and the contents of the message, are included in the event.

Type  threadexit

Details thread ID, exit value Meaning This event is generated when a thread exits. The thread’s ID and the result

of the thread’s last evaluation are included in this event.

Type  filedescriptor

Details unix file descriptor Meaning This event is generated when a file descriptor is “ready.” For a file descriptor

receiving connections with unix listen(), this means a connection has been initiated. For other file descriptors, it means the descriptor is readable. There is no way to be notified when a descriptor is writable; an attempt to write a full pipe causes then entire SOTEST process to block.

Type  messagequeue

Details unix message queue ID Meaning This event is generated when a message queue has messages in it. The message

queue ID is included in the event.

Type command (internal event type)

Details none Meaning This event is sent to a thread when it is forked as a user command, causing it

to assume the role of the foreground thread. It is never visible to the user.

Type fork (internal event type) Details parent thread ID Meaning This event is sent to a thread when it is forked from another thread. Type callreturn (internal event type) Details call return value Meaning This event is generated when a target procedure call (initiated by the call

command) returns. call causes the thread to block; this event is sent to make it unblock. The return value of the target call is included in the event, and becomes the return value of the call command. This event is never visible to the user.

Type tracecomplete (internal event type) Details none Meaning This event is generated when a target single-step (trace instruction) operation

completes. All trace commands actually cause the thread to block momentarily; this event unblocks the thread. It is never visible to the user.

Type interruptsallowed (internal event type) Details none Meaning This event is generated when the target goes into a state in which hardware-

requested interrupts can be serviced, i.e., the interrupt flag is set. It is used only by the interrupt command operator above.

A.9.2 Event Specifications

Event specifications can be dwords, time values, file descriptors, or message queue ID’s. A dword is a specifier for a breakpoint; a breakpoint event will be received when execution reaches the address indicated by the dword. A time value is a specifier for a breaktime; a breaktime event will be received when the clock reaches the given time value. A file descriptor is a specifier for filedescriptor events involving the given file descriptor; when the given file descriptor becomes readable or has a connection requested, an event will be received. A message queue ID is a specifier for messagequeue events; a messagequeue event will be generated when a message is waiting in the given event queue.

Example  blockuntil _my_func, 0:0.12345, $descriptor

Meaning The current thread will block until either CS:IP reaches _my_func, the clock reaches 12345 ticks, or the file descriptor stored in $descriptor is ready.

A.9.3 Event Specification Constructors

Only specifiers for those four event types (breakpoint, breaktime, filedescriptor, and messagequeue) can be created with SOTL objects. Specifiers for all user-visible event types can be created by event specification constructors. There is one such constructor for each user-visible event type. In each case the constructor has the same name as the event type.

Event specification constructors are allowed only immediately following break, blockuntil, or go. Event specifications are not themselves SOTL objects, and cannot be stored in variables or passed as arguments to SOTL functions.

Constructor memoryread

Example  blockuntil memoryread (_my_data, _my_data + 20)

Description Constructs a specifier for memory read events from addresses from _my_data to _my_data + 19, inclusive. If the constructor is given only one argument, a specifier for memory reads from that address alone is constructed; thus,

           memoryread(x) is equivalent to memoryread(x, x+1).

Constructor memorywrite

Example  go memorywrite (_my_data, my_data + 20)

Description Like memoryread, but for memory write events Constructor portread

Example  break portread (0x3f0, 0x3f8)

Description Like memorywrite, but for port read events Constructor portwrite

Example  blockuntil portwrite (0x3f0, 0x3f8)

Description Like memorywrite, but for port write events Constructor breakpoint

Example  break breakpoint (_my_func)

Description Constructs a specifier for breakpoint events at the given address. Note that breakpoint event specifiers can be constructed more simply by simply providing the address; this syntax is provided only for uniformity. Thus, the following forms are equivalent:

              blockuntil _my_func
              blockuntil breakpoint(my_func)

Constructor breaktime

Example  go breaktime (01:26.00000000)

Description Like breakpoint, but for breaktime events. It is redundant in the same sense

           that breakpoint is.

Constructor idlemachine

Example  blockuntil idlemachine (my_machine)

Description Constructs a specifier for idlemachine events for the given machine. When the given machine is declared idle, an idlemachine event will be received. Note that machines could be allowed as event specifiers, making this event specification constructor redundant, but currently they are not.

Constructor messagereceive

Example  break messagereceive()

Description Constructs a specifier for messagereceive events. When the current thread is sent a message by any other thread, a messagereceive event will be received. The messagereceive event, of course, contains the mesasge itself.

Constructor threadexit

Example  blockuntil threadexit($my_child)

Description Constructs a specifier for threadexit events. When the thread with the given ID exits, a threadexit event will be received, containing the exited thread’s ID and the result of its final evaluation.

Constructor filedescriptor

Example  go filedescriptor ($descriptor)

Description Like breakpoint, but for filedescriptor events. It is redundant in the same

sense that breakpoint is.

Constructor messagequeue

Example  blockuntil messagequeue ($message_queue_ID)

Description Like breakpoint, but for messagequeue events. It is redundant in the same sense that breakpoint is.

A.9.4 Event Predicates

When a thread unblocks after receiving an event, it needs some way to find out what event was received and what its details were. Event predicates provide this mechanism. Event predicates are command operators that return true if the last event received matches the pedicate, and false otherwise. If the event matches, the details of the event are written into the event predicate’s arguments. Like event specification constructors, there is one event predicate per user-visible event type. Note that event predicates can not be used anywhere in the arguments to break, blockuntil, or go, because they will be interpreted as event specification constructors.

Predicate  memoryread
Example  if memoryread($address, $size)) { ...  }

Description Tests if the last event received was a memoryread event. If so, it returns true and fills in its arguments with the address and size read. The size is 1 for a byte read, or 2 for a word read.

Predicate  memorywrite
Example  if memorywrite($address, $value, $size) { ...  }

Description Tests if the last event received was a memorywrite event. If so, it returns true and fills in its arguments with the address, value, and size written. The size is 1 for a byte write, or 2 for a word write.

Predicate portread

Example  if portread($port, $size)) { ...  }

Description Tests if the last event received was a portread event. If so, it returns true and fills in its arguments with the port and size read. The size is 1 for a byte read, or 2 for a word read.

Predicate  portwrite
Example  if portwrite($port, $value, $size) { ...  }

Description Tests if the last event received was a portwrite event. If so, it returns true and fills in its arguments with the port, value, and size written. The size is 1 for a byte write, or 2 for a word write.

Predicate  breakpoint
Example  if breakpoint($where) { ...  }

Description Tests if the last event received was a breakpoint event. If so, the code location

at which the breakpoint was hit is written into this operator’s only argument.

Predicate  breaktime
Example  if breaktime($when) { ...  }

Description Tests if the last event received was a breaktime event. If so, the value of the clock when the breaktime event was generated is written into this operator’s only argument.

Predicate  idlemachine
Example  if idlemachine($which_machine) { ...  }

Description Tests if the last event received was an idlemachine event. If so, the machine

that went idle is written into this operator’s only argument.

Predicate  messagereceive
Example  if messagereceive ($from_whom, $message) { ...  }

Description Tests if the last event received was a messagereceive event. If so, the thread ID of the sender and the message itself are written into this operator’s arguments.

Predicate  threadexit
Example  if threadexit ($who_died, $exit_value) { ...  }

Description Tests if the last event received was a threadexit event. If so, the thread ID of the thread that exited and the result of its final evaluation are written into

this operator’s arguments.

Predicate  filedescriptor
Example  if filedescriptor ($mydesc) { ...  }

Description Tests if the last event received was a filedescriptor event. If so, the file

descriptor that became ready is written into this operator’s argument.

Predicate  messagequeue
Example  if messagequeue ($thequeue) { ...  }

Description Tests if the last event received was a messagequeue event. If so, the message queue ID that has messages waiting is written into this operator’s argument.

A.10 UNIX System Calls

As mentioned above, several Unix system calls can be accessed via the unix command operator. They are described below.

System Call unix accept ()
Example  $new_connection = unix accept ($fd)

Description Performs the accept(2) system call. It returns a new file descriptor, or nil in

case of error.

System Call unix bind ()
Example  unix bind ($fd, $sockaddr)

Description Performs the bind(2) system call. The second argument is a string specifying the socket address; the length of the address is the length of the string. It returns the result of bind(2).

System Call unix connect ()
Example  unix connect ($fd, $sockaddr)

Description Performs the connect(2) system call. The second argument is a string specifying the socket address; the length of the address is the length of the string. It returns the result of connect(2).

System Call unix errno ()
Example  $errcode = unix errno()

Description Returns the current value of errno as a dword.

System Call unix getenv ()
Example  $homedir = unix getenv("HOME")

Description Returns the value of the named environment variable as a string, or nil if the

variable is not set.

System Call unix listen ()
Example  unix listen ($fd, 5)

Description Performs the listen(2) system call.

System Call unix mkfifo ()
Example  unix mkfifo ("/tmp/my_named_pipe")

Description Performs the mkfifo(2) system call, using mode 0666.

System Call unix msgget ()
Example  $queue = unix msgget ($key)

Description Performs the msgget(2) system call. It passes the key you provide (a dword),

and IPC_CREAT | 0777 for flags. Returns a message queue, or nil if it fails.

System Call unix msgsnd ()
Example  unix msgsnd ($queue, pack("H", 200) .  "hello")

Description Sends a message to the given queue. The first argument is a message queue, as returned by msgget. The second argument is a string representing a struct msgbuf; according to UNIX definitions (see msgsnd(2)), it should be a host long (indicating the message type) followed by data. An optional third argument specifies the length of the data portion of the message. For example,

              $type = pack("H", 300);
              unix msgsnd ($queue, $type . "Test", 1)

sends a message of type 300, one character in length, with that character being a ‘T’.

System Call unix msgrcv ()
Example  unix msgrcv ($queue, $message, 1024, 200);

Description Receives a message from the given queue. The first argument is the message queue, as returned by msgget. The second argument will have the message stored into it. The third is the maximum size that will be received. The fourth argument specifies the type of message that may be received, as in msgrcv(2). 0 means any type.

System Call unix open ()
Example  $descriptor = unix open ("/tmp/myfile", 1, 0x3ff)

Description Opens the given file and returns a descriptor if it succeeds, or nil otherwise. The second and third arguments are the flags and mode words, and default to 0.

System Call unix read ()
Example  unix read ($descriptor, $result, $size)

Description Reads from a file descriptor. At most $size bytes are read, and are stored in the string $result. If EOF is hit, 0 is returned; if there is an error, -1 is returned; and if the read is successful, the number of bytes read is returned.

System Call unix recv ()
Example  unix recv ($descriptor, $msg, 1024)

Description Calls the unix recv(2) system call and returns its result. The data received is stored as a string in the second argument; the third argument is the maximum number of bytes to receive.

System Call unix recvfrom ()
Example  unix recv ($descriptor, $msg, 1024, $sockaddr)

Description Calls the unix recvfrom(2) system call and returns its result. The data received is stored as a string in the second argument; the third argument is the maximum number of bytes to receive. The fourth is filled in with the address from which the message was received.

System Call unix semctl ()
Example  unix semctl ($semid, 0, $SETALL, pack("hhh", 1, 2, 3))

Description Calls the unix semctl(2) system call and returns its result. The arguments are the semaphore set ID returned by semget, the semaphore number, a command, and an argument. The constants used as commands are not defined for you; see <sys/sem.h>. If you pass GETALL or IPC_STAT, the fourth argument must be a variable which, after successful completion of the semctl(2) call, will hold the semval’s or struct semid_ds which the system call fills in. If you pass SETALL or IPC_SET, the fourth argument must be a string whose bytes make up an array of semval’s or a struct semid_ds; the pack operator is a good way to generate such a string.

System Call unix semget ()
Example  unix semget (1000, 10);

Description Calls the unix semget(2) system call and returns its result. The arguments are the key for the semaphore set and the number of semaphores in the set. A flags value of IPC_CREATE | 0777 is passed as semget’s third argument.

System Call unix semop ()
Example  unix semop ($semid, 0, -2, 0, 1, 3, 0);

Description Calls the unix semop(2) system call and returns its result. The first argument is a semaphore set ID. The remaining arguments are grouped into sets of three. Each set of three arguments makes up a semaphore operation, corresponding to the fields of struct sembuf. Thus, the above call subtracts 2 from semaphore 0 of the given set, and adds three to semaphore 1 of the set. (In each operation, a flags value of 0 is used.)

System Call unix send ()
Example  unix send ($descriptor, "Hello")

Description Calls the unix send(2) system call and returns its result. The third argument

is the number of bytes to send, and defaults to the length of the string.

System Call unix sendto ()
Example  unix sendto ($descriptor, "Hello", $sockaddr)

Description Calls the unix sendto(2) system call and returns its result. The final argument is the destination address. If four arguments are given, the third argument is the number of bytes to send, and defaults to the length of the string.

System Call unix socket ()
Example  $descriptor = unix socket (PF_UNIX, SOCK_STREAM);

Description Calls the unix socket(2) system call. You may use the symbolic constants

PF_UNIX or PF_INET as the protocol family, and SOCK_STREAM or SOCK_DGRAM

as the type. You may also pass a dword for either argument. The third argument given to the unix socket() call is always zero. In case of error, nil is returned; otherwise, the file descriptor associated with the new socket is returned.

System Call unix sprintf ()
Example  $now = unix sprintf ("%03d %s", 10, "hello")

Description Returns the output of the sprintf library function. The first argument must be a control string as described in sprintf(3). The remaining arguments are passed to sprintf. If the argument is a string, a pointer to the string’s contents (which will be null-terminated) is passed; otherwise, the argument is promoted to a dword and passed like a long. Warning: if you do evil things, like

   unix sprintf ("%s", 100);

you will lose; SOTEST will dump core and it’s your fault! (So there.)

System Call unix time ()
Example  $now = unix time ()

Description Returns the number of seconds since midnight on the first of January, 1970,

in Greenwich Mean Time.

System Call unix unlink ()
Example  unix unlink ("/tmp/myfile")

Description Unlinks the given file.

System Call unix write ()
Example  unix write ($descriptor, $data, $size)

Description Writes bytes from the given string. $size is the number of bytes, which is limited by (and defaults to) the length of the string. Returns the number of bytes successfully written, or -1 for error.