Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Hello World via ARM Semihosting
How a BKPT #0xAB instruction halts the CPU, lets the debug host execute a file operation, and returns the result — making your MCU borrow the host’s I/O facilities.
1. Overview — What This Example Does
The 002_HelloWorld_semihosting project prints “Hello world” to the host computer’s console via ARM semihosting. Unlike ITM/SWV (which uses a dedicated hardware trace path), semihosting uses the debug interface itself (SWD/JTAG) and requires the processor to halt to service each I/O request.
The single key difference from Example 01 is the call to initialise_monitor_handles() before printf(). This function, provided by the rdimon library, sets up the semihosting C library streams (stdin, stdout, stderr) via semihosting file-open calls.
2. Source Code — Annotated Walkthrough
#include <stdio.h>
/* Prototype for the semihosting initialisation function
* provided by librdimon (-lrdimon linker flag) */
extern void initialise_monitor_handles(void);
int main(void)
{
/* MUST call this before any printf() when using semihosting.
* It opens stdin/stdout/stderr via semihosting SYS_OPEN calls.
* Without it, the C library file descriptors point to null
* and nothing will appear in the host console. */
initialise_monitor_handles();
printf("Hello world\n");
for(;;);
}
What initialise_monitor_handles() Does
This function, from the rdimon (Remote Debug Interface Monitor) library, issues three semihosting SYS_OPEN calls — one each for stdin, stdout, and stderr. Each call generates a BKPT #0xAB instruction, halting the CPU and transferring control to OpenOCD (or any ARM debug agent), which executes the open call on the host file system and returns file handles back to the MCU’s registers.
/* Simplified pseudocode of what initialise_monitor_handles() does */
void initialise_monitor_handles(void)
{
/* SYS_OPEN for ":tt" (stdin) — host opens keyboard input */
__semihosting_call(SYS_OPEN, ":tt", MODE_READ); /* returns fd=0 */
/* SYS_OPEN for ":tt" (stdout) — host opens terminal output */
__semihosting_call(SYS_OPEN, ":tt", MODE_WRITE); /* returns fd=1 */
/* SYS_OPEN for ":tt" (stderr) — host opens error output */
__semihosting_call(SYS_OPEN, ":tt", MODE_WRITE); /* returns fd=2 */
}
3. How ARM Semihosting Works
Semihosting is defined in the ARM ABI (Application Binary Interface) specification. It uses a special breakpoint instruction to signal the debug host that a system call is needed.
The BKPT #0xAB Instruction
When the processor executes BKPT #0xAB (opcode 0xBEAB in Thumb), it causes a DebugMonitor exception (or halts if a hardware debugger is connected). The magic value 0xAB is the semihosting magic number recognised by debug agents like OpenOCD, QEMU, and Arm’s Fast Models.
Register Convention for Semihosting Calls
| Register | Role | Example (SYS_WRITE0) |
|---|---|---|
| R0 | Semihosting operation number | 0x04 = SYS_WRITE0 |
| R1 | Pointer to parameter block or direct value | Pointer to null-terminated string |
The semihosting call sequence in assembly:
/* Generic semihosting call in ARM Thumb assembly */
semihosting_call:
PUSH {R0, R1, R2, R3, IP, LR}
/* R0 = operation number, R1 = parameter block pointer */
BKPT #0xAB /* ← This is the magic instruction */
/* After the debug agent handles the call:
* R0 contains the return value */
POP {R0, R1, R2, R3, IP, PC}
Semihosting Call Flow
printf("Hello\n") in application code_write(1, "Hello\n", 6)_write() sets R0=SYS_WRITE(0x05), R1=param block, executes BKPT #0xABCommon Semihosting Operation Codes
| Op Code | Name | Description |
|---|---|---|
| 0x01 | SYS_OPEN | Open a file on the host (“:tt” = console) |
| 0x02 | SYS_CLOSE | Close a host file |
| 0x03 | SYS_WRITEC | Write a single character to the debug console |
| 0x04 | SYS_WRITE0 | Write a null-terminated string |
| 0x05 | SYS_WRITE | Write n bytes to a file descriptor |
| 0x06 | SYS_READ | Read bytes from a host file |
| 0x08 | SYS_ISTTY | Check if fd is a TTY |
| 0x09 | SYS_SEEK | Seek within a file |
| 0x0A | SYS_FLEN | Get file length |
| 0x18 | SYS_EXIT | Terminate application (signals debug agent) |
| 0x30 | SYS_ELAPSED | Get elapsed execution time |
4. Setup Requirements
Linker Flags
rdimon library. Add these flags in the IDE’s linker settings:
-specs=rdimon.specs -lc -lrdimon
Also: remove syscalls.c from the project. rdimon provides its own _write(), _read() stubs that use semihosting. Having both will cause multiple-definition linker errors.
OpenOCD Configuration
In your debug configuration (GDB script or OpenOCD startup command), add:
# In OpenOCD startup commands (Debug Configuration → Startup)
monitor arm semihosting enable
Without this command, OpenOCD will not intercept the BKPT #0xAB instruction and the MCU will HardFault.
IDE Steps (STM32CubeIDE)
- Right-click project → Properties → C/C++ Build → Settings → Tool Settings → Linker → Miscellaneous
- Add
-specs=rdimon.specs -lc -lrdimonto “Other linker flags” - Remove
syscalls.cfrom the project - Debug Configuration → Startup → Run Commands: add
monitor arm semihosting enable - Run → output appears in the “Console” or “OpenOCD” view
Debug Configuration Startup Script
# Example .cfg / launch startup commands
target extended-remote localhost:3333
monitor reset halt
monitor arm semihosting enable
load
monitor reset halt
continue
5. rdimon Library — What It Provides
rdimon (Remote Debug Interface Monitor) is the semihosting C library provided by arm-none-eabi-gcc. When you link with -specs=rdimon.specs, the linker includes a full set of syscall implementations that route all I/O through BKPT #0xAB.
| Function | Semihosting Call Used |
|---|---|
printf() | SYS_WRITE (via _write → SYS_WRITE op 0x05) |
scanf() | SYS_READ (op 0x06) |
fopen() | SYS_OPEN (op 0x01) |
malloc() | Uses _sbrk which reads linker symbols for heap |
exit() | SYS_EXIT (op 0x18) |
clock() | SYS_ELAPSED (op 0x30) |
Writing a Manual Semihosting Call (no library)
/* Direct semihosting call — no rdimon needed */
static inline int __semihosting(int op, void *arg)
{
int result;
__asm volatile (
"MOV R0, %1\n" /* op number */
"MOV R1, %2\n" /* parameter block */
"BKPT #0xAB\n" /* trigger semihosting */
"MOV %0, R0\n" /* get return value */
: "=r"(result)
: "r"(op), "r"(arg)
: "R0", "R1", "memory"
);
return result;
}
/* SYS_WRITE0: print a null-terminated string */
void sh_puts(const char *str)
{
__semihosting(0x04, (void*)str);
}
/* Usage */
int main(void)
{
initialise_monitor_handles();
sh_puts("Direct semihosting: no printf\n");
for(;;);
}
6. Semihosting vs ITM — Detailed Comparison
| Feature | Semihosting | ITM / SWV |
|---|---|---|
| Mechanism | BKPT #0xAB halts CPU, debug agent services request | CPU writes to ITM PORT[0], hardware serialises to SWO pin |
| CPU halted? | Yes — CPU stops for every character | No — CPU writes and continues (FIFO polling only) |
| Speed | ~1,000 chars/second typical | Up to 4 Mbps (millions of chars/second) |
| Extra pin needed? | No — uses existing SWD pins | Yes — SWO (PB3) must be available |
| Breaks real-time? | Yes — completely | No — only FIFO stall if overflow |
| Linker setup | -specs=rdimon.specs -lc -lrdimon | Custom _write() in syscalls.c |
| Debug agent needed? | Yes — OpenOCD must enable semihosting | IDE SWV viewer only |
| File I/O support? | Yes — can read/write host files | No |
| Production use? | Never | Possible (disable in release) |
| Best for | Quick printf when no SWO pin, file I/O in tests | Non-intrusive trace, profiling, real-time logging |
7. Extended Examples
Reading Input via Semihosting
#include <stdio.h>
extern void initialise_monitor_handles(void);
int main(void)
{
initialise_monitor_handles();
int n;
printf("Enter a number: ");
scanf("%d", &n); /* reads from host keyboard via SYS_READ */
printf("You entered: %d\n", n);
for(;;);
}
Writing to a Host File via Semihosting
#include <stdio.h>
extern void initialise_monitor_handles(void);
int main(void)
{
initialise_monitor_handles();
/* fopen uses SYS_OPEN semihosting call to open a real file on the host */
FILE *f = fopen("output.txt", "w");
if (f)
{
fprintf(f, "Log entry from MCU\n");
fclose(f);
printf("File written on host!\n");
}
for(;;);
}
Semihosting Exit (Used in Testing/CI)
/* Force a clean exit via semihosting — useful for automated testing */
void semihosting_exit(int code)
{
/* SYS_EXIT = 0x18, ADP_Stopped_ApplicationExit = 0x20026 */
uint32_t params[2] = { 0x20026, (uint32_t)code };
__semihosting(0x18, params);
}
8. Interview Questions & Answers
Q1. What is ARM semihosting and how does it work?
BKPT #0xAB instruction (Thumb opcode 0xBEAB). When executed, this instruction causes the processor to enter a fault/debug state. The debug agent (e.g., OpenOCD) detects this, reads the semihosting operation code from R0 and parameters from R1, executes the corresponding host-side operation (file open, write, read etc.), stores the return value in R0, and resumes the MCU. It requires the debug agent to have semihosting explicitly enabled (e.g., monitor arm semihosting enable in OpenOCD).Q2. What happens if semihosting code runs without a debugger connected?
BKPT #0xAB instruction generates a HardFault exception when no debugger is connected. This will cause the program to crash (unless you have a HardFault handler). This is a critical reason why semihosting must NEVER be left in production firmware. The presence of semihosting calls can make a product fail in the field if the debugger is not attached. Always remove semihosting code before production release, or wrap it in a macro that compiles out (e.g., #ifdef DEBUG).Q3. What is the purpose of initialise_monitor_handles()?
initialise_monitor_handles() is provided by the librdimon semihosting library. It opens the three standard streams (stdin=0, stdout=1, stderr=2) via three SYS_OPEN semihosting calls on the virtual terminal ":tt". Without calling this, the newlib C runtime’s internal file descriptors for stdin/stdout/stderr are uninitialised. Any subsequent printf() will try to write to an invalid file descriptor and nothing will appear. This call must come before any C standard I/O function.Q4. What is the -specs=rdimon.specs linker flag?
-specs=rdimon.specs is a GCC spec file that instructs the linker to: (1) include the librdimon.a library which provides semihosting implementations of all newlib syscalls (_write, _read, _open, _sbrk, _exit etc.), and (2) configure the startup code to support semihosting. The rdimon stands for Remote Debug Interface Monitor. It replaces the standard nosys stubs with semihosting-based implementations. You must also link -lc (libc) and -lrdimon explicitly in some toolchain versions.Q5. Why can’t syscalls.c and rdimon.specs be used together?
_write(), _read(), _sbrk(), _exit() etc. Having two implementations of the same symbol causes a multiple definition linker error. The syscalls.c stubs are weak symbols, but if rdimon provides strong definitions, the weak ones in syscalls.c can cause conflicts. The solution is simple: remove syscalls.c from the project when using semihosting, and add it back when switching to ITM output.Q6. What is the BKPT instruction and how is it normally used?
BKPT #imm8, Thumb-32 not directly available for semihosting). Normally it is used by debuggers to insert software breakpoints: the debugger replaces an instruction at a breakpoint address with BKPT, and when the CPU hits it, execution halts and control transfers to the debugger. The immediate value can carry information: BKPT #0 is a standard debug breakpoint; BKPT #0xAB is the ARM-defined semihosting magic value that debug agents specifically watch for to intercept semihosting calls rather than treating it as a normal breakpoint.Q7. How does the debug agent know it’s a semihosting call and not a regular breakpoint?
0xAB. When OpenOCD (or any ARM debug agent) intercepts a BKPT exception, it checks the immediate value of the instruction. If it is 0xAB, it treats it as a semihosting request and reads R0 (operation code) and R1 (parameter). If the immediate is anything else (e.g., 0x00 for a regular breakpoint), the debug agent treats it as a normal breakpoint and halts execution for inspection. This convention is defined in the ARM Semihosting specification (ARM IHI0062).Q8. Why is semihosting so much slower than ITM?
1. CPU executes BKPT — takes a few nanoseconds
2. CPU halts — debug agent must detect the halt (~10–100 microseconds via USB polling)
3. Debug agent reads MCU registers (several SWD transactions)
4. Debug agent executes the host-side write() system call
5. Debug agent writes return value to MCU R0
6. Debug agent resumes MCU execution
The round-trip through USB, operating system scheduler, and SWD protocol means each semihosting call takes roughly 100–1000 microseconds. ITM by contrast is a hardware FIFO that the CPU writes to with a single memory write instruction — taking ~3–10 ns with no halting. For a 100-byte printf, semihosting might take 50ms while ITM takes under 1ms.
Q9. Can semihosting be used in RTOS applications?
Q10. What is the “:tt” file name in SYS_OPEN and what does it represent?
":tt" is a special semihosting pseudo-file name defined in the ARM semihosting specification. It represents the “terminal” or “console” device of the host. When you open ":tt" for writing, the semihosting agent maps it to stdout of the debug host. When opened for reading, it maps to stdin (host keyboard). The colon prefix (“:”) distinguishes it from regular host filesystem paths. This is how initialise_monitor_handles() connects newlib’s stdin/stdout/stderr to the debug console — it calls SYS_OPEN with ":tt" three times.Q11. How would you implement semihosting without any library?
/* SYS_WRITE0 = 0x04: print null-terminated string */
void sh_print(const char *s) {
__asm volatile (
"MOV R0, #0x04\n" /* operation: SYS_WRITE0 */
"MOV R1, %0\n" /* R1 = pointer to string */
"BKPT #0xAB\n" /* semihosting call */
: : "r"(s) : "R0","R1"
);
}This removes the need for rdimon.specs, initialise_monitor_handles(), and syscalls.c. It is useful when you want the smallest possible semihosting footprint for unit tests or CI automation.Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments