# loov/lensm: reading Go assembly next to the source that produced it

> lensm is a desktop viewer that pairs a binary's disassembly with the source lines behind it, and it also runs as an MCP server. It is a debugging instrument, not a build tool, and it only works on binaries you built yourself.

**loov/lensm** — Go assembly and source viewer

- Repository: https://github.com/loov/lensm
- Stars: 3,689 · Forks: 130
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/loov-lensm

## What lensm shows that a plain objdump does not

Disassembling a Go binary is easy. Connecting an instruction back to the line that produced it, and then jumping to the caller that produced that line, is the tedious part. lensm is built around that connection. It reads functions from the symbol table and source from DWARF, then displays the two side by side, with call targets that can be followed and a filter for narrowing the function list.

The audience is narrow but real: people who look at generated code on purpose. Compiler and runtime contributors, performance engineers chasing an inlining decision, anyone who wants to know why a function did not get vectorised. It is a desktop GUI, so it assumes you are sitting at the machine where the binary was built.

One constraint shapes everything else. The README states that the program requires a binary built on your computer, otherwise source code for the functions cannot be loaded. You cannot ship a stripped production binary to a colleague and have them read its source in lensm.

## Symbol table, DWARF and the sidecar comment file

The data flow is straightforward. An executable path goes in. Functions come out of the symbol table; source lines come out of DWARF. The repository layout reflects this split: loader.go handles reading the binary, navigation.go handles movement between functions, and fileui.go plus its sibling files (fileui_comments.go, fileui_navigation_test.go, fileui_settings.go, fileui_tabs.go) hold the interface.

Comments are the one piece of state lensm writes. By default they live in a sidecar named `<executable>.lensm-comments.json`. When the GUI and a separate `lensm mcp` process write the same sidecar, saves merge per comment: additions, edits and deletions from each process survive, and only conflicting edits to the same comment resolve to the last writer. The README is explicit that the merge is read-then-write without a file lock, so two saves landing in the same instant can still lose one side's changes.

That is a design trade-off worth naming. Per-comment merging is more forgiving than whole-file overwrite, but it is not a database. Comments are also keyed by function name, not by binary, so pointing `-comments` at one shared file is only safe for builds of the same program.

Non-Go binaries are supported through the same mechanism. A C or C++ program built with `-g` shows source alongside assembly, and on macOS the debug info is read automatically from the `.dSYM` bundle next to the binary. C++ and Rust symbols are demangled, and for C++ the signature is kept so overloads stay apart.

## Installing lensm and loading a first binary

The README gives the standard Go install path. This fetches the module and puts a `lensm` binary in your Go bin directory.

## Skipping a windowing backend on Linux

On Linux the README points at additional Gio dependencies and offers two build tags for skipping a windowing backend. Use one or the other if your system lacks Wayland or X11 headers.

## Running lensm against a binary, and in watch mode

Once installed, run it against an executable. Starting it with no arguments opens the same window, and a binary can be loaded from the top bar instead. On macOS, Choose... opens the native Finder dialog. The README's own example runs lensm on itself, and the `-watch` flag reloads the executable and its information when the file changes:

```bash
lensm -watch lensm
```

Inside the code view, follow call targets with `Alt+Left/Right` or `Cmd/Ctrl+[` and `Cmd/Ctrl+]`. Hovering an assembly instruction shows its reference and a simplified explanation when a matching rule exists. Dragging across Go assembly, native assembly or source lines selects a block for `Cmd/Ctrl+C`, `Shift` extends the selection and `Escape` clears it.

There is also a non-GUI mode. The README gives this invocation for running lensm as an MCP server over stdio:

```bash
lensm mcp [-comments ./lensm.lensm-comments.json] ./lensm
```

The server exposes tools for listing functions, reading a function's Go source, Go assembly and native assembly, and reading or writing comments.

## WebAssembly and microcontroller targets, and where line mapping gets loose

lensm recognises `.wasm` by magic bytes and shows WAT, one instruction per line, with calls resolved to function names. Source comes from whichever line table the module carries, and the README draws a distinction that matters for reading the output correctly. Go embeds a pclntab in its data segments, which records a position per resume point rather than per instruction, so a run of instructions between two resume points shares one line. TinyGo and clang emit DWARF instead, which is per statement. If you are used to Go binaries, a TinyGo wasm build will look more precise than you expect, and a Go wasm build less precise.

A `wasip2` build is a component rather than a plain module, so its functions come from the core modules nested inside it and are listed under the module they belong to, as in `main/main.sumInts`.

