<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joey Plunkett</title>
    <description>The latest articles on DEV Community by Joey Plunkett (@joeyp).</description>
    <link>https://dev.to/joeyp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023101%2F0ca6be9b-537b-4090-bcf2-bf3dae7be9cc.png</url>
      <title>DEV Community: Joey Plunkett</title>
      <link>https://dev.to/joeyp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joeyp"/>
    <language>en</language>
    <item>
      <title>Why Your Symbols Don't Match Your Binaries</title>
      <dc:creator>Joey Plunkett</dc:creator>
      <pubDate>Wed, 15 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/joeyp/why-your-symbols-dont-match-your-binaries-2mm6</link>
      <guid>https://dev.to/joeyp/why-your-symbols-dont-match-your-binaries-2mm6</guid>
      <description>&lt;p&gt;You uploaded the symbols. They're right there. And your crash report still comes back as a wall of hex addresses with no function names, or your debugger greys out the breakpoint and tells you symbols aren't loaded for a PDB that is very obviously sitting in the folder you're pointing at.&lt;/p&gt;

&lt;p&gt;This is the single most maddening failure in native debugging, because everything looks correct. The file exists. The name matches. And it still doesn't work.&lt;/p&gt;

&lt;p&gt;Here's what's actually going on, and why "the file is right there" was never the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching is by identity, not by name
&lt;/h2&gt;

&lt;p&gt;When your compiler builds a binary, it stamps a unique identifier into it and writes the same identifier into the symbol file it generates. On Windows that's a &lt;strong&gt;GUID plus an Age&lt;/strong&gt; (a signature and a revision counter). On macOS and iOS it's a &lt;strong&gt;UUID&lt;/strong&gt; in the dSYM. The debugger doesn't look for a file called &lt;code&gt;MyApp.pdb&lt;/code&gt;. It looks for the symbol file whose identifier matches the one baked into that exact binary.&lt;/p&gt;

&lt;p&gt;If the identifiers don't match, the symbol file is treated as &lt;em&gt;missing&lt;/em&gt;, not as &lt;em&gt;close enough&lt;/em&gt;. That's deliberate. Symbols that almost match are worse than no symbols, because they'd map addresses to the wrong functions and send you chasing a bug in code that never ran. So the tooling refuses.&lt;/p&gt;

&lt;p&gt;This is the whole thing. A symbol file isn't a description of your source code. It's a fingerprint of one specific compilation. Compile again and you get a different fingerprint, even if you changed nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The causes, in order of how often they get people
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You rebuilt.&lt;/strong&gt; The most common one by far, and the most infuriating because your source didn't change. Hit rebuild and the compiler generates a fresh binary with a fresh Age, and the PDB you carefully saved from the previous build is now, formally, a different fingerprint. It's the &lt;em&gt;same code&lt;/em&gt;, and it's &lt;em&gt;not the same build&lt;/em&gt;. The debugger doesn't care about your feelings on this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug and Release got crossed.&lt;/strong&gt; Your toolbar says Debug, the module actually loaded came from a Release output path, and the symbols you're pointing at belong to the other one. Extremely common, weirdly hard to see, and the reason the Modules window exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your release build never generated symbols in the first place.&lt;/strong&gt; A release configuration set to strip debug info produces a binary with nothing to match against. There's no mismatch here, there's an absence. Check that your release config still emits PDBs. They don't ship to users, they go to your crash reporter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PDB was stripped.&lt;/strong&gt; This one is genuinely sneaky. A PDB built with &lt;code&gt;/PDBSTRIPPED&lt;/code&gt; loads &lt;em&gt;successfully&lt;/em&gt; and contains no source mapping at all. The tooling reports symbols as loaded, you still get nothing useful, and you go hunting for a completely different bug. Symbols present, symbols useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something modified the binary after it was built.&lt;/strong&gt; This one catches game and desktop teams constantly. Code signing changes the executable's timestamp after the compiler is done with it. Anti-cheat and copy-protection wrappers rewrite the binary too. Either way, the thing your users are running is no longer byte-identical to the thing your symbols were generated against. The fix is to keep the &lt;em&gt;signed&lt;/em&gt; executable, the one you actually shipped, not the pre-signing build output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something copied the binary somewhere else.&lt;/strong&gt; Build systems, installers, and runtimes that shuffle binaries into temp directories can quietly separate a binary from its symbols, or leave a stale copy of one next to a fresh copy of the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're chasing symbols that don't exist.&lt;/strong&gt; Third-party libraries often don't publish matching PDBs at all. Microsoft's public symbol server covers system and framework DLLs, not somebody's proprietary DLL. Sometimes the fix is "there is no fix, this frame stays opaque."&lt;/p&gt;

&lt;p&gt;There is a specific stage of symbol debugging where you find yourself staring at a PDB file, in the correct folder, with the correct name, and quietly asking it what more it wants from you. It wants a different Age. It will not tell you this.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually check, instead of guessing
&lt;/h2&gt;

