Reading an obfuscated crash: decoding a stack trace

An obfuscated assembly produces crash reports full of renamed symbols. The rename map turns them back into your original names — here is the workflow.

Obfuscation works both ways. Once your members are renamed, a crash report from a user comes back full of names like a.b(c d) instead of OrderService.Submit(Order order). The rename map is how you read those reports again — without shipping the readable names to anyone.

Why the trace is unreadable

A .NET stack trace is built from the method names in the assembly's metadata. After obfuscation those names are the renamed ones, so the exception that reaches your logs, your error reporter or a user's screenshot is written in the obfuscated alphabet:

System.NullReferenceException: Object reference not set to an instance of an object.
   at d.a(e f)
   at d.b()
   at m.n(String s)

That is working as intended — a reader of your shipped binary gets the same uninformative trace. The problem is that so do you. Without a way back, every production crash is a guessing game.

The rename map

When you obfuscate on Pro you can ask for a rename map — a .map.txt file that records, for that specific build, which original name became which obfuscated one. It is the private key to your own crash reports:

OrderService.Submit(Order) -> d.a(e)
OrderService.Validate()    -> d.b()
PaymentGateway.Charge(String) -> m.n(String)

Keep it somewhere safe alongside your build artifacts — the same place you would keep the PDBs for a release. One map belongs to exactly one build, so tag it with the version.

We never store it. The map is generated for your download and is not retained on our servers. That means there is nothing on our side to leak — but it also means that if you lose the map for a build, that build's traces can no longer be decoded. Treat it as a release artifact you are responsible for keeping.

Decoding a trace

We ran this on a small console app, not an illustration. The app crashes with a null reference inside a three-call chain. Obfuscated with renaming, the protected build crashes the same way, and the trace reads like this:

System.NullReferenceException: Object reference not set to an instance of an object.
   at @e.@b(String @b)
   at @d.@c(@c @b)
   at @d.@b(@c @b)
   at @f.Main()

Paste that trace and the matching map into the stack-trace decoder. The decoded output:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Shop.PaymentGateway.Charge(String @b)
   at Shop.OrderService.Validate(@c @b)
   at Shop.OrderService.Submit(@c @b)
   at Shop.Program.Main()

Be clear about what comes back. The decoder restores each method's name and its owning type, so the call chain is readable. It does not restore parameter names or parameter types (@b and @c stay), and it does not add line numbers. Line numbers come from the PDB of the unobfuscated build, which is why you keep that build archived (see below).

The decoder runs on our server: you post the map and the trace over HTTPS, the decode is computed, and nothing is stored. If you do not want a map to leave your machine, do not use the web decoder for that map.

Fitting it into a real support workflow

  • Version every build. Put the version in the assembly and in the map filename, so an incoming crash from 2.4.1 is decoded with the 2.4.1 map and not a neighbour.
  • Store maps with releases. Your CI artifact store or release bucket is the natural home. If you obfuscate from a pipeline, capture the map as a build output in the same step — see obfuscating in CI/CD.
  • Do not ship the map. It never goes near the client. Shipping it would hand a reader the exact thing obfuscation removed.
  • Keep a readable build for yourself. The map covers names; for line numbers you still want the unobfuscated build and its PDBs archived.

How this compares

Commercial suites such as SmartAssembly bundle this into automated error reporting — incoming crashes are de-obfuscated for you against a stored mapping, which is genuinely convenient and priced accordingly. Our approach is deliberately smaller and more private: you hold the map, nothing is stored, and you decode on demand. If you want the full managed error-reporting pipeline, that comparison lays out the trade-off honestly. If you want to read your own crashes without a mapping sitting on someone else's server, this is the lighter way to do it.

Try it on your own assembly

Upload a DLL or EXE and download the obfuscated result. Free, no signup, nothing stored.