2.4   Writing a Non-WDB Back End

This section addresses the task of writing a back end that communicates with an agent (such as an emulator) other than the WDB agent. In this case, your back end methods must service back-end API requests directly, in other words, by way of a proprietary emulator API. For a general approach highlighting some of the issues involved in this process, see 2.4.1 Overview of Writing a Non-WDB Back End. The remainder of this chapter presents an example implementation. The complete source code for the acecpu32 example back end plus a back-end development class library, which implements the back-end framework as a C++ abstract base class, are provided in the Back-End Developer's Kit.

2.4.1   Overview of Writing a Non-WDB Back End

The following tasks are involved in writing a non-WDB back end:

The initialization routine is the entry point of the back end. It performs the back-end initialization and attachment. Even before the rest of the back-end functions are implemented, this routine can be called to test correct attachment between the back end and the target server.

Once the attachment and initialization procedures have been carried out successfully, you are ready to begin the major task of writing, documenting, and testing the back-end functions. The remainder of this section outlines the key development issues and suggests one approach to implementing a back end.

Design

The first step in your implementation is to determine what functions your back end must provide. The easiest way to establish this is by using the Back-End Developer's Kit as a basis. It contains a sample back end, the acecpu32, which is an implementation for the fictitious ACE C API. It operates an emulator supporting Motorola CPU32 microprocessors through BDM. It provides a structure and an implementation of common back-end services, including Gopher and checksum, which are emulator independent. You will need to support memory and register accesses, event handling on the target, communication with the emulator or other agent, state handling, and other emulator-dependent services.

Figure 2-7 shows the structure of the acecpu32 implementation.   

Figure 2-8 shows how the Event_T class, which manages target event information, is implemented to queue WDB_EVT_DATA.   

Implementation

The recommended plan for implementing the back end is to add support incrementally for the desired services, exercising them with the wtxtcl shell at each stage. (For more information on wtxtcl, see 4.4 WTX Tcl API.) Initially, you should write enough of the framework so that the target server can load your skeleton back end. Then you can add the key functions, testing them to make sure that you are on the right track. If you plan to support both UNIX and Windows hosts, we recommend that you build and test on both hosts in parallel.

  1. First, create a directory in installDir/host/src/tgtsvr/backend for the back end. By convention, the name of the back end and its directory should be a short, descriptive phrase which is all lower-case, such as acecpu32, for the ACE CPU32 emulator back end.

  1. Next, implement a skeleton back-end module which initializes the back end. The module must contain the back-end initialization routine and any other necessary routines. The target server will call the initialization routine when it loads the back end. You may find it helpful to provide stubs for other functions which log an error message to the console. Then you can start the target server with your back end and use the wtxtcl shell to test that it invokes functions in response to WTX requests.

  1. Implement the functions to read and write memory and registers.

  1. Implement the functions to set breakpoints, stop execution, resume execution, and step a context.

  1. Add event handling. At this point you should be able to get CrossWind, the source-level debugger, to operate successfully.

  1. Implement desired optional functions, such as the ability to invoke functions on the target.

  1. Tune performance. If you can optimize memory and register reads, you can improve performance significantly.

By implementing the back-end functions in this order, you will be able to verify quickly that different layers of the Tornado development environment interface correctly with your back end and the emulator you are supporting.

Wind River Conventions

The interface between the target server and the back end is documented in 2.2 Back-End Implementation. In addition, 2.2.1 Attachment and Initialization describes the WDB data structures used to pass information into and out of the back end. Your back end methods must also use the WDB error codes. Both the error codes and the data structures are declared in installDir/share/src/agents/wdb/wdb.h.

The Wind River C coding conventions are documented in the Tornado User's Guide: C and C++ Coding Conventions. The following additional conventions have been adopted for C++ and the Back-End Developer's Kit:

Finally, we recommend that you do not use C++ static objects in the back end because you will have no control over when their constructors and destructors are called. Instead, manage the objects with new and delete.

Testing

Back-end testing relies on the wtxtcl shell provided with Tornado. This shell allows you to send WTX protocol requests to the target server, the same kinds of requests the tools send. Using wtxtcl, you can interactively invoke the back-end methods to verify that they work correctly. Program wtxtcl in Tcl, which Wind River has extended to support the WTX protocol.

2.4.2   Setting Up the Back-End Developer's Kit

This section and the two sections that follow it use the libraries and methods of the Back-End Developer's Kit as an example implementation for the CPU32 emulator. You may be able to use significant portions of this framework for your back end.

Design Considerations

The Back-End Developer's Kit consists of a class library that provides the basic back-end framework, an example back end for acecpu32, sample test scripts, and this document. The library also implements common back-end methods, including Gopher and checksum, which are emulator-independent. By integrating this back-end framework into a new back end, you will not have to spend time developing and debugging these back-end services. To take advantage of this framework, derive a vendor-specific back-end class from the abstract base class Backend_T, declared in backend.h as shown in the following example:

... 
#include "backend.h" 
... 
// Declaration of Vendor-specific back end class 
 
class Ace_T : public Backend_T 
    { 
    // Backend_T methods which you are overriding go here 
    ... 
    // Your vendor-specific methods and data go here 
    };

Once you have derived your vendor-specific class, determine what other classes and objects you need in your system. If you are integrating an existing C API for operating the emulator, you may find that the acecpu32 example back end can be modified.

Some of the classes and objects you may need include the following:

Figure 2-9 shows the architecture of the acecpu32 back end.     

     

Implementation

After installing the libraries supplied with the Back-End Developer's Kit, follow the implementation procedure laid out in 2.4.1 Overview of Writing a Non-WDB Back End. When you begin tuning performance, if your emulator can perform an operation more efficiently than the default implementation (for example, a memory scan) override Backend_T's methods.

For an example of how to use WDB data structures and error codes, look at the acecpu32 example code in installDir/host/src/tgtsvr/backend/acecpu32.

Porting

The Back-End Developer's Kit's class library is supported only on Solaris 2.5.1 and 2.6, Windows NT, and Windows 95 hosts. The source code for this library is provided and can be readily ported to other hosts because of the limited use of operating system calls. Furthermore, the Tornado development environment provides generic makefile stubs which abstract most of the host-dependent build issues.

Testing

The Back-End Developer's Kit provides example test scripts which are located in installDir/host/src/tgtsvr/backend/bedk/tests. You can use these scripts as a basis for your own test scripts.

2.4.3   Getting Started

The first task is to get the basic skeleton of the back end operating. This means the target server should be able to start up, load the back-end DLL, and initialize, connect, and disconnect the back end.

Creating a Framework

In order to create the skeleton back end, carry out the following steps:

Implementation

To implement the skeleton back end, write the "main" back-end module, acecpu32.cpp, which initializes the back end by creating a vendor-specific back-end object, an instance of Ace_T.

Writing acecpu32Initialize( )

First, create acecpu32.cpp and define the routine acecpu32Initialize( ), which initializes the back end. Excerpts from acecpu32.cpp are annotated throughout this section.

Tornado provides header files for the development of host tools in installDir/host/include. The first Tornado header file included must be host.h. Declare the interface for the vendor-specific back end, Ace_T, by including acecpu32Backend.h as shown:

/* acecpu32.cpp - back end for ACE's CPU32 BDM emulator */ 
... 
/* includes */ 
 
#ifdef WIN32 
/* fix clash between Wind River's ERROR and Microsoft's ERROR macros */ 
#ifdef ERROR 
#undef ERROR 
#endif 
#include <windows.h> 
#endif /* #ifdef WIN32 */ 
 
#include "host.h" 
#include "tgtlib.h" 
#include "wpwrutil.h" 
#include "windll.h" 
 
#include "acecpu32Backend.h"


*

NOTE: Always include windows.h first to avoid clashes with Wind River definitions. Do not use ERROR, which Microsoft defines to be (0); always undefine ERROR, include system header files next, and include Wind River header files last.

Next, define the back-end initialization routine, acecpu32Initialize( ). For a complete discussion, see 2.2.1 Attachment and Initialization.

The back end must initialize the TGT_OPS and BKEND_INFO structures with the information needed by the target server to operate the back end; in particular, the addresses of the back-end methods must be stored in TGT_OPS and the asynchronous notification method must be stored in BKEND_INFO. In order for the target server, which is a C application, to call the back-end methods, the Backend_T class uses static member functions to provide an interface that supports C calling conventions; these static functions then invoke normal member functions on the actual back end. For more information on the static wrapper and Backend_T architecture, see the documentation in provided in installDir/host/src/tgtsvr/backend/bedk/backend.h.

STATUS acecpu32Initialize 
    ( 
    char *       tgtName,    /* network unique target name to connect */ 
    TGT_OPS *    pTgtOps     /* vector of back end functions to fill */ 
    BKEND_INFO * pBkendInfo  /* Backend notification method */ 
    )


*

NOTE: For Windows hosts, it is important to use default calling conventions and not WINAPI.

The final step is to allocate the actual back end by calling new and to perform error logging. The constructor of Backend_T, the back-end abstract base class, will initialize the back-end function pointers in the TGT_OPS structure. Ace_T's constructor initializes the back-end-specific part of the TGT_OPS structure.

The initialization routine must return OK on success or ERROR on failure, as defined in host.h. The Tornado' wpwrutil library provides several functions for error logging, including the function WPWR_LOG_MSG used in our example. Message logging is enabled by starting the target server with the option -V. For more information on the wpwrutil library, see the online reference material under Tornado API Reference>Target Server Back End Interface.

// Create Backend 
pTheBkEnd = new Ace_T (tgtName, timeout, recallNum, pTtyDevName, 
 
                     baudRate, pTgtOps); 
 
if (pTheBkEnd == NULL) 
    { 
    WPWR_LOG_MSG ("acecpu32Init(): new() failed.\n"); 
    return (ERROR); 
    } 
 
if (! pTheBkEnd->isValid_m()) 
    { 
    WPWR_LOG_MSG ("acecpu32Init(): back end initialization failed.\n") 
    return (ERROR); 
    } 
 
return (OK); 
}


*

NOTE: Constructors can fail, but most compilers lack exception support. For this reason, the Backend_T class provides a pair of protected members. Backend_T::isNotValid_m( ) should be called if vendor-specific constructors fail. Call Backend_T::isValid_m( ) to see if the back end has been successfully initialized.

Add any other routines to acecpu32.cpp that are needed to support the initialization of the back end.

Declaring the Vendor-Specific Back-End Class Skeleton

Next, declare the skeleton of the vendor-specific back-end class, Ace_T. Derive Ace_T from Backend_T and declare all the mandatory methods in acecpu32Backend.h. The examples in this section show the declaration of the Ace_T class in acecpu32Backend.h.

After the usual preamble of comments and modification history, give the header file the standard macro #ifndef/#define construction to prevent multiple inclusions of the header file. Next, provide the #include statements for the other necessary header files.

/* acecpu32Backend.h - header file for ACE SuperBDM BDM back end */ 
... 
#ifndef __INCacecpu32Backendh 
#define __INCacecpu32Backendh 
 
/* includes */

First, include the Rogue Wave header file.

#include "rw/queuecol.h" 
 
#ifdef WIN32 
#ifdef ERROR 
#undef ERROR 
#endif 
#endif

Next, include the header file for the ACE C API, ace/api.h. Include it at this point rather than later, with the other BEDK header files, because it includes Windows header files, and they must be included before the system headers. The undefining and redefining of ERROR is required when Windows header files are included because Windows uses a non-standard ERROR definition.

#include "ace/api.h" // ACE's API header file 
 
#ifdef WIN32 
#ifdef ERROR 
#undef ERROR 
#endif 
#define ERROR (-1) 
#endif

Next, include Tornado header files; wdb.h defines the WDB data structures used to pass information between the back end and the target server.

#include "host.h" 
#include "tgtlib.h" 
#include "wpwrutil.h" 
#include "wdb.h"

Finally, include backend.h, which pulls in the declaration of Backend_T (the back-end abstract base class) and other back-end-specific header files.

#include "backend.h" 
#include "event.h" 
#include "bdmExcLib.h"

Next, create a new link ID to identify your back end as a unique target-link type. This should be done by defining a macro TGT_LINK_BDM_ACE in tgtlib.h. You should choose an unused number in the 0x200 range.

#ifndef TGT_LINK_BDM_ACE                // Should be defined in tgtlib.h 
#define TGT_LINK_BDM_ACE      (0x201)    /* ACE BDM support */ 
#endif

Now, declare the Ace_T back-end class as shown below. To reuse the back-end framework and services implemented in Backend_T, you must derive your vendor-specific back end from it. It is declared in installDir/host/src/tgtsvr/backend/bedk/backend.h. The Ace_T constructor has the same arguments as the acecpu32Init( ) routine. It is automatically called when the actual Ace_T back-end object is allocated. The destructor must be declared virtual because Ace_T has virtual functions.

class Ace_T : public Backend_T 
    { 
    public: 
// Constructors and Destructors. Ace_T (char * tgtName, u_int timeout, u_int retryNum, char * devName, u_int param, TGT_OPS * pTgtOps, BKEND_INFO * pBkendInfo); virtual ~Ace_T ();

Once the constructor and destructor have been declared, declare the mandatory back-end methods. These methods are listed in backend.h.

    ///////////////////////////////////////////////////////////////// 
    // Declare back end member functions which need to be over-ridden in  
    // the vendor-specific back end. 
 
    //  Declaration of mandatory member functions. 
    UINT32 tgtPing_m (void); 
 
    UINT32 tgtConnect_m (WDB_TGT_INFO * pTgtInfo); 
 
    UINT32 tgtDisconnect_m (void); 
    ...

For the present, ignore the optional member functions. If your emulator supports a faster way of performing these operations than the generic implementation provided by the Backend_T class, you may decide to implement them later. Typically, Backend_T implements these methods by reading or writing a block of target memory. If your emulator supports an operation (for example, memory fill) directly, you will gain performance by using your emulator's primitive.

    //  optional member functions 
    ...

Now declare the mandatory helper methods. Ace_T::fdGet_m( ) should return the event file descriptor, whose activity indicates that an event has occurred on the target. Ace_T::halt_m( ) and Ace_T::unhalt_m( ) are used by Backend_T whenever it needs to suspend the target's system context or return the target to the state it was in before suspension. Typically, these primitives are used before performing a memory operation like Gopher or checksum.

// mandatory helper methods 
 
    virtual int       fdGet_m (); 
 
    //  State management 
    virtual UINT32 halt_m   (); 
    virtual UINT32 unhalt_m ();

Eventually, you will provide whatever vendor-specific helper methods and data are needed to implement your back-end class, such as Ace_T::eventCallBack( ), which is used to handle target events.

    ... 
    // handles asynchronous events in the ACE API 
    static void eventCallBack (ACE_ConnectionHandle, ACE_Event *,  
            void *); 
    ... 
}; 
 
// End of class Ace_T 
///////////////////////////////////////////////////////////////////////// 
 
#endif /* #ifndef __INCacecpu32Backendh */
Implementing the Vendor-Specific Back-End Class Skeleton

The back-end methods are implemented in acecpu32Backend.cpp. Initially, provide stubs for these methods that log a message to the console when they return successfully. This allows you to validate that the Tornado framework can successfully invoke methods in the new back end before you have coded all the methods. For a discussion of how to implement the mandatory and optional methods, see 2.4.4 Implementing Mandatory Member Functions and 2.4.5 Implementing Optional Member Functions.

/* acecpu32Backend.cpp - implements back end for ACE's SuperBDM */ 
 
/* includes */ 
...

The constructor call has the same prototype as the acecpu32Init( ) routine and the Backend_T constructor.

Ace_T::Ace_T  
 
    ( 
    char *        tgtName, 
    TGT_OPS *     pTgtOps, 
    BKEND_INFO *  pBkendInfo 
    ) 

Because Ace_T is derived from Backend_T, whenever an Ace_T object is allocated, Backend_T's constructor is called first. It is a good idea to include the call to Backend_T's constructor in an initializer list as a reminder of this fact, to improve performance, and to make sure the correct Backend_T constructor is called. Also, include any aggregate objects used in Ace_T, such as event queues and flags.

    : 
 
    Backend_T (tgtName, pTgtOps, pBkendInfo) 
 
    {

Backend_T's constructor initializes the generic information in the TGT_OPS structure. It also initializes BKEND_INFO. In other words, it defines what functions the target server should invoke to perform the various back-end operations. (See The BKEND_INFO Structure and 2.2.2 Back-End Functions.) See installDir/host/src/tgtsvr/backend/bedk/backend.cpp for functions Backend_T initializes.

Ace_T must initialize the other information in the TGT_OPS structure. Note that when event handling is implemented, the constructor calls Ace_T::fdGet_m( ) to determine which event file descriptor the target server should monitor for events on the target. Initially, since event handling has not yet been implemented, you should store NONE in tgtEventFd. A new Tornado back-end type was specified by defining TGT_LINK_BDM_ACE in tgtlib.h.

    ... 
    pTgtOps->tgtConnected      = FALSE; 
    pTgtOps->tgtEventFd        = NONE; // Record event fd. 
 
    pTgtOps->tgtLink.name      = "ACE SuperBDM BDM Mode 1.0.1"; 
    pTgtOps->tgtLink.type      = TGT_LINK_BDM_ACE; 
    pTgtOps->tgtLink.speed     = param;

Log a diagnostic message on completion of the constructor:

    // Log diagnostic message 
    WPWR_LOG_MSG ("Ace_T::Ace_T( ) : succeeded!\n"); 
 
    }

