Semihosting

From SEGGER Wiki
Revision as of 09:37, 2 August 2019 by Johannes (talk | contribs) (Generic Handling)
Jump to: navigation, search

Semihosting is a mechanism that enables code running on an Embedded System (also called the target) to communicate with and use the I/O of the host computer.

Operations

Semihosting allows use of some of the host's resources, such as terminal I/O, File I/O and access to system information. Not all operations are supported by all target implementations and debuggers, and some are actually a security hazard, therefore potentially not implemented.

Common semihosting operations

Semihosting has been around a very long time. Some operations which can be performed with semihosting have proven to be more useful than others. A de-facto standard for (mostly) useful semihosting operations and their implementation has been defined by ARM. They are now widely adopted, also on non-ARM-based devices.

Terminal I/O

In most systems, the important part is Terminal I/O, or at least terminal output. This is the ability to send text to the host, to display it in a debugger console or terminal. In probably 90% of the cases, this is what is needed, for example for debugging with log output only.

This functionality is also (not accurately) referred to as printf functionality, since printf() requires an underlying implementation of terminal output. But printf() is more, as it also includes a formatter, which at run time creates the string to be output, thus significantly increasing the size of the program (typically by between 3 to 20 kB, depending on implementation and supported functionality). Since especially smaller embedded systems can often not afford this increase in program size, another semihosting operation, typically called Host-based formatting, is available in some implementations such as the SEGGER Run Time Library with Embedded Studio.

Semihosting operations defined by ARM

ARM defined semihosting operations, as well as groups for additional standard and user operations. Some of these operations are already obsolete, as they are not really useful in current embedded systems. Below is the list of operations as defined by ARM, along with the function number assigned:

File operations

  • SYS_OPEN (0x01) - Open a file or stream on the host system.
  • SYS_ISTTY (0x09) - Check whether a file handle is associated with a file or a stream/terminal such as stdout.
  • SYS_WRITE (0x05) - Write to a file or stream.
  • SYS_READ (0x06) - Read from a file at the current cursor position.
  • SYS_CLOSE (0x2) - Closes a file on the host which has been opened by SYS_OPEN.
  • SYS_FLEN (0x0C) - Get the length of a file.
  • SYS_SEEK (0x0A) - Set the file cursor to a given position in a file.
  • SYS_TMPNAM (0x0D) - Get a temporary absolute file path to create a temporary file.
  • SYS_REMOVE (0x0E) - Remove a file on the host system. Possibly insecure!
  • SYS_RENAME (0x0F) - Rename a file on the host system. Possibly insecure!

Terminal I/O operations

  • SYS_WRITEC (0x03) - Write one character to the debug terminal.
  • SYS_WRITE0 (0x04) - Write a 0-terminated string to the debug terminal.
  • SYS_READC (0x07) - Read one character from the debug terminal.

Time operations

  • SYS_CLOCK (0x10)
  • SYS_ELAPSED (0x30)
  • SYS_TICKFREQ (0x31)
  • SYS_TIME (0x11)

System/Misc. operations

  • SYS_ERRNO (0x13) - Returns the value of the C library errno variable that is associated with the semihosting implementation.
  • SYS_GET_CMDLINE (0x15) - Get commandline parameters for the application to run with (argc and argv for main())
  • SYS_HEAPINFO (0x16)
  • SYS_ISERROR (0x08)
  • SYS_SYSTEM (0x12)

Basic operation

Basically, the target CPU is halted, either by running into a breakpoint instruction or by some other operation which stops program execution and puts the target CPU under control of the debug-agent.
The debug agent can be either the debug probe (e.g. J-Link) or the debugger (such as GDB, SEGGER Ozone or the debugger integrated in Embedded Studio). The debug agent reads one or more register(s) to determine the type of operation to be performed by the host on behalf of the target, then performs the action, then restarts the target. The target is halted for the duration of the semihosting operation.

Ways to halt the target CPU

