Extreme Debugging
squanderingti.me
squanderingti.me
EndNote, on her machine, was just absurdly slow. Operations like editing a single reference would beachball for 30-60 seconds. She has a moderately-sized library - about 1000 references - but nothing that should be causing a modern MacBook to slow down that much. So I took a look. Happily, on a Mac, you can use spindump to automatically take a sample-based profile of any process, and you can even do it from Activity Monitor. Endnote had their symbols partially stripped - but the Objective-C functions weren't stripped, so I got to see that somewhere in the call chain was a "recalculateColumnWidths" function. That was apparently getting called on practically every operation with the library.
I got out Ghidra, traced the caller of that function, and `nop`ped the call site out. Problem fixed - no more random slowdowns, no more beachballs. Resizing the columns manually worked too - I never really saw any cases where the columns seemed out of whack.
If I had to guess, she probably had some reference with some unusual characters in it that was causing Endnote to think it needed to resize all the columns. I never quite figured that part out - but that would have been another way to solve the problem (e.g. by bisecting her library to find the offender(s)).
The obvious disclaimer is this can make you slow to get things done sometimes–but you'll be lightning fast the next time it comes around ;)
I remember experiencing this as an undergraduate CS student with a bioinformatics research position. At the time I felt like I was too stupid to understand the code.
After I struggled with that, the professor reassigned me to making their software available for web use and wanted me to take over some PHP shell_exec rollercoaster for handling job submissions.
Your NSF funds hard at work.
It's as if we sent a bunch of theoretical physics PhD's out to Geneva, gave them garden trowels, and let them break ground on a particle accelerator themselves.
Reality is that academics are bad at programming because academia is bad a teaching it and doesn't prioritise it.
However, a lot of this is using tools that I'd expect everyone (even browser JS programmers, Java programmers, folks that write code in higher level languages) to have at least a passing familiarity with: strace/ltrace, nm/ldd, gdb...no matter who you are or what you do, there's immense benefit in trying to debug your programs like they're closed-source[1].
I'm perennially disappointed to find that extremely skilled and intelligent colleagues (far better at writing and designing code than me) are routinely stymied when asked to figure out why their code is acting a certain way when it runs--why a behavior of their code is occurring in an environment where their typical tools (language-specific debuggers, IDEs, etc.) aren't available.
I think this is a skill-gap that our iteration-focused, "just try shit until it works" industry has instilled in a generation of programmers to their detriment.
This is super important stuff--not just for debugging ability, but because understanding at least the "shallow end" of runtime-analysis tools (and with eBPF and core analysis tools, to say nothing of actual embedded/kernel programming, the pool gets a lot deeper, if you want) makes you become a way better programmer!
This isn't only for folks who write code in runtime-less languages (and don't come at me about crt0); even people who write Python for a living (like me!), get bloody superpowers from understanding things like kernel memory management, syscall semantics below the standard library, and garbage collection behavior. The folks I find that don't have those skills aren't stupid or talentless, but rather victims of a series of roles and managers that have instilled a kind of Stockholm Syndrome of "when these things fail, there's nothing you can do about it, just consider it spooky and move on with your life", when getting at what's really going on is not just easy to do, but super beneficial for your career development!
Put another way, we're all systems programmers in our day jobs--some of us are just in denial.
/rant
1. https://jvns.ca/blog/2014/04/20/debug-your-programs-like-the...
IMO not enough people download and build the source. It's primitive, but adding logging or printfs to existing source, combined with strace if necessary, can be effective in a brutal way, where gdb - or any debugger - wouldn't get you far.
The other day I did exactly that with JVM to discover why our JavaFX app crashes to desktop on customer's machine. Going through the crash dump, I immediately spotted DirectX calls being prominently displayed (IIRC top of one of the stack frames), and upon investigation, it turned out that the computer haven't had its graphics drivers updated in a long, long time.
Stepping is an enormous waste of time with a debugger.
Breakpoints are where it's at. Level up your breakpoints: logging breakpoints, conditional breakpoints, memory breakpoints (super handy when combined with deterministic memory allocation), syscall / library breakpoints - and soon you're going somewhere useful.
Debugging is about formulating theories and proving them wrong, or right. Stepping doesn't scale; breakpoints can scale, if you know where to put them.
I'm not sure that the gap between a debugger with breakpoints, and printfs in source, is that large, though. It's easier to write complex conditionals or log the state of a complex predicate in the source language than in whatever the debugger gives you - even if it gives you the native language, you typically have to write it all on a single line. And you need to dive deep into the source to understand where to put those breakpoints - or printfs.
An underused debugging tool is an instrumenting profiler. The captured call graph is a useful clue for dynamic & indirect control flow in particular, without needing to decipher it from stepping or infer it from chasing source references and indirections. Things like generic message dispatchers and event callbacks are horrible to try and step through and aren't much better when eyeballing the source.
IME I find it easier to deal with modifications to code than configuration in IDEs, precisely because I can identify diffs and if desired commit the modifications to source control on my own branch. I frequently debug things like third-party libraries (e.g. Ruby gems by adding my rvm gemset) to a git repo to track my modifications.
It's generally harder to export and import breakpoints from an IDE. They are usually driven by line numbers, which aren't particularly stable over code changes.
https://dev.to/captainsafia/say-this-five-times-fast-strace-...
where I stumbled upon and read the ezine:
If that means rejecting the PR "stop using globals in $language because that triggers a 'slab allocation' (whatever that is) which stackoverflow says is bad", and instead insisting to your boss that you spend an extra week reading kernel source/stdlib source instead of moving on from the original 2-hour-scoped bug, so be it. The outcome is far more beneficial (for you and for the product/company) in the long term.
strace and gdb are your friends in catching evasive bugs. Personally I would go exactly same route, though I won't modify the binary but coined an environment for it to work instead.
Two main tools to look into are xperf, the cmdline tool for configuring and running trace sessions, and Windows Performance Analyzer, a GUI app for exploring recorded traces (additionally, there's Windows Performance Recorder, which seems to be an official GUI for xperf, but I haven't used it myself yet).
This is essentially a superpower that's even less known than strace is by Linux devs. I've only learned the basics quite recently, and already used it to debug two hairy issues in few hours, that have already taken a man-month worth of time and would likely take another one. Being able to explore the recordings of the process tree evolution, see all file IO operations, their times and targets and relation to everything else happening in the system, all in both visual and tabular form - it gives you a perspective you otherwise can't get.
(An unfortunate side effect is that the tough bugs, like multiprocess race conditions or silent crashes, all seem to be directed my way nowadays.)
Unfortunately, xperf and WPA don't come pre-installed in most Windows distributions; the fastest way to get them is through UIforETW[0] - Google's UI for xperf that can automatically locate and install the correct version of Windows Performance Toolkit[1].
Bruce Dawson (of Google Chrome fame) wrote a lot about advanced usage of this tooling, see: https://randomascii.wordpress.com/2015/09/24/etw-central/.
--
[0] - https://github.com/google/UIforETW/releases - fetch the newest .zip package with binaries.
[1] - Alternative approach, if you need xperf on a computer with no Internet access (happened to me the other day with a customer's VM), you need to download the Windows SDK ISO (e.g. https://developer.microsoft.com/en-us/windows/downloads/wind...), mount it, and grab the Windows Performance Toolkit installer from the mounted image.
The trick is to never reveal your superpower!
The article was a delight to read, it's definitely an area where I am lacking and it seems fairly difficult to pick up this skill through a curriculum.
to capture syscall, there are procmon/regmon/sysmon from sysinternals, that are also light years ahead of anything available on linux and come with an amazing UI.
for ldd I am not sure what it would be for, windows is not quite like linux when it comes to dynamic libraries. there is depends.exe from visual studio and there was a nice import analyzer tool I forgot the name.
Mostly on grounds that if one binary had that problem, maybe another one would, and I would really rather not go through the same process twice ;)
Trade-offs all the way down, as usual, and "neat hack" was meant positively, mine's just a different neat hack with different advantages and failure modes :D
In my experience, debugging threaded stuff is the worst. Remote-machine threaded debugging (like you see with driver and embedded stuff) makes one wake up screaming for years.
In the eighties and nineties, there was this famous low-level Mac debugger, called “The Debugger V2 & MacNosy.”
If anyone remembers that, I may have just reawakened trauma. Sorry.
The running joke was that the only people that knew how to use that system were the ones personally trained by its author. I never really got the hang of it, myself. I tended to use MacsBug a lot.
My favorite debugging tools were something called “In-Circuit Emulators” (ICEs). These were big-ass machines that had a PGA that you would plonk into the processor slot, and they would emulate the processor. You really could dig out those nasties with an ICE.
Processors are way too hairy for that, nowadays.
The pleasure of a successful two week debugging journey, the pain of discovering it was a off-by-one written by oneself (when one was not in a fit state to have been at the keyboard!)
I think my most inventive embedded CPU debugging was instrumenting the custom RTOS to output the task number onto data pins. Then capturing that data on a logic analyser, printing the capture from the logic analyser to the parallel port of a PC, then post processing the data so that I could profile the CPU usage per task. I guess my electronic engineer background caused the logic analyser to be my hammer...
https://github.com/eteran/edb-debugger
You will like it.
Try Ghidra, too.
While I love reading about how someone can patch a binary to work around this, to me it's more interesting to think about the systemic failures. The problem ultimately is that one system is sending another a response that it can't handle, but the original Unix API has made this impossible to handle. In fact in both this article and my talk, receiving a 64-bit inode causes the segfault, but neither program was asking the filesystem for an inode in the first place. (Ultimately the fix in TFA was to ignore the result of the stat call.)
It's amazing that software works at all when you're dealing with latent 25 year old "bugs" that are really just badly designed APIs and poorly implemented specs. And the organisational failures that force us to maintain compatibility with ancient binaries without access to the source. The author here did a great job working resolving something that a lot of folk would have just walked away from.
[0] https://www.usenix.org/conference/lisa13/drifting-fragility
I feel like there is so much more to gdb than what I've yet had a need to use.
> No, it doesn't. A static binary has no need to resolve any symbol dynamically, and therefore the dynamic loader is not invoked to resolve the usual library function symbols.
I had an embedded system with a CPU that had no instruction cache. It was crashing, and I had the crash address, but I couldn't figure out how it got there. So I attached a logic analyser to the memory bus, triggered it on the crash address (or maybe on some symptom of the crash; it's been a while). I had the trigger point be at the end of the saved trace. I then could see what the address bus had been up to (that is, what instructions were fetched) for some time before the crash. That got me enough information to find and fix the problem.
You couldn't do that today, because everything is too small to put a chip clip on, and the processors cache instructions, so the program counter is not directly visible on the address bus.
PS: ignore the little link icons at the end of each paragraph... they don’t work! I love reading about a successful bug hunting expedition.