Similarly, implement a stub for the destructor. (C++ automatically calls Backend_T's destructor.)

Ace_T::~Ace_T () 
    { 
 
    // Log diagnostic message 
    WPWR_LOG_MSG ("Ace_T::~Ace_T( ) : succeeded!\n"); 
 
    }

Finally, you need to provide stubs for the mandatory back-end methods. To validate the back-end framework, it is helpful if these methods log a message and return the appropriate value to indicate success. Back-end methods have three possible return values. You may find it helpful to implement the three functions shown below so that you can see the return value and which method was invoked.

LOCAL UINT32 stubUINT32 (const char * pMethod) 
    { 
    WPWR_LOG_MSG ("Ace_T: - method %s.\n", pMethod); 
    return (WDB_OK);  
    } 
 
LOCAL BOOL   stubBOOL (const char * pMethod) 
    { 
    WPWR_LOG_MSG ("Ace_T: - method %s.\n", pMethod); 
    return (TRUE); 
    } 
 
LOCAL void   stubVOID (const char * pMethod) 
    { 
    WPWR_LOG_MSG ("Ace_T: - method %s.\n", pMethod); 
    }


*

NOTE: UINT32 is a Wind River convention for a 32-bit, unsigned integer. It is declared in installDir/host/include/host.h.

Use the appropriate stub function for each method's return type to implement the remaining mandatory methods. For example, Ace_T::tgtPing( ) would be implemented as follows:

UINT32 Ace_T::tgtPing_m (void) 
    { 
    return (stubUINT32 ("tgtPing_m ( )")); 
    }

The only function besides Ace_T's constructor you should implement without the stub functions is the Ace_T::tgtConnect_m( ) method. This method initializes WDB_TGT_INFO, which describes the target's configuration.

UINT32 Ace_T::tgtConnect_m (WDB_TGT_INFO * pWdbTgtInfo) 
    { 
    WPWR_LOG_MSG ("Establishing ACE SuperBDM connection... "); 
 
    // XXX - this information should be read from the $WIND_TGT_INFO 
    // file.  Using hard-coded literals for now.

First, record information about the debug agent, in this case the new emulator back end. agentInfo.mtu specifies the maximum amount of data that can be sent or received by the agent. agentInfo.mode is always set to WDB_MODE_EXTERN for a system-level debug agent like our emulator; this value is defined in wdb.h.

    pWdbTgtInfo->agentInfo.agentVersion      = "ACE BDM 1.0.1";  
    pWdbTgtInfo->agentInfo.mtu               = Ace_T::MaxMtu;      // XXX 
    pWdbTgtInfo->agentInfo.mode              = WDB_MODE_EXTERN;


*

CAUTION: pWdbTgtInfo->agentInfo.agentVersion specifies which version of Tornado the back end supports. This string must include the appropriate version number for initialization to proceed.

Next, describe the real-time operating system supported. At present only VxWorks is supported. Possible values for rtInfo.cpuType are defined in installDir/host/include/cputypes.h. Set the value of rtInfo.endian to 1234 or 4321 for big-Endian or little-Endian targets, respectively.

    pWdbTgtInfo->rtInfo.rtType               = WDB_RT_VXWORKS; 
    pWdbTgtInfo->rtInfo.rtVersion            = "5.3"; 
    pWdbTgtInfo->rtInfo.cpuType              = CPU32; 
    pWdbTgtInfo->rtInfo.hasFpp               = FALSE; 
    pWdbTgtInfo->rtInfo.hasWriteProtect      = 0; 
    pWdbTgtInfo->rtInfo.pageSize             = 0xffffffff; 
 
    /* The CPU32 target is always Big Endian */ 
    pWdbTgtInfo->rtInfo.endian          = 1234; 
 
    pWdbTgtInfo->rtInfo.bspName         = "ACE SuperBDM BDM Emulator"; 
    pWdbTgtInfo->rtInfo.bootline        = NULL;


*

NOTE: pWdbTgtInfo->rtInfo.cpuType is architecture specific.

The target server invokes Backend_T::tgtConnect_m( ) to perform any generic target connection work which may be needed. This function parses the target information configuration file by invoking Backend_T::tgtInfoGet_m( ). See the source code in backend.cpp for more information on Backend_T::tgtInfoGet_m( ).

    if (Backend_T::tgtConnect_m (pWdbTgtInfo) != WDB_OK) 
        { 
        WPWR_LOG_ERR ("Backend_T::tgtConnect_m () failed.\n"); 
        return (WDB_ERR_PROC_FAILED); 
        } 
 
    WPWR_LOG_MSG ("succeeded.\n"); 
 
    return (WDB_OK); 
    }

Now the skeleton is complete and ready for building and testing.

Building the Back End on UNIX

Tornado provides makefile stubs, which implement the generic part of a makefile. This facilitates the development and porting of makefiles. Including these stubs in a makefile assures that the generic part of the makefile is correctly implemented in a host-independent fashion.

Building the Back-End Developer's Kit (BEDK) Library

In order to use the BEDK library you must build this library before building the back end. A makefile and source code for the BEDK are provided in the BEDK directory, installDir/host/src/tgtsvr/backend/bedk. The makefile is designed to be host-independent to facilitate porting the BEDK to hosts that are not SunOS 4.1.3.

Building a New Back End

Once the BEDK library is built, create a makefile to build the new back end. Use the makefile provided for acecpu32 as an example. After the usual comments, define the necessary suffixes to build a back-end DLL on your host platform from C++. Inference rules are already defined in the makefile fragments provided by Tornado, but you should define any extra macros which are needed for the back end. The ACE_XXX macros specify the location of the ACE C API library and header files.

# Makefile - for acecpu32 backend 
... 
.SUFFIXES:   .cpp .so .sl 
ACE_BASE          = /folk/bss/sig/bdm/ace/api/aceapi_1.1 
ACE_LD_FLAGS      = -L$(ACE_BASE)/SunOS/lib -lACE68K 
ACE_INC           = -I$(ACE_BASE)/include

EXTRA_CFLAGS is a macro provided by the Tornado environment; it is used to specify extra options for the compiler. In this example, the extra option is used to define a macro to specify the CPU type used by the ACE emulator. Also include the makefile stubs provided by Tornado, which define standard macros and rules for building host applications:

EXTRA_CFLAGS     = -DACETARGET_68K 
... 
include       $(WIND_BASE)/host/include/make/generic.mh 
include       $(WIND_BASE)/host/include/make/$(HOST).mh
Because the BEDK library uses Rogue Wave's Tools.h++ library, you must define the following macros which specify where Tools.h++ is installed:

RW_ROOT           = /vobs/devtools/rogue/$(HOST) 
RW_INC            = -I$(RW_ROOT) 
RW_LIB            = $(RW_ROOT)/lib/librwtool.a 
RW_LD_FLAGS       = -L/vobs/devtools/rogue/$(HOST)/lib -lrwtool

Next, define INCLUDES to specify the include files location. Define BKEND_OBJS to be the list of objects that should be built and linked into the new back end:

INCLUDES     = $(WIND_INC) $(WIND_SHARE_INC) $(ACE_INC) $(RW_INC) \ 
               -I$(WIND_BASE)/share/src/agents/bedk \ 
                -I$(GNU_ROOT)/lib/g++include 
... 
BKEND_OBJS     = acecpu32.o acecpu32Backend.o event.o 
FULL_BKD_OBJS  = $(patsubst %.o, $(HOST)/%.o, $(BKEND_OBJS)) 
...

FULL_BKD_OBJS causes the object files to be placed in a subdirectory named after your hostType (on UNIX, the WIND_HOST_TYPE environment variable, for example, sun4-sunos4).

Specify the libraries to be linked with the back end, including their path names. The Tornado makefile stub defines a macro gnu_ROOT, which specifies the root of the tree containing the GNU ToolKit used to build the back end. You may need to redefine it for your configuration. If you forget to link a required library with the back end, the target server will crash when it loads the back end. An error message will state which symbol was unresolved. Run c++filt or an equivalent tool to demangle the symbol; you can then use nm to determine which library contains that symbol and link the library with the back end.

BKEND_LIB_EXTRA =  \ 
    -L../bedk     \ 
    -lbedk        \ 
    $(ACE_LD_FLAGS)        \ 
    $(RW_LD_FLAGS)         \ 
    -L$(gnu_ROOT)/lib      \ 
    -lg++         \ 
    -liostream    \ 
    -L$(gnu_ROOT)/lib/gcc-lib/sparc-sun-sunos4.1.3_U1/2.6.3/     \ 
    -ldl -lc -lgcc
Specify the name of the back end using SH_BKEND_OBJS. Then use LOCAL_CFLAGS to define the local compiler options: -DPORTABLE specifies portable versions of host libraries, -DHOST specifies host versions of header files, and $(DYN_LK_FLAGS) sets the options for compiling a DLL.

SH_BKEND_OBJS     = acecpu32.$(SHLIB_EXT) 
... 
LOCAL_CFLAGS      = -DPORTABLE -DHOST $(DYN_LK_FLAGS) -fno-builtin  \ 
              -g -fno-inline 
...
Finally, there is a target rule and an inference rule to build the back-end DLL:

default:lib objdircre $(SH_BKEND_OBJS) 
 
%.$(SHLIB_EXT):$(FULL_BKD_OBJS) 
        $(SHARED_LD) $(SHARED_LDFLAGS) -o$(SH_BKEND_LIB)/$*.$(SHLIB_EXT) \ 
             $(BKEND_OBJS) $(BKEND_LIB_EXTRA) $(BKEND_XDR_OBJS) 
 
... 
include  $(WIND_BASE)/host/include/make/generic2.mh 
...
Once your makefile is complete, build the skeleton back end with make.

Building on Windows NT and Windows 95

  1. Use Microsoft Visual C++ 5.0.

  1. Create a project for a multi-threaded DLL which includes BEDK source, your back end source, and any third party sources.

  1. In the project settings:

  • Link with Rogue Wave and other necessary libraries. Link only with multi-threaded DLLs. Do not link with libc or libcmt.

  • Define the macros RW_NO_STL and _RWTOOLSDLL (required by Roque Wave). Also define WIN32 and HOST (required by Wind River).

  1. Build Rogue Wave's multi-threaded DLL:

nmake -f makefile.msc ENVIRON=WIN32 THREAD=MULTI BINDING=DLL
Define RW_NO_WSTR in rw/compiler.h. Define RW_NO_STL and _RWTOOLSDLL.

  1. Do not mix debug and release objects and libraries.

Testing

There are two milestones that should be achieved while testing the skeleton back end: successfully loading the back end you build, and successfully invoking back end methods with wtxtcl.

Loading the Back End

After you have built the back end, the next step is to invoke the target server. Invoke the target server using Tornado's Launch tool, or enter:

% tgtsvr -V -B acecpu32    \ 
-core $WIND_BASE/target/config/ace360/vxWorks -A hostNameOfEmulator

where -V enables verbose diagnostic output, -B specifies which back end to use, -core specifies the path to the VxWorks image loaded on the target, -A causes all of VxWorks' symbols to be loaded into the target server symbol table (this flag is optional), and hostNameOfEmulator is the emulator's host name.

If you do not have a VxWorks image for the target board, you can use the following instead:

% tgtsvr -V -B acecpu32 -f a.out hostNameOfEmulator

where -f specifies the object module format (OMF) to use. Use the same OMF which Wind River uses for your target architecture. For more information on the target server and its options see the reference pages and the Tornado User's Guide: Getting Started.

If you are using UNIX and fail to link all the necessary libraries into the back end, the target server crashes. (This problem will not occur under Windows because Microsoft Visual C++ will generate error messages for unresolved externals during the link phase.) The UNIX error message is similar to the following:

% tgtsvr -V -B acecpu32 \ 
    -core $WIND_BASE/target/config/est360/vxWorks -A myTarget 
tgtsvr (myTarget@acheron): Tue May  7 12:26:10 1996 
    Attaching backend... ld.so: Undefined symbol: ___builtin_new

To solve this problem, you need to determine which library implements the undefined symbol, ___builtin_new. You will find nm invaluable for solving this problem, because it displays the symbols in a module. If the undefined symbol is a mangled name, use c++filt to convert the name to an unmangled name to make your search easier.

A side effect of the target server's crash is that the target server does not unregister with the registry daemon, wtxregd. You can confirm this using the registry status command, wtxreg:

% wtxreg 
Registry for acheron: 
        Name               Arch          Mb    #tools  User      Idle 
--------------------      ------------- ------ ------ --------- ------ 
myTarget@acheron         DOWN

You cannot restart a target server with the same name until you clean up the registry. To do so, use the wtxtcl script unreg, shown in Shutting Down the Target Server on p. 17, or use Launch.

When the back end is correctly linked, invoking the target server should produce the following messages:

tgtsvr (myTarget@acheron): Tue May  7 12:37:27 1996 
    Attaching backend... succeeded. 
    Establishing ACE SuperBDM connection...  succeeded. 
    Attaching C++ interface... succeeded. 
    Attached a.out OMF reader. 
    Warning: Core file checksums do not match.

Note that the acecpu32 back end does not perform the checksum because it would take too long, given this emulator's low bandwidth memory-read capability. Consequently, the target server logs a warning. This warning will not appear if your back end can perform a checksum, and if it matches the target server checksum of VxWorks' text segment.

Invoking Back-End Methods

Once you have built a back end that the target server can load, use wtxtcl to invoke back-end methods. This validates that tools can use the Tornado framework to invoke back-end methods. A typical test session involves starting the wtxtcl shell, attaching the target server and testing various back-end methods by issuing wtxtcl commands:

% wtxtcl 
wtxtcl>wtxToolAttach myTarget 
myTarget@acheron 
wtxtcl>wtxMemRead 0x0 16 
mblk0 
... 
wtxtcl>exit

If your implementation is successful, the back end logs appropriate messages when you invoke a back-end method. Most WTX Tcl procedures, like wtxMemRead, directly map onto a corresponding back-end method. The first WTX Tcl command which you enter is always wtxToolAttach to connect wtxtcl to a specific target server. Then invoke appropriate WTX Tcl procedures to test the back-end framework.

If a wtxtcl method fails, use installDir/host/include/wtxerr.h to convert the error code into a meaningful message. For example:

wtxtcl>wtxVioWrite 1 -string "asdf" 
Error: WTX Error 0x100d4

To convert 0x1000d4 into a meaningful value, convert 0xd4 to a decimal value. You should find a corresponding entry in wtxerr.h. You may find it helpful to create a script to do this using grep and bc. Once you have converted the value, you find:

WTX_ERR_AGENT_NO_AGENT_PROC    = (WTXERR_BASE_NUM | 212)

2.4.4   Implementing Mandatory Member Functions

Once the back-end framework is validated, you can implement the mandatory member functions. First implement memory and register read and write methods. Once this milestone is achieved, implement the remaining mandatory back-end functions including state management and event handling. Studying the acecpu32 back-end example in parallel with this section will be helpful, especially in understanding the use of the WDB data structures. The required back-end methods are shown in Table 2-1.   

Table 2-1:  Mandatory Member Functions 


Member Function
 
Purpose
 

Functions Included in the Target Server Back End API
 
tgtConnect_m( )
 
Connect back end to emulator.
 
tgtDisconnect_m( )
 
Disconnect back end from emulator.
 
memRead_m( )
 
Read data from target memory.
 
memWrite_m( )
 
Write data to target memory.
 
regsGet_m( )
 
Read target registers.
 
regsSet_m( )
 
Write data to target register(s).
 
eventpointAdd_m( )
 
Set a breakpoint.
 
eventpointDelete_m( )
 
Remove a breakpoint.
 
contextSuspend_m( )
 
Suspend a context on the target.
 
contextResume_m( )
 
Resume a context on the target.
 
contextCont_m( )
 
Continue a target context.
 
contextStep_m( )
 
Step a target context.
 
eventGet_m( )
 
Get WDB event data.
 
evtPending_m( )
 
Check if an event is pending.
 
evtPendingClear_m( )
 
Clear an event after processing.
 
Functions Specific to the Back End Developer's Kit
 
fdGet_m( )
 
Get an event file descriptor.
 
halt_m( )
 
Halt the system.
 
unhalt_m( )
 
Return the system to its state prior to halting.
 

Implement Read and Write

The next step is to implement the methods to read and write memory and registers. This validates that you can operate the emulator from Tornado and that the WDB data structures are correctly passing information between the target server and the back end.

Implementing Ace_T::memRead_m( )

The first back-end methods to implement are the methods for reading and writing memory. This section presents a detailed example for the memory-read method. The memory-write method is similar.

When the target server calls Ace_T::memRead_m( ) it passes a pointer to a WDB_MEM_REGION structure describing the region of target memory to be read, and a pointer to a WDB_MEM_XFER structure describing where the data read from the target should be returned.

UINT32 Ace_T::memRead_m  
    ( 
    WDB_MEM_REGION * pMemRegion,  
    WDB_MEM_XFER *   pMemXfer 
    ) 
 
    { 
    int                status; 
    ACE_OperatingMode  oldState;

To check that an access request is for a valid address, Ace_T::memRead_m( ) compares the request to the bounds of target memory, which are stored in the WDB_TGT_INFO structure parsed by Backend_T::tgtInfoGet_m( ).

    // Check that memory access is valid 
    UINT32 aceMemBase = wdbTgtInfo_.rtInfo.memBase; 
    UINT32 aceMemSize = wdbTgtInfo_.rtInfo.memSize; 
 
    if  ( 
        ((UINT32) pMemRegion->baseAddr < aceMemBase) || 
        ((UINT32) pMemRegion->baseAddr + pMemRegion->numBytes) >  
            (aceMemBase + aceMemSize) 
        ) 
        { 
        ::wpwrLogWarn ("Invalid memory access of %#x bytes at %#x.\n", 
                       pMemRegion->numBytes, pMemRegion->baseAddr); 
        }

Next, the WDB_MEM_XFER structure, which will hold the result of the memory read, is initialized. The param field is used in a request-dependent manner. In this example, param contains the address of the buffer to store the data in.

    //  Book keeping for WTX protocol 
    pMemXfer->source           = (WDB_OPQ_DATA_T) pMemRegion->baseAddr; 
    pMemXfer->numBytes         = pMemRegion->numBytes; 
    pMemXfer->destination      = (TGT_ADDR_T) pMemRegion->param;

The target server will not break a memory read request up into several small requests if the initial request is larger than the agentInfo.mtu value specified in the WDB_TGT_INFO data returned by the Ace_T::tgtConnect_m( ) request. Consequently, the back end needs to break up the request if necessary.

    // The target server does not break up requests which 
    // exceed the back end's MTU, so we need to do that here. 
 
    TGT_INT_T     numLeft;        // num bytes left to read. 
    TGT_INT_T     numToRead;      // num bytes to read in this iteration. 
    UINT8 *       pTgtAddr;       // address to start next read. 
    UINT8 *       pDestAddr;      // where to put result of read. 
 
    pTgtAddr   = (UINT8 *) pMemRegion->baseAddr; 
    pDestAddr  = (UINT8 *) pMemRegion->param; 
 
    for  
        ( 
        numLeft =  pMemRegion->numBytes; 
        numLeft > 0; 
        numLeft -= Ace_T::MaxMtu 
        ) 
 
        { 
 
        numToRead = ((numLeft > Ace_T::MaxMtu) ?  
                     (TGT_INT_T) Ace_T::MaxMtu : numLeft);

Next, the back end uses a function in the ACE C API to read memory from the target via the emulator:

        status = ::ACE_ReadTargetMemory 
                                    ( 
                                    emulator_,  
                                    (ACE_TargetAddress) pTgtAddr, 
                                    (void *) pDestAddr,  
                                    (uint32) numToRead 
                                    );

If the ACE C API request fails, the back end calls Ace_T::wdbErrFigure_m( ) to convert the ACE error code into a WDB error code and log an appropriate error message. If the method's return type is UINT32, the back-end methods must return WDB_OK or a suitable WDB error code, defined in wdb.h.

        if (status != ACE_SUCCESS) 
            { 
            (void) stateRestore_m (oldState); 
            return (wdbErrFigure_m ("ACE_ReadTargetMemory ()",  
                                    status, WDB_ERR_MEM_ACCES)); 
            } 
 
        pTgtAddr  += numToRead; 
        pDestAddr += numToRead; 
 
        } 
 
        return (WDB_OK); 
    }

The next step is to rebuild the back end, start the target server, and test the memory-read method. In this example, wtxtcl is used to attach to the target server, read target memory, and display the result. wtxMemRead returns a memory block handle (mblk0 in the example) to provide more efficient memory manipulation. For more information, see the 4. The WTX Protocol.

% wtxtcl 
wtxtcl> wtxToolAttach myTarget 
myTarget@acheron-1 
wtxtcl> wtxMemRead targetAddress 16 
mblk0 
wtxtcl> memBlockGet -l mblk0 
0xdeadbeef 0xdeadbeef 0xdeadbeef 0xdeadbeef 
wtxtcl> exit

You may find it useful to create a library of scripts to exercise simple back-end requests. See installDir/host/src/tgtsvr/backend/bedk for examples of such scripts. You can invoke them from wtxtcl with the source command.

Implementing Ace_T::regsSet_m( )

The implementation of the methods to read and write registers is analogous to the memory read method. There are three possible sources of confusion about register operations. First, the WDB protocol allows just part of a register set to be accessed. Second, you should use the Wind River-defined register-set structure. Third, register information must be handled in opaque format; in other words, it must have the same byte ordering and alignment as the target. The definition of the register set is in installDir/target/h/arch/archType/regsArchType.h, where archType is the target architecture type.

UINT32  Ace_T::regsSet_m (WDB_REG_WRITE_DESC * pRegWrite) 
    { 
    ACE_AllRegisters       aceRegSet; 
    ACE_OperatingMode      oldState; 
    int               status;        // ACE API return value

Register access is one of the least portable parts of the back end. To handle the register set for your target architecture, you need to define a register-set structure which mimics the definition in regsArchType.h. The ACE emulator supports CPU32 targets, so a 68k register-set type, REG_SET_68K, is declared in acecpu32Backend.h and shown here:

    REG_SET_68K       wrsRegSet; 
    char *            pWrsRegBuf; // pointer to treat reg set as opaque

Having established the correct register-set structure, perform some sanity checking on the arguments. Note the use of WDB error codes.

    if (pRegWrite->context.contextType != WDB_CTX_SYSTEM) 
        { 
        return (WDB_ERR_INVALID_CONTEXT); 
        } 
 
    if (pRegWrite->regSetType != WTX_REG_SET_IU) 
        { 
        return (WDB_ERR_INVALID_PARAMS); 
        } 
    ...

CrossWind, the GDB-based source-level debugger, accesses only one register at a time. Consequently, it is important to optimize for the case of reading and writing only one register. The back end API also requires support for reading and writing the complete register set, so acecpu32 includes both cases.

    if (pRegWrite->memXfer.numBytes <= 4) 
        { 
        uint32            aceRegSize; 
        ACE_Register      whichAceReg;     // which register to read.

Next, the register-set method converts the Wind River register identifier, which is a byte offset into the register set, into an ACE register identifier, which is an enumerated type. The Wind River byte offset is stored in memXfer.destination.

        status = whichAceRegGet_m (pRegWrite->memXfer.destination, 
                                   &whichAceReg, &aceRegSize); 
        if (status != WDB_OK) 
            { 
            return (status); 
            }

The 68k does not support direct modification of the A7 register, so you must modify the SSP.

        if (whichAceReg == ACE_REG_A7) 
            { 
            whichAceReg = ACE_REG_SSP; 
            }

CrossWind treats the status register as 32 bits: 16 bits of padding plus 16 bits of data. The ACE API treats the status register as 16 bits of data. The appropriate conversion is performed here:

        if ((whichAceReg == ACE_REG_SR) && 
            (pRegWrite->memXfer.numBytes == 4)) 
            { 
 
            UINT8 *      pRegWriteVal = (UINT8 *) pRegWrite->memXfer.source; 
            pRegWriteVal += 2;    // Skip padding in WRS's REG_SET. 
 
            // memXfer.source contains register information 
            // in the same representation it would appear in 
            // target memory (i.e., it is opaque).  The 
            // ACE API expects this information in host byte 
            // ordering, so we must convert the register value. 
 
            *(UINT16 *) pRegWriteVal = FIX_16 (*(UINT16 *) pRegWriteVal); 
 
            status = regSetOne_m (whichAceReg, pRegWriteVal); 
            } 
        else 
            { 
            // memXfer.source contains register information 
            // in the same representation it would appear in 
            // target memory (i.e., it is opaque).  The 
            // ACE API expects this information in host byte 
            // ordering, so we must convert the register value. 
 
            UINT32 aceRegBuf = FIX_32 (*(UINT32 *)  
                        pRegWrite->memXfer.source);

A special vendor-specific helper function, Ace_T::regSetOne_m( ), efficiently writes just one register, returning a WDB error code:

            status = regSetOne_m (whichAceReg, (UINT8 *) &aceRegBuf); 
            } 
 
        if (status != WDB_OK) 
            { 
            return (status); 
            } 
 
        return (stateRestore_m (oldState)); 
        } 
 
    // End of writing only one register

Having dealt with the special case of accessing only one register, the Ace_T::regsSet_m( ) now performs the more general case by reading the target's entire register set, modifying the desired registers, and re-writing the register set.

    // Read all target registers 
 
    status = ::ACE_ReadAllTargetRegisters (emulator_, &aceRegSet); 
 
    if (status != ACE_SUCCESS) 
        { 
        ... 
        return (wdbErrFigure_m ("ACE_ReadAllTargetRegisters ()", 
                                status, WDB_ERR_PROC_FAILED)); 
        } 
 
    // Force ACE register info into WRS data structures (defined in 
    // $WIND_BASE/target/h/arch/mc68k/regsMc68k.h) 
 
    ::memset (&wrsRegSet, 0x00, sizeof (wrsRegSet)); 
 
    // memXfer.source contains register information 
    // in the same representation it would appear in 
    // target memory (i.e., it is opaque).  The 
    // ACE API expects this information in host byte 
    // ordering, so we must convert the register value. 
 
    wrsRegSet.dataReg[0] = FIX_32 (aceRegSet.Dx[0]);   /* data registers */ 
    wrsRegSet.dataReg[1] = FIX_32 (aceRegSet.Dx[1]); 
    wrsRegSet.dataReg[2] = FIX_32 (aceRegSet.Dx[2]); 
    wrsRegSet.dataReg[3] = FIX_32 (aceRegSet.Dx[3]); 
    wrsRegSet.dataReg[4] = FIX_32 (aceRegSet.Dx[4]); 
    wrsRegSet.dataReg[5] = FIX_32 (aceRegSet.Dx[5]); 
    wrsRegSet.dataReg[6] = FIX_32 (aceRegSet.Dx[6]); 
    wrsRegSet.dataReg[7] = FIX_32 (aceRegSet.Dx[7]); 
 
    wrsRegSet.addrReg[0] = FIX_32 (aceRegSet.Ax[0]);  /* address registers */ 
    wrsRegSet.addrReg[1] = FIX_32 (aceRegSet.Ax[1]); 
    wrsRegSet.addrReg[2] = FIX_32 (aceRegSet.Ax[2]); 
    wrsRegSet.addrReg[3] = FIX_32 (aceRegSet.Ax[3]); 
    wrsRegSet.addrReg[4] = FIX_32 (aceRegSet.Ax[4]); 
    wrsRegSet.addrReg[5] = FIX_32 (aceRegSet.Ax[5]); 
    wrsRegSet.addrReg[6] = FIX_32 (aceRegSet.Ax[6]); 
    wrsRegSet.addrReg[7] = FIX_32 (aceRegSet.Ax[7]); 
 
    wrsRegSet.sr         = FIX_16 (aceRegSet.SR); 
    wrsRegSet.pc         = (INSTR *) FIX_32 (aceRegSet.PC);


*

NOTE: The ACE API returns data in host byte order, which must be converted to opaque format.

In the example below, note how memXfer specifies which part of the register set to modify: memXfer.destination specifies the byte offset into the target register set where the new data should be stored; and memXfer.source contains a pointer to an opaque chunk of register data, formatted like the corresponding part of a Wind River register-set structure. For example, if the byte offset is 64 bytes, memXfer.source points to two bytes of padding, followed by two bytes of status-register information, and so on for a CPU32 register set.

    // Modify desired registers 
    pWrsRegBuf = (char *) &wrsRegSet; 
    ::memcpy ((void *) (pWrsRegBuf + pRegWrite->memXfer.destination), 
              (void *) pRegWrite->memXfer.source, 
              pRegWrite->memXfer.numBytes); 
 
    // Store in ACE's reg set. 
 
    aceRegSet.Dx[0] =   FIX_32 (wrsRegSet.dataReg[0]);   /* data registers */ 
    aceRegSet.Dx[1] =   FIX_32 (wrsRegSet.dataReg[1]); 
    aceRegSet.Dx[2] =   FIX_32 (wrsRegSet.dataReg[2]); 
    aceRegSet.Dx[3] =   FIX_32 (wrsRegSet.dataReg[3]); 
    aceRegSet.Dx[4] =   FIX_32 (wrsRegSet.dataReg[4]); 
    aceRegSet.Dx[5] =   FIX_32 (wrsRegSet.dataReg[5]); 
    aceRegSet.Dx[6] =   FIX_32 (wrsRegSet.dataReg[6]); 
    aceRegSet.Dx[7] =   FIX_32 (wrsRegSet.dataReg[7]); 
 
    aceRegSet.Ax[0] =   FIX_32 (wrsRegSet.addrReg[0]);   /* address regs */ 
    aceRegSet.Ax[1] =   FIX_32 (wrsRegSet.addrReg[1]); 
    aceRegSet.Ax[2] =   FIX_32 (wrsRegSet.addrReg[2]); 
    aceRegSet.Ax[3] =   FIX_32 (wrsRegSet.addrReg[3]); 
    aceRegSet.Ax[4] =   FIX_32 (wrsRegSet.addrReg[4]); 
    aceRegSet.Ax[5] =   FIX_32 (wrsRegSet.addrReg[5]); 
    aceRegSet.Ax[6] =   FIX_32 (wrsRegSet.addrReg[6]); 
    aceRegSet.Ax[7] =   FIX_32 (wrsRegSet.addrReg[7]); // Ignored in ACE API 
                                                        // Must set SSP instead 
    aceRegSet.SSP   =   FIX_32 (wrsRegSet.addrReg[7]); // This assumes use 
                                                         // of vxWorks! 
    aceRegSet.SR    =   FIX_16 (wrsRegSet.sr); 
    aceRegSet.PC    =   FIX_32 ((uint32) wrsRegSet.pc); 
 
    status = ::ACE_WriteAllTargetRegisters (emulator_, &aceRegSet); 
 
    if (status != ACE_SUCCESS) 
        { 
        ... 
        return (wdbErrFigure_m ("ACE_WriteAllTargetRegisters ()", 
                                status, WDB_ERR_PROC_FAILED)); 
        } 
    else 
        { 
        return (WDB_OK); 
        } 
    }

Tornado expects to receive event information (including exception information) and register information in target byte order. Register contents should be transferred in opaque fashion (in other words, bit for bit as they appear in the registers).

Finish implementing and testing the methods for reading and writing memory and registers. Use your emulator's tools to verify that wtxtcl commands correctly operate the emulator.

Implement Context Management

By implementing the read and write memory and register methods, you demonstrated that the new back end can actually operate the emulator. The next step is to add support for basic context management: setting breakpoints, suspending and resuming the system, and so on. Follow the same process of adding functionality and testing it with wtxtcl and your emulator's tools.

The key difference in this step is that you need to manage state: is the target running? do you need to stop it to perform the operation? We first examine the methods for acecpu32 back-end state management in detail. Then we discuss caveats for the other context management methods.

State Management

The ACE back end uses Ace_T::stateBDM_m( ) and Ace_T::stateRestore_m( )to manage the target state. The first method is used to halt the system by putting the target in background debug mode or BDM1 . The second method is used to return the system to the state it was in before it was halted. Once these methods are implemented, the functionality must be made available to the Backend_T class by implementing the mandatory member functions halt_m( ) and unhalt_m( ); this allows the methods implemented in Backend_T to manage target state.

The examples step through Ace_T::stateBDM_m( ) in detail. When this method is called, it is passed a variable where it can record the old state. This variable is passed to Ace_T::stateRestore_m( ) which restores the system to that state. This implementation supports nested calls of these state management methods. (Note: the argument to Ace_T::stateBDM_m( ) is a reference!)

UINT32 Ace_T::stateBDM_m (ACE_OperatingMode & oldState) 
    { 
    int status;

An ACE C API method is used to determine the target state. Based on this state, Ace_T::stateBDM_m( ) performs the appropriate operation.

    oldState = (ACE_OperatingMode) ::ACE_GetOperatingMode (emulator_); 
 
    switch (oldState) 
        {

If the state is ACE_MODE_BDM, the target is already halted.

        case ACE_MODE_BDM: 
            status = ACE_SUCCESS; 
            break;

If the system is running, use the ACE C API to halt the target:

        case ACE_MODE_RUN:      /* Running */ 
        case ACE_MODE_TRC:      /* Non-realtime trace */ 
            status = ::ACE_StopTarget (emulator_); 
            if (status != ACE_SUCCESS) 
                { 
                return wdbErrFigure_m ("ACE_StopTarget ()",  
                                       status, WDB_ERR_PROC_FAILED); 
                } 
            break;

If the system is in the error state, attempt to recover by reinitializing the system:

        case ACE_MODE_ERR:       /* Error */ 
 
            status = ::ACE_Initialize (emulator_, ACE_INN_COMMAND); 
            if (status != ACE_SUCCESS) 
                { 
                return wdbErrFigure_m ("ACE_Initialize ()",  
                                       status, WDB_ERR_PROC_FAILED); 
                } 
            break;

If the system is in any other state, an error message is logged and Ace_T::stateBDM_m( ) fails.

        case ACE_MODE_NOCONNECT:        /* Not connected... */ 
        case ACE_MODE_SIM:              /* Simulation mode */ 
        case ACE_MODE_PFA:              /* Performance */ 
            status = ACE_FAILURE; 
            WPWR_LOG_ERR ("Ace_T::stateBDM_m () : invalid state %s.\n", 
                          ::ACE_OperatingModeImage (oldState)); 
            break; 
        default: 
            status = ACE_FAILURE; 
            WPWR_LOG_ERR ("Ace_T::stateBDM_m () : invalid mode %d.\n", 
                          oldState); 
            break; 
        } 
 
    return (wdbErrFigure_m ("Ace_T::stateBDM_m ()", status,  
                            WDB_ERR_AGENT_MODE)); 
    }

The function Ace_T::stateRestore_m( ) returns the system to its previous state by following a similar pattern.

You must also implement halt_m( ) and unhalt_m( ), which wrap stateBDM_m( )and stateRestore_m( ). Note that oldTgtState_ is a data member of Ace_T:

UINT32 Ace_T::halt_m () 
    { 
    UINT32          status; 
 
    status   = stateBDM_m (oldTgtState_); 
 
    return (status); 
    }
Implementation Hints for Other Context-Management Methods

Now that you have implemented state management, you can implement the other context-management methods. Remember to add state management to the memory and register methods you have already written! Here are the key considerations to be aware of when implementing these methods:

  • eventpointAdd_m( ) should support software breakpoints. If possible, support hardware breakpoints as well.

  • contextStep_m( ) should support stepping out of a range of addresses, and stepping just a single instruction. Remember to lock interrupts when stepping if the emulator does not automatically do so. If interrupts are not locked, stepping will always result in stopping in an interrupt service routine, because external interrupts typically continue to occur when a system is halted and will cause the program counter to acquire a value outside of the targeted address range.

  • When CrossWind requests a step-range operation, it expects execution to stop on the ending address specified. The ACE C API, however, stops when the program counter has stepped past the ending address. Consequently, Ace_T::contextStep_m( ) compensates for this by calling the ACE C API function with an ending address one less than the address specified by CrossWind.

  • Finally, the Tornado tools expect a breakpoint event to be generated when the step-range request completes. The ACE C API does not automatically generate such an event. Consequently, Ace_T::contextStep_m( ) must synthesize a breakpoint event.

See acecpu32Backend.cpp for a sample implementation.

Again, use wtxtcl to test the context-management methods. Download some code to the target; if possible, download VxWorks using existing emulator tools. Otherwise, download code which performs a process such as spinning in a loop incrementing a counter. Try setting breakpoints, continuing, stepping, etc. Make sure that the program counter changes appropriately.

Implement Event Handling

The only core feature which has not yet been implemented is event handling. Event handling allows the tools to become aware of target events such as hitting a breakpoint or generating an exception. There are several components to back-end event management:

Asynchronous Event Notification

The target server is notified of events asynchronously by activity on the event file descriptor specified in the TGT_OPS structure's tgtEventFd field. The target server monitors this file descriptor via select( ). When the file descriptor becomes active, the target server invokes eventGet_m( ) to get the WDB_EVT_DATA structure describing the event. It then clears the event by calling eventPendingClear_m( ). Typically, the event file descriptor is the socket file descriptor associated with a network connection to the emulator. In any case, the back end must implement the fdGet_m( ) method which returns an event file descriptor for the target server to monitor.

When an event occurs, the back end needs to capture the WDB_EVT_DATA information associated with the event and store it on a queue, because more than one event could occur before the target server handles them. The target server retrieves this data by calling eventGet_m( ).

Synchronous Event Notification

When necessary, the target server will explicitly query the back end to determine if an event has occurred by calling evtPending_m( ). This method should return TRUE if an event has occurred and has not yet been processed by the target server.

Capturing Event Information

Whenever an event occurs, the back end should capture the event's information in a WDB_EVT_DATA structure and store this information for the target server. The acecpu32 back end manages target information by using a call-back provided by the ACE C API to capture event information and put it on the event queue, Ace_T::eventQueue_. When a target event occurs, the ACE API invokes the call-back. The call-back constructs an Event_T object, which maintains the WDB_EVT_DATA information for the event, and puts it on the event queue. Note that Ace_T::eventCallBack( ) is a static member function so that the ACE C API can call it.

void Ace_T::eventCallBack 
    ( 
    ACE_ConnectionHandle emulator, 
    ACE_Event *       pEvent, 
    void *            pData 
    ) 
    {

Ace_T::pTheAceBkEnd_s( )provides a safe way to get a pointer to the Ace_T back end, without needing to pass the this pointer around.

    // Get a back end pointer 
    Ace_T *  pBackend  = pTheAceBkEnd_s ();

The Event_T constructor, shown below, captures the event information and creates the right kind of event.

    // Create an appropriate Event_T object 
    Event_T *   pWdbEvent = new Event_T (pEvent); 
 
    // Queue the event 
    pBackend->eventQueuePut_m (pWdbEvent); 
    }

The Rogue Wave Tools.h++ library makes the implementation of the queue mechanism easy. The event queue is a RWSlistCollectablesQueue and Event_T is derived from RWCollectable, so it is not necessary to write any code to implement and debug the event queue. (See Figure 2-7 and Figure 2-8.)

The Event_T constructor captures the event information by determining what kind of event occurred, gathering the necessary event data, and storing it:

Event_T::Event_T (ACE_Event * pEvent) 
    : 
    RWCollectable () 
 
    { 
 
    // Transfer pEvent into WDB_EVT_DESC 
    switch (pEvent->any.type) 
        { 
        case ACE_BREAKPOINT: 
            bpEventMake_m (pEvent); 
            break; 
        case ACE_SIGNAL: 
            signalEventMake_m (pEvent); 
            break; 
        case ACE_MODECHANGE: 
            modeChangeEventMake_m (pEvent); 
            break; 
        default: 
            wdbEvent_.evtType = WDB_EVT_NONE; 
            WPWR_LOG_ERR ("Invalid ACE event type %#x.\n",  
                        pEvent->any.type); 
            break; 
        } 
    }


*

CAUTION: Remember to return WDB_EXC_INFO in host byte order. Use the macros FIX_16 and FIX_32.

It is easy to implement eventGet_m( ), evtPending_m( ), and evtPendingClear_m( ) using the Event_T class and event queuing mechanism, because they only need to manage the event queue using the Rogue Wave member functions.

Exception Events

Exception events are more complicated to handle than other types, because it is necessary to provide information about the exception stack frame in the event data. The easiest way to gather exception information is to install an exception hook on the target to capture the data, notify the host, and suspend the system. For CPU32 microprocessors, this can be implemented using the special bgnd instruction which puts the target in background mode. If a similar notification scheme is not available on your host, you can set a breakpoint on your exception hook and, on every breakpoint event, check to see if it was triggered by the breakpoint in the exception hook.

The acecpu32 back end uses an approach based on the bgnd instruction. First, a special library, bdmExcLib, is linked with VxWorks; in the usrInit( )startup code, bdmExcLibInit( )is called to initialize the library and install the exception hook. When the bgnd instruction is executed, the ACE API generates an ACE_SIGNAL event, and the back end builds the appropriate exception event.

A global variable, bdmExcInfo, is used to store information about the exception so that the back end can easily locate this information and upload it to the host for processing.

BDM_EXC_INFO bdmExcInfo = {0, 0, 0, FALSE}; 
... 
STATUS bdmExcLibInit (void) 
 
    { 
    if (bdmIsInitialized) 
        return (ERROR); 
 
    if (_func_excBaseHook != NULL) 
        return (ERROR);

_func_excBaseHook is a hook to allow a developer to run code whenever an exception occurs. Whatever function's address is stored in it will be invoked by VxWorks' exception handler whenever the exception occurs. With the acecpu32 back end, the VxWorks exception-handling code invokes bdmExcHook( ) whenever an exception occurs, which gathers the necessary information about the exception for the back end.

    /* Install exception hook */ 
    _func_excBaseHook = bdmExcHook; 
    bdmIsInitialized  = TRUE; 
 
    return (OK); 
    } 
 
LOCAL int bdmExcHook  
    ( 
    int               vec,  
    char *            pESF,  
    WDB_IU_REGS *     pRegs 
    )

bdmExcInfo.bdmIsException is equal to TRUE only if the bgnd instruction is executed by the hook for gathering exception information. We gather the other necessary information about the exception, suspend the system, and notify the back end of the exception by executing the bgnd instruction. When we resume the system, we clear the bdmExcInfo.bdmIsException flag so that an exception can be differentiated from the execution of a user's bgnd instruction:

    { 
    bdmExcInfo.bdmIsException = TRUE; 
 
    bdmExcInfo.excVector       = vec; 
    bdmExcInfo.pESF            = pESF; 
    bdmExcInfo.pIuRegs         = pRegs; 
 
    __asm__ (".word 0x4afa"); /* `BGND' */ 
 
    bdmExcInfo.bdmIsException = FALSE; 
 
    /* Return FALSE so that VxWorks' exception handler will continue */ 
    /* processing the exception.                */ 
 
    return (FALSE); 
    }

Once an exception has occurred, until execution is resumed on the target, requests for register information should return the register information pointed to by pRegs rather than the current CPU register values. For an example, see Ace_T::regsGet_m( ) in acecpu32.cpp.

Testing

Once event handling has been implemented, CrossWind, Tornado's source-level debugger, should operate with the new back end. First test the back end's new functionality using wtxtcl. (Note: to register wtxtcl for an event use the wtxRegisterForEvent command. For more information, see 4. The WTX Protocol.) Then bring up CrossWind.

A typical CrossWind test session will proceed as follows:

  1. Start the target server.

  1. Invoke CrossWind:

% crosswind -t targetArch &
Use targetArch to specify your target's architecture, 68k, sparc, i960, etc.

  1. Attach CrossWind to the target server.


*

NOTE: The following steps show how to enter the commands in CrossWind's command panel. Some of these commands could also be performed using the GUI.

(gdb) target wtx targetServerName

  1. If the back end is slow, or if you are using a debugger to test the back end, increase the WTX timeout (specified in seconds) so that CrossWind will not timeout when issuing commands to the target server:

(gdb) tcl wtxTimeout 6000
The tcl command causes CrossWind to pass the remainder of the line to the wtxtcl interpreter. 60 is the default timeout.

  1. Emulators typically provide only system-level debugging, so put CrossWind in external debug mode:

(gdb) attach system

  1. Set the target's program counter to the address where you want to begin execution. You can use the symbol sysInit for the entry-point of bootable VxWorks:

(gdb) set $pc=sysInit
Or you can point directly to the address:

(gdb) set $pc=0x404000

  1. Set breakpoints at key milestones in VxWorks startup: usrInit( ), sysHwInit( ), usrRoot( ), and so on:

(gdb) b usrInit 
(gdb) b sysHwInit 
(gdb) ...

  1. It is also helpful to have usrRoot( ) spawn a demo application by editing the configuration file installDir/target/h/usrConfig.h (where usrRoot( ) is configured) as shown below:

Example 2-2:  Spawning a Demo Task

/* usrConfig.c - user-defined system configuration library */ 
 
... 
 
extern void usrLoop (int arg1, ...); 
 
usrRoot 
    ( 
    char *      pMemPoolStart,        /* start of system memory partition */ 
    unsigned    memPoolSize           /* initial size of mem pool */ 
    ) 
    { 
... 
 
#ifdef INCLUDE_SHELL 
... 
 
#endif  /* INCLUDE_SHELL */ 
 
    /* XXX - for BDM testing */ 
    taskSpawn ("uLoop", 200, 0, 10000, (FUNCPTR) usrLoop,  
               0,0,0,0,0,0,0,0,0,0); 
    } 
 
/* XXX - for BDM testing */ 
 
SEM_ID    loopSemId; 
int       loopCntGlobal; 
char *    loopString = "XXXThis is a string!\n"; 
int       loopDelay = 1; 
BOOL      causeExc = FALSE; 
 
void usrCauseExc (void) 
    { 
    volatile int top     = 1; 
    volatile int bottom     = 0; 
 
    top = top / bottom;    /* generate exception */ 
    } 
 
void usrLoop (int arg1, ...) 
    { 
    static   int loopCnt = 0; 
    volatile int ix      = 0; 
 
    loopString++; 
    loopString++; 
 
    if (loopSemId == NULL) 
        loopSemId = semCCreate (SEM_Q_PRIORITY, 1); 
 
    FOREVER 
        { 
        taskDelay (loopDelay); 
 
        printf ("\n...I am alive...Count = %d\n", ix); 
 
        ix++; 
        loopCnt         = ix; 
        loopCntGlobal    = ix; 
 
        semGive (loopSemId); 
 
        if (causeExc) 
            { 
            usrCauseExc (); 
            } 
        } 
    }
When you are ready to test your application's exception handling, change causeExec to TRUE to generate an exception.

  1. Start execution on the target:

(gdb) cont


*

NOTE: The WindSh and Browser should come up also, but they may not work correctly until the system is multitasking, in other words, until usrRoot( ) has been entered. Don't use them until the system is multitasking. Once this stage is reached, another key milestone is to get the i( ) command to work from the WindSh.

Compiling and Loading VxWorks for Debugging

Do not forget to compile VxWorks with the -g flag to generate debugging symbols for the system startup code.

Since VxWorks is a fully linked module, the default load flags for the CrossWind load command are not appropriate for loading it. The correct command from the CrossWind command line is:

(gdb) wtx-obj-module-load LOAD_GLOBAL_SYMBOLS|LOAD_FULLY_LINKED vxWorks

To simplify the process, you can create a new gdb command by placing the following file in $HOME/.wind/gdb.tcl:

# gdb.tcl - User customizations to CrossWind's GDB engine 
# 
# modification history 
# -------------------- 
# 
# 01a,07nov96,bss written. 
# 
 
# 
# loadf - load a fully linked object module 
# 
 
proc loadf (file) { 
    wtxObjModuleLoad LOAD_GLOBAL_SYMBOLS|LOAD_FULLY_LINKED $file 
} 
 
# register the new command with GDB 
 
gdb tclproc loadf loadf

2.4.5   Implementing Optional Member Functions

The basic back-end functionality is now operational. If you have the resources, you may decide to support the optional methods serviceCall_m( ), directCall_m( ), contextCreate_m( ), contextKill_m( ), and funcCall( ). You may also decide to implement some of the memory operations provided by Backend_T, such as memScan_m( ) or memChecksum_m( ), particularly if your emulator provides a faster way of performing these operations than Backend_T's generic implementation.

Dynamic loading of object modules onto the target requires, in addition to a high bandwidth memory access capability, support for cacheTxtUpdate_m( ) and directCall_m( ). The required emulator support for cacheTxtUpdate_m( ) may not be available. Implementing directCall_m( ) is complex: it involves saving the processor's state, setting up a stack for the call, performing the call, notifying the host that the call has completed, and then returning the system to its previous state. Consequently, most emulator back ends do not support dynamic loading.

2.4.6   Performance

Memory access is usually the biggest performance bottleneck. Everything possible should be done to optimize the speed of memory reads and writes. Also, optimize the register read and write operations for the case of a single register. Finally, if your emulator supports primitives for performing common memory operations such as checksum, scanning for a pattern, and filling a block of memory with a pattern, over-ride Backend_T's implementation and use methods optimized for your emulator.


1:  Many microprocessors provide a special debug state which allows a debugging tool to take advantage of on-chip debug functionality. For the CPU32 this mode is called BDM. The microprocessor is halted and put into a special state so that you can perform debug operations.