Emit.redefinition has taken ~debug since it was written and was tested
with it; Session.eval never passed it, so every body installed by C-c C-c
lost its debug info in the running process.
Passing it alone would have been half a fix. Build.shared is what forces
-O0, and dev.ml built modules at -O2, so the llvm.dbg.declares would have
been emitted and then deleted by mem2reg: a line table, and no locals.
And a module with DWARF loaded into a host without it lines up against
nothing. So it is one flag — flan dev --debug and flan reload --debug —
and it sets the host build, the module builds and the emitted metadata
together. Off by default: a debug build is an -O0 build, and quietly
making every reloaded body -O0 changes the frame time of the one function
you are iterating on, in the loop whose point is watching that number.
What a dlopen'd module does to a breakpoint, measured against the reload
fixture rather than reasoned about:
- lldb reads the new module's DWARF on the dlopen and says so: "1
location added to breakpoint 3".
- A breakpoint set by NAME gains a second location either way, so
dlopen was never the difficulty. What the line table buys is that it
stops with source instead of disassembly.
- A FILE AND LINE breakpoint on the new body resolves only with it;
without, it sits at locations = 0 (pending) forever.
- A FILE AND LINE breakpoint on the HOST's copy stays pinned at
locations = 1. That is correct, not stale: the old body is still
mapped and every call site that has not gone through its cell again
still reaches it.
- The stack crosses intact — a frame in the reloaded .so and the one
below it in the host each name their own .flan file.
(lldb) frame variable
(long) step = 10
(long) prior = 11
The transcripts are in flan-dape.el, replacing the note that said the
module carries no DWARF yet.
flan-cnr.el's stack pane was refusing for the wrong reason. DWARF was
never its gap; nothing is attached to the stopped program, and a socket
cannot read another process's frames. Reworded to say that.
Source interleaving in the disassembly buffer is unblocked and not done:
objdump -dS interleaves a --debug module's Flan source correctly, so
Dev.asm_of needs the -S and a parse_listing that tolerates source lines.
260 lines
12 KiB
EmacsLisp
260 lines
12 KiB
EmacsLisp
;;; flan-dape.el --- Debug a Flan program with dape and lldb-dap -*- lexical-binding: t; -*-
|
|
|
|
;; The other half of the dev loop. flan-dev.el is about a program that keeps
|
|
;; running while you change it; this is about stopping one and reading it.
|
|
;;
|
|
;; There is no DAP implementation here and there should not be. `flan build
|
|
;; --debug' writes DWARF into the executable, lldb reads it, and `lldb-dap'
|
|
;; speaks DAP on lldb's behalf — so what is left for Emacs is a `dape-configs'
|
|
;; entry that knows how to build a .flan file and where the binary lands.
|
|
;;
|
|
;; It works at all because of the layout. A Flan struct is its C struct, a
|
|
;; slot is an ordinary alloca and there are no tag words or object headers
|
|
;; anywhere (plan.org, Memory), so lldb's own C support prints a Flan value
|
|
;; correctly with nothing taught to it. The compile unit says DW_LANG_C99 for
|
|
;; that reason.
|
|
;;
|
|
;; M-x flan-debug builds the file the current buffer is visiting and stops it
|
|
;; at `main'. Breakpoints are ordinary dape breakpoints in the .flan buffer —
|
|
;; `dape-breakpoint-toggle' on a line — because the DWARF line table names the
|
|
;; .flan file, not the generated .ll. `dape-breakpoint-global' works too, and
|
|
;; is the way to break on a function without hunting for its first line.
|
|
;;
|
|
;; Two things are worth knowing before they surprise you; both have their own
|
|
;; heading at the bottom of this file, written from lldb transcripts rather
|
|
;; than from reasoning about what ought to happen:
|
|
;;
|
|
;; - a --dev build and a --debug build are different builds, and M-x
|
|
;; flan-debug does not attach to the program `flan dev' is running;
|
|
;; - across a redefinition a breakpoint set by NAME gains a second location
|
|
;; and both stay live; one set by FILE AND LINE follows the reload if the
|
|
;; module was built with `flan dev --debug', and does not otherwise.
|
|
|
|
;;; Code:
|
|
|
|
(require 'subr-x)
|
|
|
|
(declare-function dape "dape" (config &optional skip-compile))
|
|
(declare-function dape--config-eval "dape" (key options &optional skip-functions))
|
|
(declare-function dape-breakpoint-toggle "dape" ())
|
|
(defvar dape-configs)
|
|
|
|
(defgroup flan-dape nil
|
|
"Debugging a Flan program under lldb."
|
|
:group 'flan
|
|
:prefix "flan-dape-")
|
|
|
|
(defcustom flan-dape-command "flan"
|
|
"The flan executable used to build a program for debugging.
|
|
Its own option rather than flan-dev.el's `flan-dev-command', because this
|
|
file is meant to load without that one: editing Flan, debugging Flan and
|
|
attaching to a running Flan are three independent things to want."
|
|
:type 'string)
|
|
|
|
(defcustom flan-dape-adapter "lldb-dap"
|
|
"The DAP adapter binary.
|
|
Fedora and Debian ship it as lldb-dap; older LLVM called it lldb-vscode."
|
|
:type 'string)
|
|
|
|
(defcustom flan-dape-stop-at-entry t
|
|
"Whether to stop at the program's entry before running.
|
|
On by default: a debug session that starts by running to completion has
|
|
told you nothing, and the first thing anyone does is set a breakpoint."
|
|
:type 'boolean)
|
|
|
|
(defcustom flan-dape-extra-flags '()
|
|
"Extra flags passed to `flan build' alongside --debug.
|
|
--dev belongs here if you want the cells as well as the line tables; see
|
|
\"Reloading and breakpoints\" at the bottom of flan-dape.el for what that
|
|
does and does not buy."
|
|
:type '(repeat string))
|
|
|
|
(defun flan-dape--source ()
|
|
"The .flan file this session is about.
|
|
The buffer's own file, or the nearest one up from it — so M-x flan-debug
|
|
from a *compilation* buffer or a dired still has an answer."
|
|
(or (and buffer-file-name
|
|
(string-suffix-p ".flan" buffer-file-name)
|
|
buffer-file-name)
|
|
(car (directory-files default-directory t "\\.flan\\'"))
|
|
(user-error "No .flan file here to debug")))
|
|
|
|
(defun flan-dape--binary (source)
|
|
"Where the debug build of SOURCE goes.
|
|
Not beside the source. A debug build is -O0 with DWARF in it and is not
|
|
the artefact anyone means by the program's name, so it must not overwrite
|
|
one made by `flan build'."
|
|
(expand-file-name
|
|
(concat "flan-dbg-" (file-name-base source))
|
|
temporary-file-directory))
|
|
|
|
(defun flan-dape--compile-command (source)
|
|
"The shell command that builds SOURCE for debugging."
|
|
(mapconcat #'shell-quote-argument
|
|
(append (list flan-dape-command "build" source "--debug")
|
|
flan-dape-extra-flags
|
|
(list "-o" (flan-dape--binary source)))
|
|
" "))
|
|
|
|
;; The entry itself. `compile' is dape's own pre-launch hook, so the build
|
|
;; happens through `compile-command' and its errors land in a compilation
|
|
;; buffer that `next-error' walks — which is the whole reason not to shell out
|
|
;; from here. Flan's diagnostics are file:line:col, so they are already in a
|
|
;; shape compilation-mode understands.
|
|
;;
|
|
;; The unquoted forms are evaluated by `dape--config-eval' when a session
|
|
;; starts, in the buffer it started from — which is why `flan-dape--source'
|
|
;; can just read `buffer-file-name'. `dape' itself expects an already
|
|
;; evaluated config, so `flan-debug' below must not hand it the raw entry.
|
|
(defconst flan-dape-config
|
|
'(modes (flan-mode)
|
|
ensure dape-ensure-command
|
|
command-cwd dape-command-cwd
|
|
compile (flan-dape--compile-command (flan-dape--source))
|
|
:type "lldb-dap"
|
|
:request "launch"
|
|
:cwd "."
|
|
:program (flan-dape--binary (flan-dape--source))
|
|
:args []
|
|
:stopOnEntry flan-dape-stop-at-entry)
|
|
"The `dape-configs' entry for a Flan program, without the adapter command.
|
|
Separate from the registration below so a user who wants a variant — a
|
|
different adapter, extra launch arguments — can start from this rather
|
|
than retype it.")
|
|
|
|
;;;###autoload
|
|
(defun flan-dape-register ()
|
|
"Add the `flan' entry to `dape-configs'.
|
|
Idempotent, so reloading this file does not stack duplicates."
|
|
(when (boundp 'dape-configs)
|
|
(setq dape-configs
|
|
(cons (cons 'flan (append (list 'command flan-dape-adapter)
|
|
(copy-sequence flan-dape-config)))
|
|
(assq-delete-all 'flan dape-configs)))))
|
|
|
|
;;;###autoload
|
|
(defun flan-debug ()
|
|
"Build the Flan file at point with debug info and start dape on it.
|
|
Equivalent to \\[dape] with the `flan' configuration, and exists so the
|
|
common case is one command rather than a config prompt."
|
|
(interactive)
|
|
(require 'dape)
|
|
(flan-dape-register)
|
|
;; Through `dape--config-eval', not `alist-get': `dape' takes a config whose
|
|
;; forms have already been evaluated — that is what \[dape] does after
|
|
;; reading one — and handing it the stored entry would pass the *list*
|
|
;; (flan-dape--binary (flan-dape--source)) to lldb as a program name.
|
|
(dape (dape--config-eval 'flan nil)))
|
|
|
|
;; Registered on load and again after dape loads, because either order
|
|
;; happens: a user may load this from their init before dape exists.
|
|
(with-eval-after-load 'dape (flan-dape-register))
|
|
(flan-dape-register)
|
|
|
|
;; The binding goes in here rather than in flan-mode.el so that this file is
|
|
;; the only thing that has to be loaded to get it, and flan-mode keeps working
|
|
;; for anyone who never installs dape. C-c C-g, for "go": every other letter
|
|
;; that suggests debugging is taken — C-c C-d is `flan-describe', C-c C-b is
|
|
;; `flan-break', which is the condition system's break loop and a different
|
|
;; thing entirely.
|
|
;;;###autoload
|
|
(with-eval-after-load 'flan-mode
|
|
(define-key (symbol-value 'flan-mode-map) (kbd "C-c C-g") #'flan-debug))
|
|
|
|
;;; --dev and --debug are different builds
|
|
;;
|
|
;; `flan dev' — what flan-dev.el connects to — builds with --dev: every
|
|
;; cross-function call goes through a cell so a redefinition can be installed,
|
|
;; and -rdynamic exports those cells. `flan build --debug' is a different
|
|
;; axis: -O0, DWARF, and no cells unless --dev is also passed.
|
|
;;
|
|
;; They compose, and `flan-dape-extra-flags' is where to say so, but they do
|
|
;; not share a process. M-x flan-debug launches its own program under lldb;
|
|
;; it does not attach to the one `flan dev' is running. Attaching to that one
|
|
;; would want lldb-dap's attach request and a pid, which is a further thing and
|
|
;; is not implemented here — said plainly rather than half-offered.
|
|
;;
|
|
;; What --dev costs the debugger is less than it sounds. A call site becomes a
|
|
;; load from a mutable global and an indirect call through the result, so the
|
|
;; callee is found at run time rather than bound at link time. Stack walking
|
|
;; is unaffected: the frame is laid out the same way and lldb reads it the
|
|
;; same way, so a backtrace through a cell still names the Flan caller and its
|
|
;; line. Stepping *into* a call is where it shows — `step' lands in whatever
|
|
;; the cell currently holds, which is the honest answer and occasionally not
|
|
;; the one on the screen, if the body was redefined since.
|
|
;;
|
|
;;; Reloading and breakpoints
|
|
;;
|
|
;; The first confusing thing anyone will hit, so it is written down rather
|
|
;; than discovered. What follows was measured with lldb against a --dev
|
|
;; --debug build of test/programs/reload.flan and a redefinition module built
|
|
;; by `flan reload'; none of it is inference.
|
|
;;
|
|
;; Each redefinition is a fresh .so that the program dlopens, and the cell is
|
|
;; then pointed at the new body. Nothing is ever dlclosed, so the old body is
|
|
;; still mapped, and every call site that has not gone through the cell again
|
|
;; still reaches it. There are therefore two live bodies, and what a
|
|
;; breakpoint does depends on how it was set.
|
|
;;
|
|
;; A breakpoint set by NAME follows the reload by itself. lldb re-resolves
|
|
;; name breakpoints against each module as it loads, so on the dlopen it prints
|
|
;; "1 location added to breakpoint 1" and then has two:
|
|
;;
|
|
;; 1: name = 'flan.bump', locations = 2, resolved = 2, hit count = 2
|
|
;; 1.1: where = host`flan.bump + 12 at reload.flan:34:3, ... hit count = 1
|
|
;; 1.2: where = v2.so`flan.bump, address = 0x00007ffff7fba1a0, ... hit count = 1
|
|
;;
|
|
;; Both fire, and both are correct — 1.1 is not stale, it is the body the old
|
|
;; call sites still run. That is `dape-breakpoint-global', which sets by name.
|
|
;;
|
|
;; A breakpoint set by FILE AND LINE depends on how the module was built, and
|
|
;; the difference is a line table and nothing about dlopen. Both halves were
|
|
;; measured against the same host, one redefinition module built each way.
|
|
;;
|
|
;; Without DWARF in the module the line breakpoint stays where it was:
|
|
;;
|
|
;; 2: file = 'v2local.flan', line = 25, locations = 0 (pending)
|
|
;;
|
|
;; and the name breakpoint still gains its second location — so dlopen was
|
|
;; never the problem — but stops into disassembly, because there is no source
|
|
;; to show:
|
|
;;
|
|
;; frame #0: 0x7ffff7fba190 nodbg-v2.so`flan.bump
|
|
;; -> 0x7ffff7fba190 <+0>: pushq %rbx
|
|
;;
|
|
;; With DWARF in the module, the same breakpoint resolves on the dlopen — lldb
|
|
;; prints "1 location added to breakpoint 3" as the module loads — and stops
|
|
;; with source and named locals:
|
|
;;
|
|
;; 3: file = 'v2local.flan', line = 25, locations = 1, resolved = 1
|
|
;; 3.1: where = v2.so`flan.bump + 78 at v2local.flan:25:21, resolved
|
|
;;
|
|
;; (lldb) frame variable
|
|
;; (long) step = 10
|
|
;; (long) prior = 11
|
|
;;
|
|
;; The stack crosses the boundary intact, which is the part worth knowing: a
|
|
;; frame in the reloaded .so and the frame below it in the host each name their
|
|
;; own .flan file, and the C host below both.
|
|
;;
|
|
;; frame #0: v2.so`flan.bump at v2local.flan:25:21
|
|
;; frame #2: host`flan.outer at reload.flan:38:20
|
|
;; frame #3: host`main at reload_host.c:94:44
|
|
;;
|
|
;; A breakpoint set by FILE AND LINE on the *host's* copy stays at locations = 1
|
|
;; and does not move. That is correct rather than stale: the old body is still
|
|
;; mapped and every call site that has not gone through the cell again still
|
|
;; reaches it, so pinning there is the only honest thing to do.
|
|
;;
|
|
;; How to get it: `flan dev --debug'. It is one flag on purpose — the host
|
|
;; needs a line table for a breakpoint to fire before the first C-c C-c, and
|
|
;; each module needs one for it to still be firing after — and it is off by
|
|
;; default because a debug build is an -O0 build, which is not what you want
|
|
;; under a frame budget unless you asked for it.
|
|
;;
|
|
;; So: with `flan dev --debug', line breakpoints work across a reload. Without
|
|
;; it, debug with `dape-breakpoint-global', which sets by name.
|
|
|
|
(provide 'flan-dape)
|
|
;;; flan-dape.el ends here
|