There are different ways to halt the target CPU, primarily special calls such as "Supervisor calls" or Breakpoints. Ideally, an implementation of semihosting is done in a way that when the system does not run under control of a debugger, it still works. This is really important for systems which use semihosting for things that are optional, not essential, such as debug output (printf) to the host. For example, ARM's Cortex-M implementation has the following problem: It uses a breakpoint instruction, which leads to a hard fault if the CPU is not running in debug mode. The way to handle this is using a special hardfault handler, which checks the source of the problem, and allows "skipping" of the BKPT instruction and resumption of program execution.

Implementation

Supervisor Call

As a reference for semihosting via supervisor call, refer to SEGGER_SEMIHOST_CortexA.

Legacy ARM cores, such as ARM7, ARM9, ARM11, as well as Cortex-A and Cortex-R devices typically use the supervisor call (SVC instruction, previously SWI) for semihosting. The debug agent sets a breakpoint or vector catch on the SVC handler. This typically halts the CPU at address 0x8, in SVC mode.

The debugger checks whether the instruction triggering the SVC call is the semihosting instruction, by looking at the parameter, the SVC number (0x123456 when entering from ARM mode, 0xAB when entering from Thumb mode). If it is a different number, program execution continues. If it is, the debugger performs the semihosting operation, then resumes program operation by copying the values saved in SPSR_SVC and LR_SVC to CPSR and PC, identical to an exception return. If the supervisor call does not trigger semihosting, execution is continued in the SVC handler.

In ARM Mode the SVC number is 24 bit wide. The standard SVC number for semihosting is 0x123456:

SVC 0x123456

In Thumb mode the SVC number is only 8 bit wide. The standard SVC number for semihosting is 0xAB:

SVC 0xAB

ARM SVC Handler

To ensure that the target application keeps running even when no debugger is connected, an SVC handler should be implemented. When SVC is used for semihosting only, the SVC handler can simply do an exception return to continue execution in the application.

SVC_Handler:
        subs PC, LR, 0  // Perform exception return

When the application uses supervisor calls for semihosting as well as for any other operation, the SVC handler should check whether a call should have triggered semihosting or not.

In case semihosting should have been triggered, the application might do a simple exception return. When the SVC handler has been called for any other reason, the application might handle this accordingly.

As the debugger usually sets a breakpoint or vector catch on the SVC vector, the target application halts on every SVC, for semihosting operations and for other operations. To avoid this, some debuggers, such as Ozone, can be configured to break on an instruction or function called by the SVC handler. In this case the SVC handler always runs without break on entry and checks whether to do semihosting or not. Refer to the example implementation below.

SVC_Handler:
        push {R2,R3}
        //
        // Check whether we come from Thumb or ARM mode.
        // Application's CPSR is saved in SPSR.
        //
        mrs R2, SPSR
        tst R2, #0x20
        beq _CheckSemiARM               // SPSR.T (Bit 5) not set -> ARM mode
_CheckSemiThumb:
        //
        // Load immediate of Thumb SVC instruction
        //
