Semihosting: A Cautionary Tale
Some of us were rambling about semihosting the other night and what a pain it can be. “What’s semihosting,” you ask? Why, it’s this.
In a nutshell, it’s a way to redirect stdio to the debugger. This lets you call printf() from within your embedded target and watch the output magically appear inside a console window inside your IDE. No serial ports, serial port drivers, or USB-to-RS232 adapters needed.
“But that sounds awesome! Why is this a pain?” you interrobang.
Well, while it’s nice for development, it’s yet another thing you have to figure out how to correctly deal with for production. Semihosting, by definition, requires a debugger attached. Without a debugger attached, semihosted applications often flat-out refuse to run. Unless you expect your customers to have debuggers attached while operating your product, you can’t expect semihosted code to work properly in your customers’ hands.
There was once a time when standard library writers asked, “what the heck should printf() do on an embedded system? Is there a UART? If there is, should printf() write to it? What if the UART is connected to hardware other than a console? Writing debug messages to that hardware might be bad. Let’s not do that. In fact, let’s punt on this. Let’s make printf() call a low-level function named putc(), which will be responsible for putting a character where it belongs.”
Then they asked, “Should we provide a default implementation of putc()? Maybe it could do nothing and the user could override it.”
“No,” they decided. “It’d be confusing if the default implementation did nothing. We can imagine many thousands of hours lost to debugging ‘Hello, world’ apps that do nothing because the developer didn’t realize they needed to override putc() to make it print to their serial port.”
“No,” they said. “We’ll just leave putc() undefined. If the developer uses printf() without defining putc(), we’ll cause a linker error. Thus, the developer will be forced to Gopher it, learn about our retargeting layer, and decide what the heck putc() should do on their system.”
And thus, for thousands of years, developers wrote embedded “Hello, world” applications that threw linker errors when built. Said developers gophered/expertsexchanged/googled/stackoverflowed the solution, and realized they didn’t understand retargeting. So, they learned it, realizing quickly that they simply needed to wrap the serial driver they’d probably already written with a putc() layer.
But today, young library writers–discarding thousands of years of history–think they can do better than these relics with their undefined symbols. “In these modern times,” they say, “developers want more to happen out of the box. A developer should be able to push ‘play’ in their IDE and observe immediate results–even on an embedded system. It’s obvious what we must do. We must make semihosting the default behavior.”
And so, they implemented standard libraries with default putc() implementations that assumed that the debugger is attached. Millions of embedded developers excitedly developed and debugged applications to perfection with amazing diagnostic tools that worked out of the box.
Those millions of developers then detached their debug adapters and handed their prototypes to their bosses for the “big demo”. The bosses attempted to operate the prototypes, but they refused to work, so they returned the prototypes to the developers with looks of disappointment and stern warnings. The developers then spent billions of hours (combined) diagnosing the problem.
It looks a bit like a timing issue–since the prototypes work fine with the debugger attached. But after futzing around with diagnostic printf()s to try to determine where their programs are crashing, the developers pull their hair out when not a single debug statement makes it out their serial ports. Suddenly, they remember that when they’re debugging, the debug output doesn’t appear on their serial ports–it appears in a special window in their debuggers. They vaguely wonder how that works, but they think it’s probably unrelated to their timing crashes.
Resigned and demoralized, the developers resort to bit-banging numbers onto GPIOs around every statement in their programs. They discover that their programs are actually crashing on the first printf(). Confused, they double-check their serial drivers, and find no problems. More GPIO-toggling later, they realize that printf() isn’t even invoking their serial drivers. Stumped, they turn to Google, where they eventually learn about semihosting and how to solve their problem. They curse the standard library writers for defaulting the behavior to something that can only work with the debugger attached. They fix the problem, leave the prototypes on their bosses’ desks, and get to bed around 4am; the stress of the day shortening their expected lifespan by roughly 1%.
And, thus, literally trillions of hours of developers’ lives are lost each year to a poor decision.