&lt;p&gt;The good news about identity-based matching is that it's checkable. You don't have to theorize about whether your symbols match. Read the identifier out of both sides and compare.&lt;/p&gt;

&lt;p&gt;Platform tooling handles this. On Windows, &lt;code&gt;dumpbin&lt;/code&gt; and &lt;code&gt;symchk&lt;/code&gt; will report the GUID and Age. On macOS, &lt;code&gt;dwarfdump&lt;/code&gt; gives you the UUID. For Crashpad symbols, the GUID sits on the first line of each &lt;code&gt;.sym&lt;/code&gt; file. There's also &lt;a href="http://www.debuginfo.com/" rel="noopener noreferrer"&gt;ChkMatch&lt;/a&gt;, an old but excellent tool that tells you directly whether a PDB matches a binary and &lt;em&gt;why&lt;/em&gt; it doesn't when it fails ("Age mismatch" is a phrase you'll come to know).&lt;/p&gt;

&lt;p&gt;I work at &lt;a href="https://www.bugsplat.com/" rel="noopener noreferrer"&gt;BugSplat&lt;/a&gt;, where we deal with this problem at volume, so we ended up open-sourcing a few small tools for reading these identifiers out of symbol files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/BugSplat-Git/pdb-guid" rel="noopener noreferrer"&gt;&lt;code&gt;pdb-guid&lt;/code&gt;&lt;/a&gt; for Windows PDBs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/BugSplat-Git/macho-uuid" rel="noopener noreferrer"&gt;&lt;code&gt;macho-uuid&lt;/code&gt;&lt;/a&gt; for macOS and iOS dSYMs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/BugSplat-Git/elfy" rel="noopener noreferrer"&gt;&lt;code&gt;elfy&lt;/code&gt;&lt;/a&gt; for ELF files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They do one thing, they're MIT licensed, and they'll tell you in about a second whether the symbol file in your hand belongs to the binary you're debugging. Use them, fork them, ignore them, whatever's useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually stings in production
&lt;/h2&gt;

&lt;p&gt;All of the above is annoying on your own machine, where you can just rebuild and try again.&lt;/p&gt;

&lt;p&gt;In production it's a different category of problem, and it's permanent. The crashes coming in from users were produced by a binary you shipped weeks ago. The only symbol file that will ever match it is the one generated at the moment that binary was built. If you didn't keep it, those crashes are unreadable, forever. There is no recovering it later, no rebuilding from the same commit, because a rebuild produces a new fingerprint.&lt;/p&gt;

&lt;p&gt;The tag in git is not enough. The symbols are the artifact.&lt;/p&gt;

&lt;p&gt;This is the whole reason symbol management is a discipline and not a chore. Every build that could reach a user, including QA and beta builds, has to have its symbols captured at build time, or you're accepting in advance that some future crash will be undebuggable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to stop fighting this
&lt;/h2&gt;

&lt;p&gt;The fix is boring, which is how you know it's the right one: take the human out of it.&lt;/p&gt;

&lt;p&gt;Upload or archive symbols automatically, from the build pipeline, on every build. Not as a step someone remembers, as a step that runs. If the symbols leave the build machine at the same moment the binary does, they cannot get out of sync, because there was never a window in which they could.&lt;/p&gt;

&lt;p&gt;Concretely, that means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm your release config emits symbols.&lt;/strong&gt; On Windows, set the debug information format so release builds produce PDBs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture symbols as a build artifact&lt;/strong&gt;, not as something on someone's machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive the post-signing binary too&lt;/strong&gt;, since signing changed its timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the upload a pipeline step&lt;/strong&gt;, so a build physically cannot ship without its symbols going somewhere durable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whether that "somewhere durable" is a symbol server you run, an artifact store, or a crash reporting service doesn't much matter. What matters is that it happens on every build, without a human deciding to do it.&lt;/p&gt;

&lt;p&gt;For what it's worth, at BugSplat we solve step 4 with a CLI called &lt;a href="https://github.com/BugSplat-Git/symbol-upload" rel="noopener noreferrer"&gt;&lt;code&gt;symbol-upload&lt;/code&gt;&lt;/a&gt; that you drop into CI (there's a GitHub Action too). It walks your build output, reads the identifiers, and ships the symbols up so that when a crash lands six months later we can match it back by GUID rather than by filename and hope. But the principle is tool-agnostic, and if you're rolling your own symbol server with &lt;code&gt;symstore&lt;/code&gt;, the same discipline applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Symbols match binaries by a unique identifier baked in at compile time, not by filename. Rebuild and the identifier changes, even if your code didn't. Sign or wrap the binary afterward and it changes too. So a symbol file is a snapshot of one specific build, and if you lose it, the crashes from that build are unreadable permanently.&lt;/p&gt;

&lt;p&gt;When something looks wrong, read the GUID out of both sides and compare instead of guessing. And automate symbol capture from your build pipeline, so the window in which symbols can drift from binaries never opens.&lt;/p&gt;

&lt;p&gt;If you've got a symbol horror story, I'd genuinely like to hear it. I collect them.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>gamedev</category>
      <category>crashreporting</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