#if BIG_ENDIAN
        ldrb R2, [LR, #-2]
#else
        ldrb R2, [LR, #-1]
#endif
        //
        // Check if it has been a semihosting SVC
        //
        ldrb R3, =0xAB
        cmp R2, R3
        pop {R2,R3}                     // Restore registers (might be used by semihosting or SVC handler)
        bne _DoSVC                      // No semihosting SVC. Continue in SVC handler
        b _DoSemihost                   // Semihosting SVC. Run through debugger breakpoint and return
_CheckSemiARM:
        //
        // Load immediate of ARM SVC instruction
        //
        ldr R2, [LR, #-4]
        bic R2, R2, #0xFF000000
        //
        // Check if it has been a semihosting SVC
        //
        ldr R3, =0x00123456
        cmp R2, R3
        //
        // Restore registers (might be used by semihosting or SVC handler)
        //
        pop {R2,R3}
        bne _DoSVC                      // No semihosting SVC. Continue in SVC handler
        //b _DoSemihost                   // Semihosting SVC. Run through debugger breakpoint and return
_DoSemihost:
       //
       // Call semihosting handler for the debugger to set a breakpoint on
       //
       blx SEGGER_SEMIHOST_DebugHalt
       //
       // Perform exception return to instruction after SVC.
       // Also resets CPSR.
       //
       subs PC, LR, 0
_DoSVC:
        //
        // Stay in endless loop
        // User SVC handler could start here instead
        //
        b _DoSVC
        .end

Software Breakpoint

As a reference for semihosting via software breakpoint, refer to SEGGER_SEMIHOST_CortexM.

Cortex-M devices can use the software breakpoint instruction (BKPT) instead of the supervisor call.

When a debug probe is connected, the target halts on execution of the BKPT. The debugger checks whether the breakpint should trigger semihosting or not. If it should, it performs the semihosting operation, skips the BKPT instruction, and lets the target run.

Semihosting via software breakpoint is triggered by the parameter which can be encoded with the BKPT instruction.

BKPT is always a 16 bit instruction with 8 bit parameter. The standard parameter for semihosting is 0xAB:

BKPT 0xAB

Cortex-M HardFault Handler with Semihosting

The disadvantage of the BKPT instruction is, that when no debugger is connected, a hard fault error occurs. The target needs to recover from the hard fault in order to continue execution.

HardFault_Handler:
        //
        // This version is for Cortex M3, Cortex M4 and Cortex M4F
        // Copy previously selected stack pointer to R0.
        //
        tst    LR, #4           // Check EXC_RETURN in Link register bit 2.
        ite    EQ
        mrseq  R0, MSP          // Stacking was using MSP.
        mrsne  R0, PSP          // Stacking was using PSP.
        //
        // Check if hardfault has been caused by software break.
        // Otherwise stay in HardFault handler.
        //
        ldr    R1, =0xE000ED2C  // NVIC_HFSR
        ldr    R2, [R1]
        lsls   R2, R2, 1        // Shift Bit 31 into carry.
_Loop:  bcc    _Loop            // Carry == 0? Hardfault has been caused by other reason. Stay in endless loop.
        //
        // Reset hardfault status
        //
        str    R2, [R1]         // NVIC_HFSR = NVIC_HFSR
        //
        // Modify return address to return to next instruction after BKPT
        //
        ldr    R1, [R0, #24]    // R0 == Current SP
        adds   R1, #2
        str    R1, [R0, #24]    // *(pStack + 6u) += 2u;
        //
        // Return
        //
        bx     LR
  .end

Generic Handling

As a reference for generic semihosting, refer to SEGGER_SEMIHOST_Generic and SEGGER_SEMIHOST_Generic_RISCV.

Supervisor calls and software breakpoints are target-depentent implementations for semihosting. Some architectures do not implement such functionality. For example the RISC-V instruction set includes a breakpoint instruction, but it does not take an extra parameter which could identify it as a semihosting call.

When the developer might need terminal I/O for debugging, a debugger can provide generic handling for semihosting, which does not rely on any target-specific features. This can also be used instead of the target-dependent implementation, for example to avoid the SVC vector catch on Cortex-A devices.

The target application prepares the operation as usual, but instead of causing the break itself, it calls a function, such as SEGGER_SEMIHOST_DebugHalt(). To the target application this handling is generic. It has to only pass the semihosting parameters and return the return value in the function.

When a debugger, such as Ozone, is connected, it automagically sets a breakpoint in the entry of the function, and handles semihosting when it breaks there. While the implementation is generic for the target, the debugger has to know about the calling convention and ABI of the target appliction, to know how the parameters are passed.

When no debugger is connected, the target application simply returns from the function. If necessary it can check whether a debugger is connected or not (for example by calling SEGGER_SEMIHOST_IsConnected).

Generic Handler (for Ozone)

The following function is recoginzed by Ozone for semihosting.

/*********************************************************************
*
*       SEGGER_SEMIHOST_DebugHalt()
*
*  Function description
*    Dummy call for generic implementation.
*    The debugger may set a breakpoint on this function, 
*    handle the semihosting request,
*    and return to the caller.
*
*  Return value
*    Return parameter r0 if debugger is not connected.
*/
int __attribute__((noinline)) SEGGER_SEMIHOST_DebugHalt(int r0, int r1) {
  (void)r1;   // Avoid unused parameter warning
  return r0;
}

Using Semihosting with J-Link

Semihosting with Embedded Studio

The implementation of semihosting in Embedded Studio is straight forward. Standard library functions for fopen, fwrite, fread etc. are already implemented and just need to be called. The project wizard is recommended when creating a new project so that all default settings are set correctly.

The following example project opens a file on your host system and writes a string into it triggered by your target device: Semihosting Embedded Studio

Make sure the file path exists, otherwise the project will not run. You can of course change the path by simply editing variable pFilename and rebuild it with Embedded Studio.

Prerequisites for the example project are:

  • Embedded Studio V4.16 or later
  • Cortex-M Trace Reference Board (ST STM32F407VE)
  • J-Link V10 or later

Baremetal Semihosting with Ozone

Ozone is a debugger so it does not come with standard libraries and predefined semihosting code like it does with some IDEs. Thus to make use of semihosting the semihosting calls need to be implemented manually by the user in the target application. The big benefit of this approach without predefined library functions is that you get full control about the complexity of the semihosting implementation so you can write more efficient code as the library overhead is missing.

A baremetal example implementation can be found in the following example project: Semihosting Ozone

The example project will open a file on the host system and write a string to it. Make sure the file path is existing otherwise the project will not run. You can of course change the path by simply editing variable pFilename and rebuild it with Embedded Studio.

Prerequisites for the example project are:

  • Ozone V2.62 or later
  • Embedded Studio V4.16 or later
  • Cortex-M Trace Reference Board (ST STM32F407VE)
  • J-Link V10 or later

This baremetal code can be used with any debug software which supports semihosting handling.

Semihosting with newlib and GNU Arm toolchain

This section will cover how semihosting can be used with Eclipse in combination with GDB and newlib. Generally newlib will only provide a rudimentary setup for semihosting so it will not work out-of-thebox except for printf calls. All callbacks used by e.g. fopen need to be implemented by the user to utilize semihosting.

The following project will show such an example project and create a file at a certain path, fill it with a string and close that file: Semihosting Eclipse

Make sure the file path is existing otherwise the project will not run. You can of course change the path by simply editing variable pFilename and rebuild it with Eclipse. As a base project a default J-Link debug project was created for Cortex-M. Then a debug config with the correct target device was set up. Next make sure that in the debug config semihosting is enabled.

The example project comes also with an Ozone setup that runs out-of-the-box should Eclipse not be the debugger of your choice.

Prerequisites for the example project are:

  • Eclipse Cpp 2019-03
  • Latest GNU ARM Eclipse plugin
  • Cortex-M Trace Reference Board (ST STM32F407VE)
  • J-Link V10 or later
  • (optional) Ozone V2.62 or later

Semihosting with EWARM

The implementation of semihosting in EWARM is generally available but requires some extra settings in your project. Standard library functions for fopen, fwrite, fread etc. are implemented and just need to be called. Under project options semihosting must be enabled as well as the library support must be set to "full" which will drastically increase memory usage of your project.

The project options can be found at the following project settings:

EWARM Library setting.png

Set the library level to "Full" and the library low level implementation to semihosting each.

Note: This will also increase your application size immensely so make sure your target device has enough memory resources left.

The following example project opens a file on your host system and writes a string into it triggered by your target device: Semihosting EWARM

Make sure the file path is existing otherwise the project will not run. You can of course change the path by simply editing variable pFilename and rebuild it with Embedded Studio.

Prerequisites for the example project are:

  • EWARM V8.322 or later
  • Cortex-M Trace Reference Board (ST STM32F407VE)
  • J-Link V10 or later