Shotizam blames every byte of a Go binary on something you can name
Shotizam analyzes the size of Go binaries
At a glance
- What is it?
- Shotizam is a Go tool from bradfitz that breaks an ELF, Mach-O or PE binary down to the byte and writes the result into SQLite for querying. Its selling point is total accounting: unexplained bytes are labelled unknown and the rows still sum to the file size.
- Who is it for?
- Shotizam suits Go engineers who need to explain why a binary grew, especially where DWARF data or pclntab metadata dominates the file, and it suits anyone willing to query SQLite rather than read a text report. It does not suit teams whose build already enforces a size budget, and it does not give you a build flag to fix what it finds, since it reports and does not trim.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The output is a table of byte ranges, not a summary report
Shotizam analyzes the size of Go binaries and outputs SQL with size information for analysis in SQLite3. The schema is the whole design. Two tables are created, and every byte of the file appears in exactly one row of the first one.
CREATE TABLE Bin (Off int64, Size int64, Section varchar, What varchar, Name varchar, Pkg varchar);
CREATE TABLE Fixup (Addr int64, Section varchar, What varchar, Name varchar, Pkg varchar);Each Bin row is a contiguous range of the file identified by Off and Size, with the Section it belongs to, a What value describing the kind of bytes, a Name for the function, symbol, type, file or DWARF entry the bytes belong to, and a Pkg when the Go package is known. The section name is the real one, like .text or __TEXT,__gopclntab, or a pseudo-section such as (elf-header) or __LINKEDIT,rebase. The ambition is stated plainly in the project description: every byte gets blamed on something, be it a function's code, its pclntab metadata including name, line tables, stack maps and inline trees, a type descriptor or its names, an itab, a string literal, a symbol table entry, a DWARF entry, a relocation, or alignment padding. Bytes that cannot be explained are reported with What = 'unknown', and the rows always sum to the file size.
Fixup rows track the pointers the loader rewrites
The second table is where the project goes beyond static file layout. A Fixup row is a pointer that the dynamic loader adjusts at startup, covering ELF R_*_RELATIVE relocations, Mach-O rebases and PE base relocations, and each one is blamed on whatever owns the pointer. The motivation given is concrete rather than academic. On iOS, where the memory limit for network extensions was once 15 MB, these pages become dirty and count against you, and the project links to a writeup on the Go linker on the tailscale site. So the Fixup table is the part of the tool that answers a runtime question from static data: how much of your binary turns into dirty pages when the loader starts. Fixup rows are counted separately and carry their own What values, with type, rodata and data appearing in the documented sample. The obvious workflow is to count them and multiply by the pointer width, which gives an estimate of that dirty page cost for the platform you ship to.
What values separate machine code from metadata
The What vocabulary is where the analysis lives, and it is worth learning before you write a query. text is the machine code of the named func or symbol. padding is alignment padding, meaning runs of 0x00 or 0xCC not covered by anything else. The pclntab family covers pcheader, funcname, cutab, filename, functab, the _func structure and its table offsets, the pcvalue tables such as pcsp, pcfile and pcln, the funcdata covering stack maps and inline trees, and findfunctab. Type bytes are split into type for descriptors including method, field and parameter tables, type-name for the names themselves, type-pkgpath, gcbits and itab. Data has its own set: string for literal data blamed on the first function that references it, rodata-ref, data-ref and noptrdata-ref for data without a symbol of its own such as compiler generated statics and //go:embed data, funcdesc for top-level func values, and symtab, strtab, dynsym, dynstr, reloc, rebase, bind, code_signature and pdata for linker and loader metadata. DWARF is broken down per top-level DIE, per compile unit or per function across dwarf-info, dwarf-line, dwarf-loclists, dwarf-rnglists, dwarf-loc, dwarf-ranges, dwarf-frame and dwarf-addr.
Running it means writing SQL against your own binary
The tool writes SQL and the analysis happens in SQLite. The documented invocation points at a binary and opens a database, and the sample output shows how a session with sqlite3 looks once the data is loaded.
$ shotizam --sqlite ./tailscaledThe example then turns on column mode and headers and runs a grouped query, and the schema makes that query natural. Grouping by Pkg and summing Size answers which package is responsible for the bulk of the file. Grouping by What and summing Size answers what kind of data it is, which in the documented run of a tailscale binary is a mixture of text, dwarf-info, type, dwarf-line, dwarf-loclists and funcname. Filtering on Section, What and Name with a range condition on Off and Size lets you find what straddles a particular offset. The pattern is worth internalising: Shotizam produces a relational view of a binary, and it does not try to be a report. For a quick look without the database there is a summary mode, and DWARF can be skipped with -nodwarf when you only care about code and metadata.
JSON mode turns the tool into a size regression check
There is a second mode aimed at a different job. The -mode=json flag writes sizes aggregated by Section, What, Name and Pkg, and a -base option compares a run against a previous one, writing the difference instead. The documented sequence captures one binary, then measures a new one against it.
$ shotizam -mode=json old-binary > old.json
$ shotizam -mode=json -base=old.json new-binaryThat turns a size analyser into a bisect tool. Instead of asking why the binary is large, you ask what changed between two builds, and the answer arrives as a diff of aggregated sizes attributable to a section, a kind of byte, a symbol or a package. A CI job that keeps the previous build artefact and diffs against it can tell you whether a dependency bump added 40 KB of DWARF or whether a new dependency pulled in a package you did not expect. The aggregation is by four keys, so the diff is coarse by design: it names a symbol or a package rather than a line of code. For finding a regression that a text diff of the source cannot explain, that granularity is exactly what you want.
The attribution is a heuristic in three specific places
The caveats section is short and each caveat matters. First, the linker deduplicates identical pcvalue tables, funcdata and names, and shared data is blamed on the first function or type that uses it, so a large shared table appears once and only its first user carries the blame. Second, DWARF sections are usually compressed, and Shotizam maps compressed bytes back to DWARF entries by tracking how much input the decompressor consumes, so per-entry sizes are approximate even though the total always sums correctly. Third, string and *-ref attribution comes from scanning machine code on amd64 and arm64 for references to data, and scanning data for pointers, and that is described as a heuristic. So the rows that attribute bytes to the first referencing function are inference, not proof. A fourth limitation covers relocatable objects: for the go.o inside a c-archive, only the file structure, sections and symbols are broken down, because the Go runtime structures' pointers are not relocated yet. Support for ELF, Mach-O and PE covers binaries built by Go 1.18 and newer, with the most detail for recent versions.
A tool with no releases and a single author
Shotizam is a personal project from bradfitz, with the repository, the license file and the source at the top level, plus per-format files for ELF, Mach-O and PE, a pclntab parser, a DWARF parser, a types parser and a blame module. The go.mod declares module github.com/bradfitz/shotizam with Go 1.21. There are no GitHub releases, so installation is a matter of building from source with go build or go install against that module path, and there is no versioned artefact to pin. The last push to the master branch was on 2026-09-30 and the repository is not archived, so the code is being worked on. The licence is BSD-3-Clause, which permits reuse and redistribution with the notice retained and no warranty. Two practical consequences of a single-author tool with no releases: the parser for each binary format has to track Go linker changes made by someone else, and if you vendor it you own that maintenance. The stated support window, Go 1.18 and newer, is also the boundary of what the parsers have been written against.
Editorial conclusion
Shotizam suits Go engineers who need to explain why a binary grew, especially where DWARF data or pclntab metadata dominates the file, and it suits anyone willing to query SQLite rather than read a text report. It does not suit teams whose build already enforces a size budget, and it does not give you a build flag to fix what it finds, since it reports and does not trim. Before adopting it, run shotizam --sqlite on a binary you already understand and check that the unknown row is small, because attribution for string literals and embedded data is a heuristic that works on amd64 and arm64, and check the totals against the file size before trusting a per-package breakdown.
Frequently asked questions
What does Shotizam analyze?
Shotizam analyzes the size of Go binaries and outputs SQL with size information for analysis in SQLite3, supporting ELF, Mach-O and PE binaries built by Go 1.18 and newer.
How do I run Shotizam on a binary?
Run shotizam --sqlite with the path to the binary, then query the Bin and Fixup tables in SQLite. For a quick view without a database, use -mode=summary, and use -nodwarf to skip breaking down DWARF entries.
Can Shotizam compare two builds?
Yes. -mode=json writes sizes aggregated by Section, What, Name and Pkg, and adding -base=old.json writes the difference from a previous run instead of the absolute sizes.
What are the Fixup rows in Shotizam for?
A Fixup row is a pointer the dynamic loader adjusts at startup, covering ELF relative relocations, Mach-O rebases and PE base relocations, blamed on whatever owns the pointer. These pages become dirty on iOS and count against the memory limit for network extensions.
How accurate is Shotizam's attribution of string literals and embedded data?
Attribution for string literals and for data without a symbol of its own comes from scanning machine code on amd64 and arm64 for references, and the project describes it as a heuristic. DWARF per-entry sizes are approximate because the sections are usually compressed.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bradfitz-shotizam)Was this useful?