Microcontroller builds are decoded too. TinyGo's Cortex-M targets such as `pico`, `pico2`, `feather-m4` and `teensy40` produce Thumb code, decoded by a disassembler generated from ARM's architecture reference XML, covering Thumb-1, Thumb-2 and the floating-point extension, IT blocks included, with branch targets resolved to function names and literal pools shown as `.word` data. 32-bit RISC-V targets such as the ESP32-C3 use the RISC-V decoder. AVR targets (`arduino`, `attiny`) and Xtensa targets (`esp32-*`, `nodemcu`, `d1mini`) have decoders of their own, in the syntax of the GNU objdump for each.

The honest limitation is stated in the README: a function is listed under the file it was written in, which for heavily inlined code is not the file its first instruction belongs to. Inlining is exactly the case where you reach for a tool like this, so expect to reconcile that yourself.

## How lensm differs from Delve and from compiler explorer sites

Delve is the obvious comparison. It is a debugger: you set breakpoints, step, inspect variables, and read disassembly at a stop point. lensm is not a debugger. There is no process to attach to, no breakpoints, no runtime state. It is a static viewer over a binary plus its debug info, which is why it can show a whole function's assembly and source at once and let you jump between call targets without running anything.

Compiler Explorer takes the other axis. It compiles a snippet you paste and shows the assembly, which is ideal for small isolated questions and useless for a real binary with its actual inlining and its actual layout. lensm reads a binary you already have. If your question is about a twenty-line function in isolation, a web compiler is faster. If your question is why this specific build made this specific choice, you need the binary.

The MCP server is the third mode, and it is the least conventional. Instead of a window, an agent gets tools to list functions and read Go source, Go assembly and native assembly, with comments in a JSON sidecar. Whether that is useful depends entirely on whether you already drive an agent that can call those tools.

## Maintenance, licence and what upgrading actually costs

The repository is not archived, and the last push was on 2026-08-25, so the code is being touched. Releases are a different story: the most recent listed release is v0.0.3 from 2023-02-15. The README's install instructions point at `@main` rather than a tagged version, which tells you where the project expects users to be. Installing from `main` means your build moves whenever the branch does.

The dependency list in go.mod is where the practical upgrade cost sits. lensm pins `gioui.org v0.10.2` and `gioui.org/x v0.10.2` for the UI, `github.com/loov/disasm v0.1.2` for disassembly, and `golang.org/x/arch v0.30.0`. It also requires Go 1.27. Gio is the piece most likely to cause friction, because it is what pulls in the platform windowing dependencies the README warns Linux users about, and the `nowayland` and `nox11` tags exist precisely because those backends are not always available. A Gio major bump is a rebuild-and-check exercise, not a drop-in.

Licensing is MIT, which is permissive and imposes few conditions beyond keeping the notice. Note that the disassembly and architecture decoding come from separate modules with their own licences, and the README mentions a disassembler generated from ARM's architecture reference XML, which is a different kind of artefact from hand-written code. If you redistribute a built lensm, check the terms of the dependencies rather than assuming the MIT header on this repository covers everything in the binary.

## Conclusion

Adopt lensm if you debug Go performance or code generation on your own machine and want assembly and source in one window, and if the MCP mode fits an agent workflow you already have. Do not adopt it if you need to inspect binaries you did not build locally, since the README states source cannot be loaded for those, or if you need locked, conflict-free comment storage. Before relying on it, verify that a function you know well appears under the file you expect, and check whether your target architecture is one of the decoded ones.

## FAQ

### Is loov/lensm a real project?

Yes. It is a Go module at loov.dev/lensm, licensed MIT, with source files such as loader.go, navigation.go and fileui.go in the repository root. The README links to a blog post at storj.io explaining why and how the core functionality works.

### Does loov/lensm work on binaries I did not build on my own computer?

No. The README states that the program requires a binary built on your computer, otherwise the source code for the functions cannot be loaded. Source comes from DWARF, so a binary without usable debug info will not show source lines.

### Can loov/lensm show C, C++ or Rust binaries?

Yes. Functions come from the symbol table and source from DWARF, so a C or C++ program built with -g shows its source alongside the assembly, and on macOS the debug info is read automatically from the .dSYM bundle next to the binary. C++ and Rust symbols are demangled.

### What happens if two loov/lensm processes write the same comments file?

Saves merge per comment, so each process's additions, edits and deletions survive, and only conflicting edits to the same comment resolve to the last writer. The merge is read-then-write without a file lock, so two saves landing in the same instant can still lose one side's changes.

## Sources

- [Issues](https://github.com/loov/lensm/issues)
- [License: MIT](https://github.com/loov/lensm/blob/main/LICENSE)
- [loov/lensm on GitHub](https://github.com/loov/lensm)
- [README](https://github.com/loov/lensm/blob/main/README.md)
- [Releases](https://github.com/loov/lensm/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/loov-lensm
