A Personal History of WORA — Part II: Debuggers and Visual C++

David W. Gray
7 min readSep 4, 2023

--

One essential aspect of WORA (Write Once Run Anywhere) that is too often overlooked is that you really need to be able to debug your app on all the targets that you intend to ship it on.

This post is part of a series where I’m alternating between digging into modern multi-platform frameworks to see how effective they are and sharing some of my memories of how these platforms came to be. My last post discussed my early introduction to WORA and LOWA (Learn Once Write Anywhere). Today, I’d like to discuss one of my early contributions to WORA.

I started full-time at Microsoft in 1989, working on the CodeView team. CodeView was Microsoft’s CHUI¹ (CHaracter User Interface) source-level debugger. This was an amazing little program. I had almost no experience using debuggers at the time, much less understanding how they worked, so I was both overwhelmed and excited to jump into learning how debuggers worked.

CodeView was one monolithic executable written in C and x86 assembly language. It was fully rebuilt for each target platform that it supported. At the time I started on the team, if I recall correctly, we were shipping a DOS version and OS/2 and Windows versions were under active development. CodeView also had built-in support for all the languages that Microsoft was shipping at the time. Again, purely from memory, this list was C, BASIC, PASCAL, and COBOL. However, now that I think about it, I believe COBOL was shipped by a third party that we had a tight enough relationship with that we incorporated support into the debugger.

The problem we were running into as the company grew and the industry evolved was that one monolithic application for debugging wasn’t in the cards. On the one hand, the GUI revolution was well underway, and the idea of having an IDE that included debugging capabilities was front of mind. On the other hand, the number of languages we needed to support was getting out of control, and it would be great if the compiler teams could take ownership of the expression evaluator for their languages — especially if that team was outside of the company. And, on the gripping hand, there was an increasing push to enable remote debugging, including being able to remote debug to other platforms like Apple’s Macintosh and other systems built on different processors. The processor matrix notably included the transition between 16-bit and 32-bit x86 architectures.

The way we were supporting different flavors of the product was by sprinkling #ifdefs throughout the code and recompiling the entire project to create an executable that ran on a specific target operating system and supported one language. We called this “#ifdef hell” and were anxious to escape it as quickly as possible.

We spent a very intense couple of years in the early nineties pulling CodeView apart and creating a pluggable system that answered all of these demands and a couple more. A key part of that system was that each component was built as a .dll (dynamic link library) and registered with the system. This way, you wouldn’t have to load the BASIC expression evaluator if you were just coding in C++ or vice versa. And the core debugger components could be loaded into different applications. We enabled the debugger initially in QuickC and then Visual C++. For at least one release, we were shipping a full-featured standalone debugger (CodeView) alongside a GUI IDE (Visual C++) that didn’t have the full scope of debugging features but was, for the time, an amazingly smooth integration between coding, building, and debugging.

The system was relatively straightforward: A debug monitor (DM) always ran on the machine that the debuggee² (the target of debugging) was running on and had to be built to target that operating system and processor architecture. There was a native execution model (NEM) that contained a full disassembler and “understood” the target’s processor architecture, including what needed to be done to do things like set breakpoints, walk call stacks, and intercept exceptions. There was a transport layer that connected the NEM and the DM. The first two transport layers supported communication over a serial port and named pipes. For local debugging, we built a single .dll that collapsed the transport layer, NEM and DM into a single .dll.

There were all kinds of crazy challenges that we needed to address with this architecture. One fun challenge was that we were working in C (with a sprinkling of processor-specific assembly language in the DM) since this was when other parts of the team were actively working to build Microsoft’s first C++ compiler and it was too early to dogfood the new compiler. So, we built our own vtable mechanism to allow us to dynamically subclass things like expression evaluators and execution models. Another challenge was that while Windows and OS/2 (and then NT) supported .dlls, DOS didn’t. So, we got to write our own .dll loader for DOS.

There was another requirement that wasn’t evident when we started this project but that we had the foresight to enable. I believe we managed this in large part because of my early obsession with WORA variants. The requirement was to support P-code debugging. Yes, the Microsoft P-code that the applications teams had been using since the early 80s came around, and my team, which was in the Systems division at the time, had to support debugging applications running under the P-code interpreter. This was a lot of fun (I’m not using the word fun sarcastically — I recently had a conversation with the engineer who wrote the bulk of that code, and it is evident that he remembers the experience fondly). We had set up the system so that you could layer an arbitrary number of execution models on top of the native execution model for a particular system. So, we wrote a P-code execution model. Since Microsoft implemented P-code as an interpreter, we could detect if the native IP (instruction pointer) was running in the interpreter and give control to the P-code EM. The P-code EM then used the native execution model to read memory and grok the internal structures of the P-code interpreter. This let us figure out what the virtual IP for the p-code was, as well as the stack and other key data. We could write a breakpoint (again using the NEM) at the correct place in the interpreter to allow us to single step.

Ultimately, we had one set of code that could drive debugging for multiple tools — CodeView, QuickC, Visual C++, and eventually WinDbg, which replaced CodeView as the full-featured standalone debugger using this architecture. We supported C++, C, BASIC, and several other languages via pluggable expression evaluators. We supported several processor architectures, x86 (both 16 and 32-bit), 68k, briefly the i960, PowerPC, and eventually IA64 and x64. We could run natively on DOS, Windows, OS/2, and NT and remotely on the Mac. So developers writing code in our system could deploy their program to many different operating systems running on many different processor architectures and debug it.

In all, that was probably my biggest individual contribution to WORA, and we’re still talking about times before the term had officially been coined. Unfortunately, I became frustrated with the lack of resources devoted to porting what I considered essential features to the IDE version of the debugger and the termination of a standalone debugger. I moved on shortly after we had the system in place. My biggest lesson from that experience (other than that business needs at a company of any size trump technical vision more often than not) was that documentation is essential. While I did get some of the architecture down on paper, enough of the team had moved on by the time the technical challenges of writing a Java and then a .NET debugger came along that the new folks wrapped the components I worked on in a black box and solved some of the same problems in new ways.

Next week, I’ll return to the more practical and modern version of WORA and LORA with React Native — the site that uses LORA in its marketing.

¹ CHUI was a means of simulating a Windows-like environment using just the extended ASCII set available on the PC. I’m having difficulty finding a good reference for the term. The relevant Wikipedia article about Text-based user interface calls this kind of interface TUI, but we were definitely using the term CHUI at the time.

² This is probably a stretch, but we may have coined the term “debuggee” during that era. It’s still common for folks who don’t work on debuggers to talk about the “target of the debugger,” which is quite cumbersome. But the only way I’ve seen to verify first use of a word is via the OED, and they don’t even list “debuggee” yet.

--

--

David W. Gray

I am a software engineer, mentor, and dancer. I'm also passionate about speculative fiction, music, climate justice, and disability rights.