Preface

The premise of this collection of notes is to gather information about OCaml Debugging and Performance, and provide a single source of information about OCaml Performance and tooling, native OCaml debugging and related topics.

This work is inherently practical in nature, if you have a working knowledge of OCaml and systems performance tools then it should be possible to follow along.

Part I: Preliminaries

This section covers the foundational concepts needed before diving into performance work with OCaml. You'll learn about OCaml's compilation model, the philosophy behind effective performance optimization, and how to configure your build system for optimal results.

How to Use This Book

Who This Book Is For

What Makes OCaml Fast (and When It Isn't)

The Compilation Model: Bytecode vs Native

The OCaml Memory Model and GC

Strict Evaluation: Predictable Performance

When OCaml Surprises You

The Observability Stack for OCaml

Layered Approach: Hardware, OS, Runtime, Application

Tools at Each Layer

Measure First, Optimize Second

The USE Method for OCaml

Utilization, Saturation, and Errors

Applying USE to OCaml Resources (CPU, Memory, GC, Domains)

The Performance Triage Process

Is It CPU-Bound or Memory-Bound?

Is It GC-Bound?

Is It Algorithmic or Mechanical?

Quick Wins vs Deep Optimization

Setting Up Reproducible Benchmarks

Controlling for System Variability

Warming Up the Cache

Statistical Significance in Measurements

The Performance Checklist

Native vs Bytecode Compilation

Optimization Flags (-O2, -O3)

Flambda Enabled?

Debug Symbols and Their Cost

Release Build Configuration in Dune

Dune Profiles and Optimization Flags

Default Flags and What They Do

Setting Up Release Profiles

Common Pitfalls

The OCaml Compiler Optimization Levels

-O2 vs -O3 Behavior

-unsafe and Its Implications

-noassert for Production

-inline and Inlining Thresholds

Flambda: The Optimizing Backend

When to Use Flambda

Flambda-Specific Flags

Trade-offs: Compile Time vs Runtime Performance

Flambda 2 and Future Directions

Native Code vs Bytecode

When Bytecode Makes Sense

Performance Characteristics of Each

Part II: Measurement, Profiling, and Observation

This section covers the tools and techniques for understanding where your OCaml program spends its time and memory. Before optimizing anything, you need to measure it.

The Observability Landscape

Static Tools vs Dynamic Tracing

Counters, Profiling, and Tracing

Trade-offs: Overhead vs Detail

Tool Categories for OCaml

Hardware Counters (perf stat, CPU PMCs)

OS-Level Tools (top, vmstat, iostat)

Runtime Observability (GC Stats, Runtime Counters)

Application Profiling (Sampling Profilers)

Dynamic Tracing (USDT, eBPF, DTrace)

Choosing the Right Tool

Decision Tree for Common Problems

Platform-Specific Considerations

Linux Profiling Ecosystem

macOS Profiling (Instruments, DTrace)

Cross-Platform Tools

Profiling

This is an attempt to map the software profiling ecosystem, with a focus on the OCaml programming language. A review / survey of profilers, their data formats, analysis UIs and converters between data formats.

Architecture of profiling tools, visualisations and data formats

Profilers

There are a number of different profilers available for OCaml, each with their strengths and usecases.

ProfilerPurposeProfile Data FormatLink
ollyobservability tools around the runtime events tracing system introduced in OCaml 5.0Chrome Tracing Format (JSON), Fuchsia Trace Formatruntime-events-tool
magic-tracecollects and displays high-resolution traces of what a process is doingFuchsia Trace Formatmagic-trace
memtraceStatistical Memory Profiling for OCaml aka statmemprofCommon Trace Format (CTF)memtrace
eio-traceeio-trace can be used to record and display traces of programs using the eio libraryFuchsia Trace Formateio-trace
perfGeneral-purpose profiler that uses hardware performance counters on LinuxCommon Trace Format (CTF)
InstrumentsGeneral-purpose profiler that comes with Xcode on macOS
dtraceGeneral Performance analysis tool for FreeBSD, Solaris and MacOSdtrace
eBPFGeneral Performance analysis tool for Linux (similar to DTrace)

The intent is to have a minimal set of Profile Data Formats produced by OCaml tooling that can be used with existing Analysis UI tools.

Visualisation and Analysis UI

NameDescriptionProfile Data FormatLink
PerfettoSystem profiling, app tracing and trace analysisFuchsia Trace Format (others)perfetto.dev
SpeedscopeAn interactive flamegraph visualizerVariousgithub.com/jlfwong/speedscope
memtrace_viewerStatistical Memory ProfilerCTFgithub.com/janestreet/memtrace_viewer
flamegraphStack trace visualiserFolded Stackswww.brendangregg.com/flamegraphs.html
flamescopeVisualization tool for exploring different time ranges as Flame GraphsLinux perf scriptgithub.com/Netflix/flamescope
pprofVisualise profiling data as directed graph or flamegraphsProtocol Buffersgithub.com/google/pprof

Profile Data Format

Chrome Tracing Format is a textual JSON also know as Trace Event Format or Chrome Performance Profile. spec No OCaml libraries for reading / writing

Fuchsia Trace Format is a binary format used to collect, store, and transmit trace records. Focuses on performance. spec Two libraries for reading / writing trace-fuchsia and tracing.

Common Trace Format is a binary trace format designed to be very fast to write. Uses a declaritive language called the Trace Stream Description Language TSDL to describe trace stream layout. spec No OCaml libraries for reading / writing.

Pprof is a common stacktrace profile format using protocol buffers. Used by pprof supported as an input format by Speedscope and commercial monitoring tools (Google Cloud Profiler, Polar Signals, Datadog).

Converters

Nothing to see here!

Walk throughs

  • OCaml 5 Performance part 1/part 2 comprehensive investigation of OCaml 5 performance for an application.
  • Generating CPU Flamegraphs for OCaml on Linux here
  • Generating CPU Flamegraphs for OCaml on MacOS here
  • Memory profiling with statmemprof in 4.14 LTS and 5.3

Resources

This format was inspired by https://profilerpedia.markhansen.co.nz

Prior art:

  • OCaml-bench profiling notes https://github.com/ocaml-bench/notes/blob/master/profiling_notes.md

  • Add Profile-Guided Optimization (PGO) support to the compiler - https://github.com/ocaml/ocaml/issues/12200 [runtime-events-tools]: https://github.com/tarides/runtime_events_tools [magic-trace]: https://github.com/janestreet/magic-trace [memtrace]: https://github.com/janestreet/memtrace [dtrace]: https://dtrace.org/ [eio]: https://github.com/ocaml-multicore/eio [eio-trace]: https://github.com/ocaml-multicore/eio-trace [perfetto.dev]: https://ui.perfetto.dev/ [github.com/jlfwong/speedscope]: https://github.com/jlfwong/speedscope [github.com/janestreet/memtrace_viewer]: https://github.com/janestreet/memtrace_viewer [www.brendangregg.com/flamegraphs.html]: https://www.brendangregg.com/flamegraphs.html [github.com/Netflix/flamescope]: https://github.com/Netflix/flamescope [github.com/google/pprof]: https://github.com/google/pprof/blob/main/doc/README.md

Execution Profiling

Execution Profiling involves identifying which parts of a program are "hot" (executing frequently enough to impact runtime), causing expensive operations or simply causing "interesting" behaviour (like TLB misses or swapping off-cpu). Identifying this is best done using a profiler.

Profilers

There are many common profilers that work with OCaml, here we will focus on the following:

  • perf is a general-purpose profiler for Linux that uses hardware performance counters. Many visualisation tools like Hotspot and Firefox Profiler take perf output.
  • Instruments is a general purpose profiler provided with XCode on macOS. It uses Dtrace technology that is built into macOS and provides custom scripting of probes.
  • pmcstat is a performance measurement tool for FreeBSD that uses hardware performance counters, similar to "perf".
  • dtrace
  • samply is a sampling profiler that produces profiles that can be viewed in the Firefox Profiler. It works on Mac, Linux, and Windows.
  • flamegraph is a Cargo command that uses perf/DTrace to profile your code and then displays the results in a flame graph. It works on Linux and all platforms that support DTrace (macOS, FreeBSD, and possibly Illumos).

Debug Info

To profile a release build you should enable debug information. To do this, add "-g" to your flags in dune:

(env
 (dev TODO What is the difference between flags and ocamlopt_flags?
   (flags (:standard -g))
   (ocamlopt_flags (:standard -g)))
 (release
  (ocamlopt_flags (:standard -g))))

where dev and release correspond to build profiles. Alternatively add "-g" to the ocamlopt native compiler.

TODO How can we force "-g" for all packages in an opam switch including the OCaml compiler itself?

At the time of writing this will include Call Frame Information (CFI) suitable for unwinding the call stack and source line debug information (identifying how symbols in a binary map back to source code). Ensure that no build tools call strip on the output binaries, this will remove debug information.

Frame Pointers

Profiling OCaml with perf

  • perf support for DWARF vs FP vs LBR
  • Importance of DWARF for performance tools like perf

Take content from ocaml/ocaml#12563, ocaml/ocaml#11144 and ocaml/ocaml#11031

LBR descriptions https://lwn.net/Articles/680985/ and https://lwn.net/Articles/680996/.

perf is the official Linux profiler. It is a large tool that covers many areas of profiling, tracing and scripting. Here we will only cover the intersection between OCaml and perf, and will leave further discussion to other books like [Systems Performance by Brendan Gregg] which will do a more thorough job of it.

Profiling with Instruments

Profiling with

OCaml Flamegraphs

Generating OCaml flamegraphs on Linux

Generating OCaml flamegraphs on MacOS

Flame Graphs are a visualisation for sampled stack frames. They provide a great way to visualise stack traces of profiled software to identify the most frequent code paths and optimise them. Popularised by Brendan Gregg loads more information can be found on his blog https://www.brendangregg.com/flamegraphs.html

Here, we are interested in how we can generate Flame Graphs for OCaml programs on macOS. I use macOS for development work, and I could not find other documentation on how to do this. So here we are.

Getting Started

First, we need XCode and Instruments installed. Get them via the App Store by searching for XCode and installing it. Instruments.app is Apple's profiling tool for capturing and visualising traces, along with many other interesting things. It is built on top of the DTrace tracing framework from OpenSolaris, which was ported to Mac OS X v10.5 and is available in all following versions of macOS. Dtrace is very cool and deserves its own post on how to use it effectively on OCaml programs.

After XCode is installed confirm that Instruments is also installed, this command will find the xctrace executable:

$ xcrun --find xctrace
/Applications/Xcode.app/Contents/Developer/usr/bin/xctrace

xctrace is a command line version of Instruments which allows capturing traces from a terminal. To run a Time Profiler against an OCaml process use the following command, substituing PROGRAM with the OCaml executable. Often found under _build/default directory of a Dune project. For my project the executable is called./_build/default/stress/stress.exe

$ xctrace record --output . --template "Time Profiler" \
                 --target-stdout - --launch -- PROGRAM

This will create a trace file ending in .trace, open that trace file in Instruments, select the Time Profiler and select a Stack Thread in the lower left pane. Choose Select Edit > Deep Copy from the menu and paste the output into a file called ocaml-program.trace. On MacOS Sonoma using Instruments 15.2 it looks like:

instruments-screenshot

Next download the Perl file https://github.com/brendangregg/FlameGraph/blob/master/stackcollapse-instruments.pl to reduce the trace into a format that Flame Graphs can understand. You will need to make it executable with chmod +x stackcollapse-instruments.pl

./stackcollapse-instruments.pl ocaml-program.trace > ocaml-program.outfile

Then download the Flame Graph tool https://github.com/brendangregg/FlameGraph/blob/master/flamegraph.pl and run that against the outfile created by stackcollapse-instruments. Again make it executable with chmod +x

./flamegraph.pl ocaml-program.outfile > ocaml-program.svg

This will produce an application level Flame Graph of the traced application stored in ocaml-program.svg. Open that up in a Browser and click around to see where your OCaml program is spending it's time.

Another visualisation option is https://www.speedscope.app which can take the .trace file and produce a Flame Graph, without needing to run any scripts locally.

In Practice: Flame Graph for solver-service

Recently I have been looking at the performance of the solver-service which is used to perform opam solves for the OCaml Continuous Integration services run by Tarides. The code uses OCaml 5 with EIO and is expected to scale across multiple CPUs.

I used this xctrace command to run the stress test, setting some Garbage Collection parameters:

$ xctrace record --template "Time Profiler" \
                 --env=OCAMLRUNPARAM="M=352" --target-stdout \
                 - --launch -- ./_build/default/stress/stress.exe local \
                 --cache-dir=./cache --count=10
...
Solved warm-up requests in: 12.37s
Running another 10 solves...
10/10 complete
Solved 10 requests in 10.06s (1.01s/iter) (15.91 solves/s)
Target app exited, ending recording...
Recording completed. Saving output file...
Output file saved as: Launch_stress.exe_2024-01-18_11.45.55_042428AC.trace
$ open Launch_stress.exe_2024-01-18_11.45.55_042428AC.trace
# Export data into a file called `macos-solver-02.trace`
$ ./stackcollapse-instruments.pl macos-solver-02.trace > macos-solver-02.outfile
$ ./flamegraph.pl macos-solver-02.outfile > macos-solver-02.svg

Producing this Flame Graph for the service. Note this was running on a 12-core M3Pro MacBookPro, hence the 12 peaks relating to OCaml 5 using all available cores in this machine. macos-solver-02

Next step is working out where time is being spend and where we can speed up things.

Using Instruments on macOS

Time-based Profiling with Landmarks

Instrumenting Code with Landmarks

Overhead Considerations

Introduction to Dynamic Tracing

The Observability Gap: Beyond Sampling

DTrace, SystemTap, and eBPF/bpftrace

Platform Availability (Linux, macOS, FreeBSD, illumos)

OCaml Runtime USDT Probes

Available Probes in the Runtime

GC Phase Probes (Minor/Major Collection Start/End)

STW Synchronization Tracking

Runtime Counter Probes

Building OCaml with USDT Support

Tracing GC Behavior in Production

Debugging Slow Domain Synchronization

Application-Level USDT with ocaml_usdt

Installation and Setup

Simple Probes for Hot Paths (probe1-probe6)

String Probes and Int+String Combinations

JSON Probes for Complex Types (Usdt_json)

Performance Characteristics (Overhead When Disabled vs Enabled)

Conditional Firing and Lazy Evaluation

Tracing Workflows

Listing Probes in a Binary

Basic Tracing with bpftrace

Basic Tracing with DTrace

Aggregating and Histogramming Probe Data

Correlating Runtime Probes with Application Probes

Building Custom Analysis Tools

Writing bpftrace Scripts for OCaml Analysis

Combining USDT with perf Events

Production Tracing Considerations

Understanding OCaml Memory Layout

The Minor Heap and Major Heap

Value Representation and Boxing

When Allocation Happens

Finding memory leaks with Memtrace

Finding memory leaks with Memtrace

Using memtrace and memtrace-viewer

Installation and Setup

Recording Traces

Analyzing Allocation Patterns

Finding Memory Leaks

Other Memory Tools

memthol for Visualization

memprof-limits for Allocation Limits

Peak Memory Evaluation Techniques

GC Statistics and Tuning

Interpreting Gc.stat()

GC Control Parameters

When to Tune vs When to Fix the Code

Combining Memory Profiling with USDT

Correlating Allocation Spikes with GC Probes

Tracing Major Collection Triggers

Debugging support in the OCaml compiler

This document explains the state of debugging tools support in the OCaml compiler. It gives an overview of GDB, LLDB, WinDbg/CDB, as well as infrastructure around OCaml compiler to debug OCaml code.

The material contains both current support and ideas for future areas to improve.

Preliminaries

Debuggers

According to wikipedia 1

A debugger or debugging tool is a computer program used to test and debug other programs (the "target" program).

Writing a debugger from scratch for a language requries considerable work, especially if you want to support various platforms like Linux, MacOS, and Windows. Existing debuggers like GDB and LLDB can be extended to support debugging a language like OCaml. This is the path OCaml has chosen for what I'll call native debugging, debugging exectuables compiled to object code via assembly and run on a CPU. OCaml also includes a debugger for the bytecode exectuables it also produces, more on that later.

DWARF

According to the DWARF standard website:

DWARF is a debugging information file format used by many compilers and debuggers to support source level debugging. It addresses the requirements of a number of procedural languages, such as C, C++, and Fortran, and is designed to be extensible to other languages. DWARF is architecture independent and applicable to any processor or operating system. It is widely used on Unix, Linux and other operating systems, as well as in stand-alone environments.

DWARF allows a compiler to describe how program source translates to assembly, using a data structure called Debugging Information Entry (DIE) which stores the information as "tags" to denote functions, variables etc., e.g., DW_TAG_variable, DW_TAG_pointer_type, DW_TAG_subprogram etc. You can also invent your own tags and attributes.

DWARF reader is a program that consumes the DWARF format and creates debugger compatible output. This program may live in the compiler itself.

CFI

What is CFI? What is CFI vs DWARF? https://sourceware.org/binutils/docs/as/CFI-directives.html

CodeView/PDB

PDB (Program Database) is a file format created by Microsoft that contains debug information. PDBs can be consumed by debuggers such as WinDbg/CDB and other tools to display debug information. A PDB contains multiple streams that describe debug information about a specific binary such as types, symbols, and source files used to compile the given binary. CodeView is another format which defines the structure of symbol records and type records that appear within PDB streams.

Compact Unwinding Format

Apple introduced a new kind of unwinding info the “compact unwinding format” on Apple platforms like macOS and iOS. The Clang compiler on those platforms emits this format along with DWARF CFI. The format is described by the implementation in clang/llvm, with an independent description provided at https://faultlore.com/blah/compact-unwinding/ and https://github.com/mstange/macho-unwind-info So to generate good backtraces on Apple platforms, you need to be able to parse and interpret compact unwinding tables. Further details of how Apple uses STABS plus DWARF for debug info https://wiki.dwarfstd.org/Apple%27s_%22Lazy%22_DWARF_Scheme.md.

Supported debuggers

GDB

OCaml supports GBD on Linux for all OCaml supported platforms, it supports basic debugging like setting breakpoints, stepping, backtraces etc. A sample GDB/Linux session is shown here. Additionally there are GDB macros gdb-macros and Python macros gdb_ocamlrun.py that provide low-level debugging of OCaml programs and of the OCaml runtime itself (both native and byte-code).

OCaml parser extensions

To be able to show debug output, we need an expression parser. GDB expression parsers are written in [Bison], and could be written to accept a subset of OCaml expressions, including printing OCaml values knowing the boxed types used by OCaml. Read the Memory Representation of Values chapter of RealWorld OCaml for more details.

Future work:

  • Restore DWARF CFI for POWER native code
  • Port more GDB macros to Python
  • Add a source language to GDB https://sourceware.org/gdb/wiki/Internals%20Adding-a-Source-Language-to-GDB

LLDB

LLDB is the default debugger on MacOS, shipped with XCode and generally works the best on that platform. It is also available on Linux and is the default debugger for FreeBSD.

A sample LLDB/Linux session is shown here.

  • LLDB has a plugin architecture but that does not work for language support.
  • At present GDB generally works better on Linux.

OCaml parser extensions

This expression parser is written in C++. It is a type of Recursive Descent parser.

Some initial work on OCaml LLDB support at https://github.com/ocaml-flambda/llvm-project

What is included here?

RR

rr is a lightweight tool for recording, replaying and debugging execution of applications (trees of processes and threads). Debugging extends GDB with very efficient reverse-execution, which in combination with standard features like hardware data watchpoints, makes debugging much more fun. OCaml supports RR on Linux for certain x86_64 and ARM64 platforms.

Future work:

  • Validate RR on Linux/LLDB platform
  • RR doesn't currently support Apple M3 chips see https://github.com/rr-debugger/rr/pull/3528

WinDbg/CDB

Microsoft provides Windows Debugging Tools such as the Windows Debugger (WinDbg) and the Console Debugger (CDB) which both support debugging programs that provide PDB information. These debuggers parse the debug info for a binary from the PDB, if available, to construct a visualization to serve up in the debugger. Currently the OCaml compiler does not produce PDB and future work is required to support debugging on Windows. Additionally OCaml on Windows provides three different ports, complicating the matter further.

DWARF and OCaml

DWARF is a widely-used format for representing debug information for consumption by debugging tools but also for runtime systems and profiling. DWARF information is typically embedded within an executable and provides a way to represent a variety of information:

  • line information mapping instructions back to their location in the source program. eg assembly instruction at address x originated from main.ml at line 13
  • unwind information allowing call chains to be reconstructed from the runtime state of the exectution stack. eg OCaml program is currently running function x, which was called from y and so on. This is particularly interesting for multicore which introduced fibers and effects, and their runtime managed stack segments.
  • type information allowing debugging tools to reconstruct the structure and identity of values from the runtime state of the program. eg when an OCaml program is executing assembly instruction at address x what is the OCaml value sitting in a register.

This information allows debuggers (eg gdb or lldb) and profiling tools to do what they do.

The OCaml compiler has included DWARF support for some time, with the large changes comming from OCaml 5 and the associated runtime changes the DWARF support needed to be restored and improved.

There are a number of potential user-cases for DWARF information:

  1. Use in native debugging tools like gdb and lldb
  2. Statisical profiling using tools like perf
  3. Computing call stacks for ThreadSanitizer, a data race detection tool

OCaml to DWARF DIE Mapping

Provide a small example of how OCaml gets mapped to DWARF DIEs

DW_TAG_base_type provide base type mappings that can be defined per language. Should OCaml be using this to output DWARF representations.

DW_TAG_variable describes a variable in the source language

https://dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf

$ 
COMPILE_UNIT<header overall offset = 0x00000047>:
< 0><0x0000000b>  DW_TAG_compile_unit
                    DW_AT_stmt_list             0x00000062
                    DW_AT_low_pc                0x0004b6a0
                    DW_AT_high_pc               0x0004b760
                    DW_AT_name                  fib.ml
                    DW_AT_comp_dir              /home/tsmc/ocaml
                    DW_AT_producer              GNU AS 2.41
                    DW_AT_language              DW_LANG_Mips_Assembler

LOCAL_SYMBOLS:
< 1><0x0000002e>    DW_TAG_subprogram
                      DW_AT_name                  camlFib__main_1_3_code
                      DW_AT_external              yes(1)
                      DW_AT_type                  <0x00000073>
                      DW_AT_low_pc                0x0004b6f8
                      DW_AT_high_pc               0x0004b73c
< 1><0x00000045>    DW_TAG_subprogram
                      DW_AT_name                  camlFib__fib_0_2_code
                      DW_AT_external              yes(1)
                      DW_AT_type                  <0x00000073>
                      DW_AT_low_pc                0x0004b6a0
                      DW_AT_high_pc               0x0004b6f4
< 1><0x0000005c>    DW_TAG_subprogram
                      DW_AT_name                  camlFib__entry
                      DW_AT_external              yes(1)
                      DW_AT_type                  <0x00000073>
                      DW_AT_low_pc                0x0004b740
                      DW_AT_high_pc               0x0004b760
< 1><0x00000073>    DW_TAG_unspecified_type

OCaml Name Mangling

What is missing?

  • Integration with IDEs (Debug Adapter Protocol)

Resources

Tom Tromey discusses debugging support in rustc, provides a good overview of the area and what OCaml might also want to do - https://www.youtube.com/watch?v=elBxMRSNYr4

https://rustc-dev-guide.rust-lang.org/debugging-support-in-rustc.html

DWARF support in GHC (4 part series) https://well-typed.com/blog/2020/04/dwarf-1/

Sample GDB on Linux debugging session

Starting from an OCaml 4.14 switch, create one if it doesn't already exist with opam switch create 4.14.1 --no-install.

$ opam switch
#  switch                                              compiler                    description
   4.14.1                                              ocaml-base-compiler.4.14.1  4.14.1

Consider this program:

$ cat fib.ml
let rec fib n =
  if n < 2 then 1
	else fib (n-1) + fib (n-2)

let main () =
  let r = fib 20 in
	Printf.printf "fib(20) = %d" r

let _ = main ()

compiled with

$ ocamlopt -g -o fib.exe fib.ml

Here the OCaml from our fib program gets name mangled into the following:

$ nm -pa fib.exe|grep "camlFib"
000000010005da58 D _camlFib
000000010005daf0 D _camlFib__1
000000010005dac8 D _camlFib__2
000000010005dab0 D _camlFib__3
000000010005da98 D _camlFib__4
000000010005da80 D _camlFib__5
000000010005da40 D _camlFib__6
000000010005da28 D _camlFib__7
0000000100003838 T _camlFib__code_begin
0000000100003928 T _camlFib__code_end
000000010005da20 D _camlFib__data_begin
000000010005db08 D _camlFib__data_end
00000001000038e8 T _camlFib__entry
0000000100003838 T _camlFib__fib_267
000000010005db10 D _camlFib__frametable
000000010005da68 D _camlFib__gc_roots
0000000100003890 T _camlFib__main_269

OCaml functions are mangled as caml<MODULENAME>__<FUNCTIONNAME>_<RANDOMINT>. The numbers used can be recovered from the lambda format. Re-running the command with -dlambda will output the lamdba form, and -S will output the assembly for the program as fib.S. You can see the symbol _camlFib__main_269 is coming from the main/269 seen in the lambda format.

$ ocamlopt -dlambda -g -S -o fib.exe fib.ml
(seq
  (letrec
    (fib/267
       (function n/268[int] : int
         (if (< n/268 2) 1
           (+ (apply fib/267 (- n/268 1)) (apply fib/267 (- n/268 2))))))
    (setfield_ptr(root-init) 0 (global Fib!) fib/267))
  (let
    (main/269 =
       (function param/308[int] : int
         (let (r/271 =[int] (apply (field 0 (global Fib!)) 20))
           (apply (field 1 (global Stdlib__Printf!))
             [0: [11: "fib(20) = " [4: 0 0 0 0]] "fib(20) = %d"] r/271))))
    (setfield_ptr(root-init) 1 (global Fib!) main/269))
  (apply (field 1 (global Fib!)) 0) 0 0)
$ gdb fib.exe
GNU gdb (Ubuntu 14.0.50.20230907-0ubuntu1) 14.0.50.20230907-git
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from fib.exe...
(gdb) break camlFib__fib_267
Breakpoint 1 at 0x4f9b8: file fib.ml, line 1.
(gdb) r
Starting program: /home/tsmc/ocaml/fib-4.14.1.exe 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".

Breakpoint 1, camlFib__fib_267 () at fib.ml:1
1	let rec fib n =
(gdb) bt
#0  camlFib__fib_267 () at fib.ml:1
#1  0x0000aaaaaaaefa2c in camlFib__main_269 () at fib.ml:6
#2  0x0000aaaaaaaefa98 in camlFib__entry () at fib.ml:9
#3  0x0000aaaaaaaec0a4 in caml_program ()
#4  0x0000aaaaaab337d4 in caml_start_program ()
#5  0x0000aaaaaab34090 in caml_startup_common (argv=0xaaaaaab709c8, 
    pooling=<optimized out>, pooling@entry=0) at startup_nat.c:160
#6  0x0000aaaaaab34110 in caml_startup_exn (argv=<optimized out>)
    at startup_nat.c:167
#7  caml_startup (argv=<optimized out>) at startup_nat.c:172
#8  caml_main (argv=<optimized out>) at startup_nat.c:179
#9  0x0000aaaaaaaebdd0 in main (argc=<optimized out>, argv=<optimized out>)
    at main.c:37
(gdb) 

Observe that I have the full backtrace all the way from the main function in the runtime.

You can keep continuing and the backtrace continues to build up, showing the recursive calls to fib.

(gdb) c
Continuing.

Breakpoint 1, camlFib__fib_267 () at fib.ml:1
1	let rec fib n =
(gdb) bt
#0  camlFib__fib_267 () at fib.ml:1
#1  0x0000aaaaaaaef9e4 in camlFib__fib_267 () at fib.ml:3
#2  0x0000aaaaaaaefa2c in camlFib__main_269 () at fib.ml:6
#3  0x0000aaaaaaaefa98 in camlFib__entry () at fib.ml:9
#4  0x0000aaaaaaaec0a4 in caml_program ()
#5  0x0000aaaaaab337d4 in caml_start_program ()
#6  0x0000aaaaaab34090 in caml_startup_common (argv=0xaaaaaab709c8, pooling=<optimized out>, pooling@entry=0)
    at startup_nat.c:160
#7  0x0000aaaaaab34110 in caml_startup_exn (argv=<optimized out>) at startup_nat.c:167
#8  caml_startup (argv=<optimized out>) at startup_nat.c:172
#9  caml_main (argv=<optimized out>) at startup_nat.c:179
#10 0x0000aaaaaaaebdd0 in main (argc=<optimized out>, argv=<optimized out>) at main.c:37
(gdb) c
Continuing.

Breakpoint 1, camlFib__fib_267 () at fib.ml:1
1	let rec fib n =
(gdb) bt
#0  camlFib__fib_267 () at fib.ml:1
#1  0x0000aaaaaaaef9e4 in camlFib__fib_267 () at fib.ml:3
#2  0x0000aaaaaaaef9e4 in camlFib__fib_267 () at fib.ml:3
#3  0x0000aaaaaaaefa2c in camlFib__main_269 () at fib.ml:6
#4  0x0000aaaaaaaefa98 in camlFib__entry () at fib.ml:9
#5  0x0000aaaaaaaec0a4 in caml_program ()
#6  0x0000aaaaaab337d4 in caml_start_program ()
#7  0x0000aaaaaab34090 in caml_startup_common (argv=0xaaaaaab709c8, pooling=<optimized out>, pooling@entry=0)
    at startup_nat.c:160
#8  0x0000aaaaaab34110 in caml_startup_exn (argv=<optimized out>) at startup_nat.c:167
#9  caml_startup (argv=<optimized out>) at startup_nat.c:172
#10 caml_main (argv=<optimized out>) at startup_nat.c:179
#11 0x0000aaaaaaaebdd0 in main (argc=<optimized out>, argv=<optimized out>) at main.c:37
(gdb)

Here we are on Linux / ARM64, so while the values cannot be printed directly, I know that the arguments are sent in registers, for ARM64 the first 4 arguments are passed in registers x0-x3. I can examine the value at entry to the function like so:

(gdb) p $x0 >> 1
$1 = 16
(gdb) 

right shifting by 1 due to OCaml value representation, which uses 31 bits for integer values.

(gdb) c
Continuing.

Breakpoint 1, camlFib__fib_267 () at fib.ml:1

(gdb) p $x0 >> 1
$3 = 14

You can also set break points based on the line numbers in gbd.

(gdb) list
1	let rec fib n =
2	  if n < 2 then 1
3	  else fib (n-1) + fib (n-2)
4	
5	let main () =
6	  let r = fib 20 in
7	  Printf.printf "fib(20) = %d" r
8	
9	let _ = main ()
(gdb) break fib.ml:6
Breakpoint 2 at 0xaaaaaaaefa28: file fib.ml, line 6.

From this point you can run the entire OCaml program, setting breakpoints and interacting with it as you would a regular C/C++ program.

Sample LLDB on MacOS session

Starting from an OCaml 4.14 switch, create one if it doesn't already exist with opam switch create 4.14.1 --no-install.

$ opam switch
#  switch                                              compiler                    description
   4.14.1                                              ocaml-base-compiler.4.14.1  4.14.1

Consider this program:

$ cat fib.ml
let rec fib n =
  if n < 2 then 1
	else fib (n-1) + fib (n-2)

let main () =
  let r = fib 20 in
	Printf.printf "fib(20) = %d" r

let _ = main ()

compiled with

$ ocamlopt -g -o fib-4.14.1.exe fib.ml

Here the OCaml from our fib program gets name mangled into the following:

$ nm -pa fib-4.14.1.exe|grep "camlFib"
000000010005da58 D _camlFib
000000010005daf0 D _camlFib__1
000000010005dac8 D _camlFib__2
000000010005dab0 D _camlFib__3
000000010005da98 D _camlFib__4
000000010005da80 D _camlFib__5
000000010005da40 D _camlFib__6
000000010005da28 D _camlFib__7
0000000100003838 T _camlFib__code_begin
0000000100003928 T _camlFib__code_end
000000010005da20 D _camlFib__data_begin
000000010005db08 D _camlFib__data_end
00000001000038e8 T _camlFib__entry
0000000100003838 T _camlFib__fib_267
000000010005db10 D _camlFib__frametable
000000010005da68 D _camlFib__gc_roots
0000000100003890 T _camlFib__main_269

OCaml functions are mangled as caml<MODULENAME>__<FUNCTIONNAME>_<RANDOMINT>. The numbers used can be recovered from the lambda format. Re-running the command with -dlambda will output the lamdba form, and -S will output the assembly for the program as fib.S. You can see the symbol _camlFib__main_269 is coming from the main/269 seen in the lambda format.

$ ocamlopt -dlambda -g -S -o fib-4.14.1.exe fib.ml
(seq
  (letrec
    (fib/267
       (function n/268[int] : int
         (if (< n/268 2) 1
           (+ (apply fib/267 (- n/268 1)) (apply fib/267 (- n/268 2))))))
    (setfield_ptr(root-init) 0 (global Fib!) fib/267))
  (let
    (main/269 =
       (function param/308[int] : int
         (let (r/271 =[int] (apply (field 0 (global Fib!)) 20))
           (apply (field 1 (global Stdlib__Printf!))
             [0: [11: "fib(20) = " [4: 0 0 0 0]] "fib(20) = %d"] r/271))))
    (setfield_ptr(root-init) 1 (global Fib!) main/269))
  (apply (field 1 (global Fib!)) 0) 0 0)
 $ lldb fib-4.14.1.exe 
(lldb) target create "fib-4.14.1.exe"
Current executable set to '/Users/tsmc/projects/ocaml/fib-4.14.1.exe' (arm64).
(lldb) br s -n camlFib__fib_267
Breakpoint 1: where = fib-4.14.1.exe`camlFib__code_begin, address = 0x0000000100003838
(lldb) r
Process 63927 launched: '/Users/tsmc/projects/ocaml/fib-4.14.1.exe' (arm64)
Process 63927 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
fib-4.14.1.exe`camlFib__code_begin:
->  0x100003838 <+0>:  sub    sp, sp, #0x20
    0x10000383c <+4>:  str    x30, [sp, #0x18]
    0x100003840 <+8>:  cmp    x0, #0x5
    0x100003844 <+12>: b.ge   0x100003858               ; <+32>
Target 0: (fib-4.14.1.exe) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
    frame #1: 0x00000001000038ac fib-4.14.1.exe`camlFib__code_begin + 116
    frame #2: 0x0000000100029c44 fib-4.14.1.exe`caml_startup_common(argv=<unavailable>, pooling=<unavailable>) at startup_nat.c:160:9 [opt]
    frame #3: 0x0000000100029cb8 fib-4.14.1.exe`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup_nat.c:167:10 [opt]
    frame #4: 0x0000000100029cb0 fib-4.14.1.exe`caml_main [inlined] caml_startup(argv=<unavailable>) at startup_nat.c:172:15 [opt]
    frame #5: 0x0000000100029cb0 fib-4.14.1.exe`caml_main(argv=<unavailable>) at startup_nat.c:179:3 [opt]
    frame #6: 0x0000000100029d18 fib-4.14.1.exe`main(argc=<unavailable>, argv=<unavailable>) at main.c:37:3 [opt]
    frame #7: 0x000000018863d0e0 dyld`start + 2360

Observe that I have the full backtrace all the way from the main function in the runtime.

You can keep continuing and the backtrace continues to build up, showing the recursive calls to fib.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
    frame #1: 0x0000000100003864 fib-4.14.1.exe`camlFib__code_begin + 44
    frame #2: 0x00000001000038ac fib-4.14.1.exe`camlFib__code_begin + 116
    frame #3: 0x0000000100029c44 fib-4.14.1.exe`caml_startup_common(argv=<unavailable>, pooling=<unavailable>) at startup_nat.c:160:9 [opt]
    frame #4: 0x0000000100029cb8 fib-4.14.1.exe`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup_nat.c:167:10 [opt]
    frame #5: 0x0000000100029cb0 fib-4.14.1.exe`caml_main [inlined] caml_startup(argv=<unavailable>) at startup_nat.c:172:15 [opt]
    frame #6: 0x0000000100029cb0 fib-4.14.1.exe`caml_main(argv=<unavailable>) at startup_nat.c:179:3 [opt]
    frame #7: 0x0000000100029d18 fib-4.14.1.exe`main(argc=<unavailable>, argv=<unavailable>) at main.c:37:3 [opt]
    frame #8: 0x000000018863d0e0 dyld`start + 2360
(lldb) c
Process 63927 resuming
Process 63927 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
fib-4.14.1.exe`camlFib__code_begin:
->  0x100003838 <+0>:  sub    sp, sp, #0x20
    0x10000383c <+4>:  str    x30, [sp, #0x18]
    0x100003840 <+8>:  cmp    x0, #0x5
    0x100003844 <+12>: b.ge   0x100003858               ; <+32>
Target 0: (fib-4.14.1.exe) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
    frame #1: 0x0000000100003864 fib-4.14.1.exe`camlFib__code_begin + 44
    frame #2: 0x0000000100003864 fib-4.14.1.exe`camlFib__code_begin + 44
    frame #3: 0x00000001000038ac fib-4.14.1.exe`camlFib__code_begin + 116
    frame #4: 0x0000000100029c44 fib-4.14.1.exe`caml_startup_common(argv=<unavailable>, pooling=<unavailable>) at startup_nat.c:160:9 [opt]
    frame #5: 0x0000000100029cb8 fib-4.14.1.exe`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup_nat.c:167:10 [opt]
    frame #6: 0x0000000100029cb0 fib-4.14.1.exe`caml_main [inlined] caml_startup(argv=<unavailable>) at startup_nat.c:172:15 [opt]
    frame #7: 0x0000000100029cb0 fib-4.14.1.exe`caml_main(argv=<unavailable>) at startup_nat.c:179:3 [opt]
    frame #8: 0x0000000100029d18 fib-4.14.1.exe`main(argc=<unavailable>, argv=<unavailable>) at main.c:37:3 [opt]
    frame #9: 0x000000018863d0e0 dyld`start + 2360
(lldb)

Here we are on MacOS / ARM64, so while the values cannot be printed directly, I know that the arguments are sent in registers, for ARM64 the first 4 arguments are passed in registers x0-x3. I can examine the value at entry to the function like so:

(lldb) p $x0 >> 1
(unsigned long) 16
(lldb)  

right shifting by 1 due to OCaml value representation, which uses 31 bits for integer values.

(lldb) c
Process 63927 resuming
Process 63927 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003838 fib-4.14.1.exe`camlFib__code_begin
fib-4.14.1.exe`camlFib__code_begin:
->  0x100003838 <+0>:  sub    sp, sp, #0x20
    0x10000383c <+4>:  str    x30, [sp, #0x18]
    0x100003840 <+8>:  cmp    x0, #0x5
    0x100003844 <+12>: b.ge   0x100003858               ; <+32>
Target 0: (fib-4.14.1.exe) stopped.
(lldb) p $x0 >> 1
(unsigned long) 14

From this point you can run the entire OCaml program, setting breakpoints and interacting with it as you would a regular C/C++ program.

Issues

  • Setting breakpoints using line numbers eg br s -f fib.ml -l 6 does not work in 4.14, 5.* or flambda2 (ARM64).
  • Setting breakpoints using symbols eg br s -n camlFib__fib_267 is broken in OCaml 5.1 onwards. OCaml 5.1 changed name mangling to use . separators over __ which breaks lldb on MacOS. Linux LLDB is unaffected.
  • Backtraces in 4.14 onwards show offset camlFib__code_begin + 116 rather than line number in source code
  • Backtraces in 5.0 onwards missing C code for OCaml runtime setup, beginning at camlFib__code_begin

Wed 6 Mar 14:48:12 2024

$ cat fib.ml
let rec fib n =
  if n < 2 then 1
  else fib (n-1) + fib (n-2)

let main () =
  let r = fib 20 in
  Printf.printf "fib(20) = %d" r

let _ = main ()

$ ocamlopt -g -o fib.exe fib.ml

$ dsymutil --symtab fib.exe |grep camlFib
----------------------------------------------------------------------
Symbol table for: 'fib.exe' (arm64)
----------------------------------------------------------------------
Index    n_strx   n_type             n_sect n_desc n_value
======== -------- ------------------ ------ ------ ----------------
[     0] 00013fc5 0e (     SECT    ) 01     0000   0000000100035590 '_caml_array_gather'
....
[  5471] 000057e7 0f (     SECT EXT) 09     0200   000000010006da60 '_camlFib'
[  5472] 000057f0 0f (     SECT EXT) 09     0200   000000010006daf8 '_camlFib$1'
[  5473] 000057fb 0f (     SECT EXT) 09     0200   000000010006dad0 '_camlFib$2'
[  5474] 00005806 0f (     SECT EXT) 09     0200   000000010006dab8 '_camlFib$3'
[  5475] 00005811 0f (     SECT EXT) 09     0200   000000010006daa0 '_camlFib$4'
[  5476] 0000581c 0f (     SECT EXT) 09     0200   000000010006da88 '_camlFib$5'
[  5477] 00005827 0f (     SECT EXT) 09     0200   000000010006da48 '_camlFib$6'
[  5478] 00005832 0f (     SECT EXT) 09     0200   000000010006da30 '_camlFib$7'
[  5479] 0000583d 0f (     SECT EXT) 01     0000   00000001000071f8 '_camlFib$code_begin'
[  5480] 00005851 0f (     SECT EXT) 01     0200   0000000100007394 '_camlFib$code_end'
[  5481] 00005863 0f (     SECT EXT) 09     0000   000000010006da28 '_camlFib$data_begin'
[  5482] 00005877 0f (     SECT EXT) 09     0200   000000010006db10 '_camlFib$data_end'
[  5483] 00005889 0f (     SECT EXT) 01     0200   0000000100007318 '_camlFib$entry'
[  5484] 00005898 0f (     SECT EXT) 01     0200   0000000100007210 '_camlFib$fib_270'
[  5485] 000058a9 0f (     SECT EXT) 09     0200   000000010006db18 '_camlFib$frametable'
[  5486] 000058bd 0f (     SECT EXT) 09     0200   000000010006da70 '_camlFib$gc_roots'
[  5487] 000058cf 0f (     SECT EXT) 01     0200   00000001000072a0 '_camlFib$main_272'

$ nm -pa fib.exe |grep camlFib
000000010006da60 D _camlFib
000000010006daf8 D _camlFib$1
000000010006dad0 D _camlFib$2
000000010006dab8 D _camlFib$3
000000010006daa0 D _camlFib$4
000000010006da88 D _camlFib$5
000000010006da48 D _camlFib$6
000000010006da30 D _camlFib$7
00000001000071f8 T _camlFib$code_begin
0000000100007394 T _camlFib$code_end
000000010006da28 D _camlFib$data_begin
000000010006db10 D _camlFib$data_end
0000000100007318 T _camlFib$entry
0000000100007210 T _camlFib$fib_270
000000010006db18 D _camlFib$frametable
000000010006da70 D _camlFib$gc_roots
00000001000072a0 T _camlFib$main_272
image lookup -r -n camlFib
5 matches found in /Users/tsmc/code/ocaml/ocaml/fib-5.3.0.exe:
        Address: fib-5.3.0.exe[0x00000001000071f8] (fib-5.3.0.exe.__TEXT.__text + 11408)
        Summary: fib-5.3.0.exe`camlFib$code_begin        Address: fib-5.3.0.exe[0x0000000100007394] (fib-5.3.0.exe.__TEXT.__text + 11820)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 412        Address: fib-5.3.0.exe[0x0000000100007318] (fib-5.3.0.exe.__TEXT.__text + 11696)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 288        Address: fib-5.3.0.exe[0x0000000100007210] (fib-5.3.0.exe.__TEXT.__text + 11432)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 24        Address: fib-5.3.0.exe[0x00000001000072a0] (fib-5.3.0.exe.__TEXT.__text + 11576)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 168

Noticing that the OCaml executable has entries for OCaml compiler runtime but not the fib executable. We need OSO debug information like the Rust example above. See compile_unit_proto_die from flambda-backend repo that apparently emits the right SO/OSO combinations. Need to validate this works. Off to build flambda-backend on MacOS.

Hacked assembly file to include .file fib.ml at the top of the file.

Using flambda we get these debug symbols:

(lldb) image lookup -r -n camlFib
5 matches found in /Users/tsmc/code/ocaml/flambda-backend/fib-jst.exe:
        Address: fib-jst.exe[0x0000000100003518] (fib-jst.exe.__TEXT.__text + 10448)
        Summary: fib-jst.exe`camlFib__code_begin        Address: fib-jst.exe[0x00000001000035e0] (fib-jst.exe.__TEXT.__text + 10648)
        Summary: fib-jst.exe`camlCamlinternalFormatBasics__code_begin        Address: fib-jst.exe[0x00000001000035c0] (fib-jst.exe.__TEXT.__text + 10616)
        Summary: fib-jst.exe`camlFib__entry        Address: fib-jst.exe[0x0000000100003520] (fib-jst.exe.__TEXT.__text + 10456)
        Summary: fib-jst.exe`camlFib__fib_0_2_code        Address: fib-jst.exe[0x0000000100003578] (fib-jst.exe.__TEXT.__text + 10544)
        Summary: fib-jst.exe`camlFib__main_1_3_code

and the backtrace has C and OCaml frames:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 11.1
  * frame #0: 0x0000000100003520 fib-jst.exe`camlFib__fib_0_2_code
    frame #1: 0x000000010000354c fib-jst.exe`camlFib__fib_0_2_code + 44
    frame #2: 0x000000010000354c fib-jst.exe`camlFib__fib_0_2_code + 44
    frame #3: 0x000000010000354c fib-jst.exe`camlFib__fib_0_2_code + 44
    frame #4: 0x0000000100003588 fib-jst.exe`camlFib__main_1_3_code + 16
    frame #5: 0x00000001000035d0 fib-jst.exe`camlFib__entry + 16
    frame #6: 0x0000000100000c84 fib-jst.exe`caml_program + 52
    frame #7: 0x000000010005718c fib-jst.exe`caml_start_program + 104
    frame #8: 0x00000001000309f0 fib-jst.exe`caml_startup_common(argv=0x000000016fdfedc8, pooling=<unavailable>) at startup_nat.c:165:9 [opt]
    frame #9: 0x0000000100030a6c fib-jst.exe`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup_nat.c:175:10 [opt]
    frame #10: 0x0000000100030a64 fib-jst.exe`caml_main [inlined] caml_startup(argv=<unavailable>) at startup_nat.c:180:15 [opt]
    frame #11: 0x0000000100030a64 fib-jst.exe`caml_main(argv=<unavailable>) at startup_nat.c:187:3 [opt]
    frame #12: 0x0000000100030acc fib-jst.exe`main(argc=<unavailable>, argv=<unavailable>) at main.c:37:3 [opt]
    frame #13: 0x0000000186a9d0e0 dyld`start + 2360

OCaml 5.3 +trunk

(lldb) image lookup -r -n camlFib
5 matches found in /Users/tsmc/code/ocaml/ocaml/fib-5.3.0.exe:
        Address: fib-5.3.0.exe[0x00000001000071f8] (fib-5.3.0.exe.__TEXT.__text + 11408)
        Summary: fib-5.3.0.exe`camlFib$code_begin        Address: fib-5.3.0.exe[0x0000000100007394] (fib-5.3.0.exe.__TEXT.__text + 11820)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 412        Address: fib-5.3.0.exe[0x0000000100007318] (fib-5.3.0.exe.__TEXT.__text + 11696)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 288        Address: fib-5.3.0.exe[0x0000000100007210] (fib-5.3.0.exe.__TEXT.__text + 11432)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 24        Address: fib-5.3.0.exe[0x00000001000072a0] (fib-5.3.0.exe.__TEXT.__text + 11576)
        Summary: fib-5.3.0.exe`camlFib$code_begin + 168

(lldb) br s -n camlFib$main_272
Breakpoint 1: where = fib-5.3.0.exe`camlFib$code_begin + 168, address = 0x00000001000072a0

In the backtrace we have this for C runtime files:

0000000000000000 - 01 0000    SO 
0000000000000000 - 00 0000    SO /Users/tsmc/code/ocaml/flambda-backend/_build/runtime_stdlib/ocaml/runtime4/
0000000000000000 - 00 0000    SO codefrag.c
0000000065e99e6e - 00 0001   OSO /Users/tsmc/code/ocaml/flambda-backend/lib/ocaml/libasmrun.a(codefrag.n.o)
0000000100056d58 - 01 0000 BNSYM 
0000000100056d58 - 01 0000   FUN _caml_register_code_fragment
00000000000000bc - 00 0000   FUN 
0000000100056d58 - 01 0000 ENSYM 
...
000000010008afa0 S _Caml_state
0000000100000000 T __mh_execute_header
0000000100078eb0 D _camlCamlinternalFormat
0000000100069c30 D _camlCamlinternalFormatBasics

Build using verbose commands, dump lambda representation and assembly file:

$ opam exec --switch="4.14.1" -- ocamlopt -dlambda -verbose -g -S -o fib-4.14.1.exe fib.ml 
(seq
  (letrec
    (fib/267
       (function n/268[int] : int
         (if (== n/268 0) 0
           (if (== n/268 1) 1
             (+ (apply fib/267 (- n/268 1)) (apply fib/267 (- n/268 2)))))))
    (setfield_ptr(root-init) 0 (global Fib!) fib/267))
  (let
    (main/269 =
       (function param/308[int] : int
         (let (r/271 =[int] (apply (field 0 (global Fib!)) 200))
           (apply (field 1 (global Stdlib__Printf!))
             [0: [11: "fib(200) = " [4: 0 0 0 0]] "fib(200) = %d"] r/271))))
    (setfield_ptr(root-init) 1 (global Fib!) main/269))
  (apply (field 1 (global Fib!)) 0) 0 0)
+ cc -c -Wno-trigraphs  -o 'fib.o' 'fib.s'
+ cc -c -Wno-trigraphs  -o '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartupc4c0a3.o' '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartup0f5911.s'
+ cc -O2 -fno-strict-aliasing -fwrapv -pthread -Wall -Wdeclaration-after-statement -fno-common    -Wl,-no_compact_unwind -o 'fib-4.14.1.exe'  '-L/Users/tsmc/.opam/4.14.1/lib/ocaml'  '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartupc4c0a3.o' '/Users/tsmc/.opam/4.14.1/lib/ocaml/std_exit.o' 'fib.o' '/Users/tsmc/.opam/4.14.1/lib/ocaml/stdlib.a' '/Users/tsmc/.opam/4.14.1/lib/ocaml/libasmrun.a' -lm 

Looking at cargo/rust binaries compiled with Debug turned on

$ nm -pa target/debug/dwarfdump
....
0000000000000000 - 01 0000    SO 
0000000000000000 - 00 0000    SO /Users/tsmc/code/rust/gimli/crates/examples/src/bin/dwarfdump.rs/@/
0000000000000000 - 00 0000    SO 16r479fmyrfz78wy
0000000000000000 - 00 0001   OSO /Users/tsmc/code/rust/gimli/target/debug/deps/dwarfdump-7bf704beafb18da7.16r479fmyrfz78wy.rcgu.o
0000000100006850 - 01 0000 BNSYM 
0000000100006850 - 01 0000   FUN __ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17hd091cb1a61463214E
0000000000000034 - 00 0000   FUN 
0000000100006850 - 01 0000 ENSYM 

We get a section of SO SO SO OSO BNSYM FUN FUN ENSYM

Adding .file 0 "/home/tsmc" "fib.c" section to the top of the assembly generated by each compilation unit. Restores an accurate backtrace and improves setting breakpoints based on symbol names.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x0000000100004f98 fib-test.exe`camlFib$fib_270
    frame #1: 0x0000000100004fec fib-test.exe`camlFib$fib_270 + 84
    frame #2: 0x0000000100005054 fib-test.exe`camlFib$main_272 + 44
    frame #3: 0x000000010000510c fib-test.exe`camlFib$entry + 108
    frame #4: 0x00000001000024e4 fib-test.exe`caml_program + 476
    frame #5: 0x0000000100060124 fib-test.exe`caml_start_program + 132
    frame #6: 0x00000001000024e4 fib-test.exe`caml_program + 476
(lldb) image lookup -r -n camlFib
5 matches found in /Users/tsmc/code/ocaml/ocaml/fib-test.exe:
        Address: fib-test.exe[0x0000000100004f80] (fib-test.exe.__TEXT.__text + 11408)
        Summary: fib-test.exe`camlFib$code_begin        Address: fib-test.exe[0x000000010000511c] (fib-test.exe.__TEXT.__text + 11820)
        Summary: fib-test.exe`camlFib$code_end        Address: fib-test.exe[0x00000001000050a0] (fib-test.exe.__TEXT.__text + 11696)
        Summary: fib-test.exe`camlFib$entry        Address: fib-test.exe[0x0000000100004f98] (fib-test.exe.__TEXT.__text + 11432)
        Summary: fib-test.exe`camlFib$fib_270        Address: fib-test.exe[0x0000000100005028] (fib-test.exe.__TEXT.__text + 11576)
        Summary: fib-test.exe`camlFib$main_272
(lldb) br list
Current breakpoints:
1: name = 'camlFib$main_272', locations = 1, resolved = 1, hit count = 1
  1.1: where = fib-test.exe`camlFib$main_272, address = 0x0000000100005028, resolved, hit count = 1 

2: name = 'camlFib$fib_270', locations = 1, resolved = 1, hit count = 2
  2.1: where = fib-test.exe`camlFib$fib_270, address = 0x0000000100004f98, resolved, hit count = 2 

LLVM doesn't allow STABS directives in assembly files but seems to generate some of the information using .file and .loc directives. OCaml doesn't generate .loc equivalent to clang. It needs to be modified to use the more verbose .loc 1 4 14 is_stmt 0 ; fib.c:4:14 sections. The CFI generated by ARM64 might not be correct, seems like we don't generate enough details.

LOC showing .stab directives being rejected https://github.com/llvm/llvm-project/blob/release/15.x/llvm/lib/MC/MCParser/AsmParser.cpp#L3729. In theory we could override using llvm-as and use GNU as instead, this would yield direct STABS support. Probably should use the provided tooling.

The "stabs" debug format - https://opensource.apple.com/source/gdb/gdb-250/doc/stabs.pdf

Compared to Flambda2 JST we are missing C parts of the stack trace and locations of the source code

    frame #7: 0x000000010005718c fib-jst.exe`caml_start_program + 104
    frame #8: 0x00000001000309f0 fib-jst.exe`caml_startup_common(argv=0x000000016fdfedc8, pooling=<unavailable>) at startup_nat.c:165:9 [opt]
    frame #9: 0x0000000100030a6c fib-jst.exe`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup_nat.c:175:10 [opt]
    frame #10: 0x0000000100030a64 fib-jst.exe`caml_main [inlined] caml_startup(argv=<unavailable>) at startup_nat.c:180:15 [opt]

Mon 18 Mar 10:47:51 2024

Flambda / 4.14 -> Missing source mappings to ML files 5.0.0 -> truncated stack without C frames 5.1.1 -> Can't set named breakpoints -> truncated stack without C frames 5.3 -> name mangling restores setting named breakpoints -> Still contains truncated stack without C frames.

Why? Missing framepointers? Especially in jump to caml_start_program

Mon 1 Apr 10:28:28 2024

Adding debugger support for your target https://llvm.org/devmtg/2016-03/Tutorials/LLDB-tutorial.pdf Useful for debugging why symbols don't load.

Use log enable lldb unwind

fib-test-2.exe`camlCamlinternalFormatBasics$entry:
->  0x100006f28 <+32>: ldr    x16, [x28, #0x40]
    0x100006f2c <+36>: mov    sp, x16
    0x100006f30 <+40>: bl     0x100064440               ; symbol stub for: caml_system__code_end + 168
    0x100006f34 <+44>: mov    sp, x29
Target 0: (fib-test-2.exe) stopped.
(lldb) bt
 th1/fr0 supplying caller's saved fp (29)'s location, cached
 th1/fr0 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
 th1/fr0 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
 th1/fr0 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030008]
 th1/fr0 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
 th1/fr0 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
 th1/fr0 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030008]
  th1/fr1 pc = 0x100002fdc
 th1/fr0 supplying caller's saved fp (29)'s location, cached
  th1/fr1 fp = 0x148030000
 th1/fr0 supplying caller's saved sp (31)'s location, cached
  th1/fr1 sp = 0x148030010
  th1/fr1 with pc value of 0x100002fdc, symbol name is 'caml_program'
  th1/fr1 Backing up the pc value of 0x100002fdc by 1 and re-doing symbol lookup; old symbol was caml_program
  th1/fr1 Symbol is now caml_program
  th1/fr1 Using full unwind plan 'eh_frame CFI'
  th1/fr1 active row: 0x0000000100002fd4: CFA=sp+16 => lr=[CFA-8] 
 th1/fr0 supplying caller's saved sp (31)'s location, cached
  th1/fr1 CFA is 0x148030020: Register sp (31) contents are 0x148030010, offset is 16
  th1/fr1 m_cfa = 0x148030020 m_afa = 0xffffffffffffffff
  th1/fr1 initialized frame current pc is 0x100002fdb cfa is 0x148030020 afa is 0xffffffffffffffff
 th1/fr0 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
 th1/fr0 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
 th1/fr0 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030008]
  th1/fr1 no save location for fp (29) via 'eh_frame CFI'
 th1/fr0 supplying caller's saved fp (29)'s location, cached
  th1/fr1 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
  th1/fr1 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
  th1/fr1 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030018]
  th1/fr1 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
  th1/fr1 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
  th1/fr1 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030018]
   th1/fr2 pc = 0x1000640b4
  th1/fr1 no save location for fp (29) via 'eh_frame CFI'
 th1/fr0 supplying caller's saved fp (29)'s location, cached
   th1/fr2 fp = 0x148030000
  th1/fr1 supplying caller's saved sp (31)'s location using ABI default
  th1/fr1 supplying caller's register sp (31), value is CFA plus offset 0 [value is 0x148030020]
   th1/fr2 sp = 0x148030020
   th1/fr2 with pc value of 0x1000640b4, symbol name is 'caml_start_program'
   th1/fr2 Backing up the pc value of 0x1000640b4 by 1 and re-doing symbol lookup; old symbol was caml_start_program
   th1/fr2 Symbol is now caml_start_program
   th1/fr2 Using full unwind plan 'EmulateInstructionARM64'
   th1/fr2 active row: 0x0000000100064078: CFA=fp+160 => x8=[CFA-176] x19=[CFA-144] x20=[CFA-136] x21=[CFA-128] x22=[CFA-120] x23=[CFA-112] x24=[CFA-104] x25=[CFA-96] x26=[CFA-88] x27=[CFA-80] x28=[CFA-72] fp=[CFA-160] lr=[CFA-152] d6=[CFA-64] d7=[CFA-56] d8=[CFA-48] d9=[CFA-40] d10=[CFA-32] d11=[CFA-24] d12=[CFA-16] d13=[CFA-8] 
  th1/fr1 no save location for fp (29) via 'eh_frame CFI'
 th1/fr0 supplying caller's saved fp (29)'s location, cached
   th1/fr2 CFA is 0x1480300a0: Register fp (29) contents are 0x148030000, offset is 160
   th1/fr2 m_cfa = 0x1480300a0 m_afa = 0xffffffffffffffff
   th1/fr2 initialized frame current pc is 0x1000640b3 cfa is 0x1480300a0 afa is 0xffffffffffffffff
  th1/fr1 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
  th1/fr1 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
  th1/fr1 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x148030018]
   th1/fr2 supplying caller's saved fp (29)'s location using EmulateInstructionARM64 UnwindPlan
   th1/fr2 supplying caller's register fp (29) from the stack, saved at CFA plus offset -160 [saved at 0x148030000]
   th1/fr2 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
   th1/fr2 supplying caller's saved lr (30)'s location using EmulateInstructionARM64 UnwindPlan
   th1/fr2 supplying caller's register lr (30) from the stack, saved at CFA plus offset -152 [saved at 0x148030008]
   th1/fr2 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
   th1/fr2 supplying caller's saved lr (30)'s location using EmulateInstructionARM64 UnwindPlan
   th1/fr2 supplying caller's register lr (30) from the stack, saved at CFA plus offset -152 [saved at 0x148030008]
    th1/fr3 pc = 0x100002fdc
   th1/fr2 supplying caller's saved fp (29)'s location, cached
    th1/fr3 fp = 0x0
   th1/fr2 supplying caller's saved sp (31)'s location using ABI default
   th1/fr2 supplying caller's register sp (31), value is CFA plus offset 0 [value is 0x1480300a0]
    th1/fr3 sp = 0x1480300a0
    th1/fr3 with pc value of 0x100002fdc, symbol name is 'caml_program'
    th1/fr3 Backing up the pc value of 0x100002fdc by 1 and re-doing symbol lookup; old symbol was caml_program
    th1/fr3 Symbol is now caml_program
    th1/fr3 Using full unwind plan 'eh_frame CFI'
    th1/fr3 active row: 0x0000000100002fd4: CFA=sp+16 => lr=[CFA-8] 
   th1/fr2 supplying caller's saved sp (31)'s location, cached
    th1/fr3 CFA is 0x1480300b0: Register sp (31) contents are 0x1480300a0, offset is 16
    th1/fr3 m_cfa = 0x1480300b0 m_afa = 0xffffffffffffffff
    th1/fr3 initialized frame current pc is 0x100002fdb cfa is 0x1480300b0 afa is 0xffffffffffffffff
   th1/fr2 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
   th1/fr2 supplying caller's saved lr (30)'s location using EmulateInstructionARM64 UnwindPlan
   th1/fr2 supplying caller's register lr (30) from the stack, saved at CFA plus offset -152 [saved at 0x148030008]
    th1/fr3 no save location for fp (29) via 'eh_frame CFI'
   th1/fr2 supplying caller's saved fp (29)'s location, cached
    th1/fr3 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
    th1/fr3 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
    th1/fr3 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x1480300a8]
    th1/fr3 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
    th1/fr3 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
    th1/fr3 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x1480300a8]
     th1/fr4 pc = 0x0
    th1/fr3 no save location for fp (29) via 'eh_frame CFI'
   th1/fr2 supplying caller's saved fp (29)'s location, cached
     th1/fr4 fp = 0x0
    th1/fr3 supplying caller's saved sp (31)'s location using ABI default
    th1/fr3 supplying caller's register sp (31), value is CFA plus offset 0 [value is 0x1480300b0]
     th1/fr4 sp = 0x1480300b0
     th1/fr4 this frame has a pc of 0x0
     Frame 4 invalid RegisterContext for this frame, stopping stack walk
   th1/fr2 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
   th1/fr2 supplying caller's saved lr (30)'s location using EmulateInstructionARM64 UnwindPlan
   th1/fr2 supplying caller's register lr (30) from the stack, saved at CFA plus offset -152 [saved at 0x148030008]
  th1/fr1 no save location for fp (29) via 'eh_frame CFI'
 th1/fr0 supplying caller's saved fp (29)'s location, cached
   th1/fr2 CFA is 0x148030010: Register fp (29) contents are 0x148030000, offset is 16
   th1/fr2 supplying caller's saved pc (32)'s location using arm64-apple-darwin default unwind plan UnwindPlan
   th1/fr2 supplying caller's register pc (32) from the stack, saved at CFA plus offset -8 [saved at 0x148030008]
   th1/fr2 trying to unwind from this function with the UnwindPlan 'arm64-apple-darwin default unwind plan' because UnwindPlan 'EmulateInstructionARM64' failed.
   th1/fr2 supplying caller's saved fp (29)'s location using arm64-apple-darwin default unwind plan UnwindPlan
   th1/fr2 supplying caller's register fp (29) from the stack, saved at CFA plus offset -16 [saved at 0x148030000]
   th1/fr2 supplying caller's saved pc (32)'s location, cached
   th1/fr2 supplying caller's saved pc (32)'s location, cached
    th1/fr3 pc = 0x100002fdc
   th1/fr2 supplying caller's saved fp (29)'s location, cached
    th1/fr3 fp = 0x0
   th1/fr2 supplying caller's saved sp (31)'s location using arm64-apple-darwin default unwind plan UnwindPlan
   th1/fr2 did not supply reg location for sp (31) because it is volatile
    th1/fr3 with pc value of 0x100002fdc, symbol name is 'caml_program'
    th1/fr3 Backing up the pc value of 0x100002fdc by 1 and re-doing symbol lookup; old symbol was caml_program
    th1/fr3 Symbol is now caml_program
    th1/fr3 Using full unwind plan 'eh_frame CFI'
    th1/fr3 active row: 0x0000000100002fd4: CFA=sp+16 => lr=[CFA-8] 
   th1/fr2 supplying caller's saved sp (31)'s location using arm64-apple-darwin default unwind plan UnwindPlan
   th1/fr2 did not supply reg location for sp (31) because it is volatile
    th1/fr3 failed to get cfa
    Frame 3 invalid RegisterContext for this frame, stopping stack walk
    th1/fr3 no save location for fp (29) via 'eh_frame CFI'
   th1/fr2 supplying caller's saved fp (29)'s location, cached
    th1/fr3 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
    th1/fr3 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
    th1/fr3 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x1480300a8]
    th1/fr3 requested caller's saved PC but this UnwindPlan uses a RA reg; getting lr (30) instead
    th1/fr3 supplying caller's saved lr (30)'s location using eh_frame CFI UnwindPlan
    th1/fr3 supplying caller's register lr (30) from the stack, saved at CFA plus offset -8 [saved at 0x1480300a8]
     th1/fr4 pc = 0x0
    th1/fr3 no save location for fp (29) via 'eh_frame CFI'
   th1/fr2 supplying caller's saved fp (29)'s location, cached
     th1/fr4 fp = 0x0
    th1/fr3 supplying caller's saved sp (31)'s location, cached
     th1/fr4 sp = 0x1480300b0
     th1/fr4 this frame has a pc of 0x0
     Frame 4 invalid RegisterContext for this frame, stopping stack walk
 th1 Unwind of this thread is complete.
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
  * frame #0: 0x0000000100006f28 fib-test-2.exe`camlCamlinternalFormatBasics$entry + 32
    frame #1: 0x0000000100002fdc fib-test-2.exe`caml_program + 28
    frame #2: 0x00000001000640b4 fib-test-2.exe`caml_start_program + 132
    frame #3: 0x0000000100002fdc fib-test-2.exe`caml_program + 28

Tue 21 May 10:12:44 2024

There are two fundamental documents that are useful here:

  1. An Apple Library Primer https://forums.developer.apple.com/forums/thread/715385 This covers the terminology used by Apple developer tools, what tools are used to inspect MachO binaries and generally how linking works.

Apple platforms use DWARF. When you compile a file, the compiler puts the debug info into the resulting object file. When you link a set of object files into a executable, dynamic library, or bundle for distribution, the linker does not include this debug info. Rather, debug info is stored in a separate debug symbols document package. This has the extension .dSYM and is created using dsymutil. Use symbols to learn about the symbols in a file. Use dwarfdump to get detailed information about DWARF debug info. Use atos to map an address to its corresponding symbol name.

  1. Apple Lazy DWARF Scheme https://wiki.dwarfstd.org/Apple%27s_%22Lazy%22_DWARF_Scheme.md older document describing the use of STABS information to support DWARF.

How do we get OSO into object files?

 $ ./bin/ocamlopt -verbose -S -g -o fib.exe fib.ml
+ gcc -c -Wno-trigraphs  -o 'fib.o' 'fib.s'
+ gcc -c -Wno-trigraphs  -o '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartup736830.o' '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartup51ae7e.s'
+ gcc -O2 -fno-strict-aliasing -fwrapv -pthread  -pthread   -o 'fib.exe'  '-L/Users/tsmc/projects/ocaml/lib/ocaml'  '/var/folders/z_/7yzlrkjn6pd441zs1qhzpjv00000gn/T/camlstartup736830.o' '/Users/tsmc/projects/ocaml/lib/ocaml/std_exit.o' 'fib.o' '/Users/tsmc/projects/ocaml/lib/ocaml/stdlib.a' '/Users/tsmc/projects/ocaml/lib/ocaml/libasmrun.a'     -lpthread

(lldb) image dump symtab fib.exe Dumps what lldb knows about an executable including these entries for C code

[ 1273] 4701 D SourceFile 0x0000000000000000 Sibling -> [ 1284] 0x00640000 /Users/tsmc/projects/ocaml/runtime/dynlink_nat.c [ 1274] 4703 D ObjectFile 0x00000000664bfe6b 0x0000000000000000 0x00660001 /Users/tsmc/code/ocaml/ocaml/lib/ocaml/libasmrun.a(dynlink_nat.n.o)

We've tried using .file directive with DWARF 5 format as per simple compiled C programs which have .file 0 "/Users/tsmc/projects/ocaml" "prog.c" md5 0xaf28ba27a1bfcef40a3244a7c73faa94 to show the directory plus the file and a checksum. Uses DWARF 5 or .file 1 "/Users/tsmc/projects/ocaml" "prog.c" using DWARF4.

The C parts of the runtime have OSO entries which is good. The object file produced from fib.s to fib.o doesn't have OSO information included.

./bin/ocamlopt -dstartup -verbose -S -g -o fib.exe fib.ml will output the startup assembly file for OCaml. eg the part that includes caml_program

Debugging OCaml bytecode

Preliminaries

Debuggers

OCaml includes a compiler that produces bytecode and an interpreter for that bytecode. There are two supported options for debugging bytecode.

  1. ocamldebug - provided with the compiler distribution
  2. earlybird - VSCode integrated debugger using DAP.

Both options reuse the protocol from ocamldebug to interface with the bytecode executables.

DAP

The Debug Adapter Protocol (DAP) defines the abstract protocol used between a development tool (e.g. IDE or editor) and a debugger. The idea behind the Debug Adapter Protocol (DAP) is to abstract the way how the debugging support of development tools communicates with debuggers or runtimes into a protocol. By using DAP OCaml debugging can be integrated with many different IDEs or editors. See https://microsoft.github.io/debug-adapter-protocol/.

Emacs support for DAP using both Bytecode and Native debuggers.

What is missing?

  • ocamldebug support for DAP
  • No support for MinGW or MSVC Windows ports
  • DAP integration for Vim
  • Limited support for Domains in bytecode debug - single domain only
  • Improvements to earlybird

Debugging OCaml with Emacs

This post started as a March Hacking Days effort at Tarides. I have been working on improving the debugging situation for OCaml and wanted to see how easily I could setup debug support in Emacs using DAP. Debug Adapter Protocol (DAP) is a wire protocol for communicating between an editor/IDE and a debug server like LLDB, providing an abstraction over debugging, similar to how Language Server Protocol (LSP) provides language support.

OCaml comes with support for debugging native programs with GDB and LLDB, and bytecode code programs using ocamldebug and earlybird. In this post we will cover setting up and debugging both kinds of programs. I am using an M3 Mac so all examples will show ARM64 assembly and macOS specific paths. The same setup should work on Linux. I use prelude to configure my Emacs with my own customistations in .emacs/personal, adjust for your own personal Emacs setup.

Let's start with the following program:

(* fib.ml *)
let rec fib n =
  if n = 0 then 0
  else if n = 1 then 1
  else fib (n-1) + fib (n-2)

let main () =
  let r = fib 20 in
  Printf.printf "fib(20) = %d" r

let _ = main ()

And this dune configuration in the same directory.

; dune
(executable
 (name fib)
 (modules fib)
 (modes exe byte))
; dune-project
(lang dune 3.11)
(map_workspace_root false)

Create an empty opam switch in same directory and install dune:

$ opam switch create . 5.1.1 --no-install
$ opam install dune

Emacs configuration

Emacs has dap-mode that provides everything we need. Install it using M-x package-install and choose the dap-mode package. I have the following lines in my .emacs/personal/init.el:

; Require dap-mode plus the two extra files we need
(require 'dap-mode)
(require 'dap-codelldb)
(require 'dap-ocaml)

; Setup key bindings using use-package.
(use-package dap-mode
  :bind (("C-c M-n" . dap-next)
         ("C-c M-s" . dap-step-in)
         ("C-c M-a" . dap-step-out)
         ("C-c M-w" . dap-continue)))

Save and restart Emacs, then we can move onto setting up Bytecode debugging.

Bytecode debugging

The earlybird project provides DAP support for debugging OCaml bytecode. Earlybird uses the (undocumented) protocol of ocamldebug to communicate with a bytecode executable, inheriting the same functionality as ocamldebug. Start by installing the package:

opam install earlybird

then create a file in .vscode/launch.json with this configuration:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "OCaml earlybird (experimental)",
            "type": "ocaml.earlybird",
            "request": "launch",
            "program": "./_build/default/fib.bc",
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        },
}

Build the project with dune build to create the fib.bc bytecode file. Finally start a debugger with M-x dap-debug. It will prompt you to choose a session, we want OCaml earlybird (experimental) from the named configuration above. It will start earlybird and immediately stop it before executing any OCaml code.

To set breakpoints you need to open the OCaml source file in _build/default/fib.ml and click on the source lines you want to stop at. Here is what it looks like after a few recursions. Use the buttons to control the debugger or use the keybindings we added. Curiously they are not pre-defined but here I've tried to reuse mappings from ocamldebug.

Native debugging

OCaml can also produce native binaries that can be debugged using GDB or LLDB, depending on your platform. Here we will use LLDB on macOS.

Add another section to .vscode/launch.json for starting lldb.

        {
            "type": "lldb",
            "request": "launch",
            "name": "LLDB with ocamlopt",
            "program": "./fib.exe",
            "args": [],
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        },

Run M-x dap-codelldb-setup which will download the codelldb DAP program that we are using to communicate with LLDB. This gets installed into .extension/vscode/codelldb.

Now compile the fib program with ocamlopt -g -o fib.exe fib.ml and startup a debugger session with M-x dap-debug choose the LLDB with ocamlopt option. You should see something similar to:

Now DAP as setup with LLDB and macOS, is a little broken and is missing support for setting breakpoints on symbols. Lets add that :-)

The second option is debugging with Dune, this is slightly different for two reasons. First Dune places the executable into _build/default/fib.exe and second Dune produces slightly different sym bols. We need a new section in .vscode/launch.json for Dune

        {
            "type": "lldb",
            "request": "launch",
            "name": "LLDB with Dune",
            "program": "./_build/default/fib.exe",
            "args": [],
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        },

Remove the old fib.exe in the project directory and run dune build. Startup a new DAP session with M-x dap-debug and choose LLDB with Dune. You should see the same debugger session as before.

Conclusion

Debugging OCaml with DAP inside Emacs is possible. Use dap-mode with:

(require 'dap-mode)
(require 'dap-codelldb)
(require 'dap-ocaml)

(use-package dap-mode
  :bind (("C-c M-n" . dap-next)
         ("C-c M-s" . dap-step-in)
         ("C-c M-a" . dap-step-out)
         ("C-c M-w" . dap-continue)))

and a launch.json of

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "OCaml earlybird (experimental)",
            "type": "ocaml.earlybird",
            "request": "launch",
            "program": "./_build/default/fib.bc",
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        },
        {
            "type": "lldb",
            "request": "launch",
            "name": "LLDB with Dune",
            "program": "./_build/default/fib.exe",
            "args": [],
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        },
        {
            "type": "lldb",
            "request": "launch",
            "name": "LLDB with ocamlopt",
            "program": "./fib.exe",
            "args": [],
            "stopOnEntry": true,
            "cwd": "${workspaceFolder}"
        }
    ]
}

The same setup will work under VSCode with the CodeLLDB and OCaml Platform extensions installed. Happy Emacs debugging.

Inspecting Compiler Output

Reading -dcmm and -dlinear Output

Understanding the Generated Assembly

Identifying Missed Optimizations

Micro-benchmarking with Core_bench

Writing Good Benchmarks

Interpreting Results

Common Pitfalls

Macro-benchmarking Strategies

Continuous Benchmarking in CI

Detecting Performance Regressions

Statistical Comparison Across Runs

Part III: Optimizations

This section covers concrete optimization techniques for OCaml programs, from compiler-level flags and annotations to data structure choices, memory management patterns, and I/O performance.

Inlining

How OCaml Inlines Functions

The [@inline] Attribute

[@inline always] vs [@inline never]

Cross-Module Inlining Limitations

Inlining with Flambda

Inlining Reports and How to Read Them

Tuning Inlining Parameters

Common Inlining Problems and Solutions

caml_apply in Profiles

Functors and Recursive Modules

Unboxing and Specialization

Float Arrays and the Flat Representation

[@unboxed] Types

[@local] for Stack Allocation

float vs Float.t and Numeric Performance

Flambda Float Limitations and Workarounds

Tail Call Optimization

When TCO Applies

Writing Tail-Recursive Code

Continuation-Passing Style

Lists vs Arrays vs Sequences

Performance Characteristics

Array.map vs List.map Trade-offs

When to Use Each

Hash Tables and Maps

Hashtbl vs Map

Hash Function Quality

Pre-sizing and Growth Costs

Strings and Buffers

String Concatenation Anti-patterns

(^) vs String.concat vs Buffer

Bytes for Mutation

Records and Tuples

Memory Layout Differences

Unboxed Fields

Flat vs Boxed Representations

Custom Data Structures

When Stdlib Isn't Enough

Persistent vs Ephemeral Trade-offs

Cache-Friendly Data Layouts

Reducing Allocations

Identifying Allocation Hot Spots

Pre-allocating Buffers

Object Pooling Patterns

Avoiding Intermediate Data Structures

Boxing and Unboxing

Understanding When Boxing Occurs

Techniques to Avoid Boxing

The Cost of Polymorphism

GC-Friendly Programming

Short-Lived vs Long-Lived Allocations

Reducing Major Heap Pressure

Finalizers and Their Overhead

Sys.opaque_identity and Optimization Barriers

Memory Layout Optimization

Struct-of-Arrays vs Array-of-Structs

Cache Line Considerations

False Sharing in Parallel Code

Closures and Their Cost

When Closures Allocate

Local Functions and Performance

Lifting Closures to Top Level

Partial Application

Cost of Currying

Eta-Expansion for Performance

Direct vs Indirect Calls

Higher-Order Functions

The Overhead of Abstraction

Monomorphization Strategies

Staging and Metaprogramming

Exception Handling

Cost of Exceptions

Exceptions vs Option Types

[@cold] for Unlikely Paths

File I/O

Buffered vs Unbuffered I/O

In_channel/Out_channel Best Practices

Memory-Mapped Files

Network I/O

Blocking vs Non-blocking

Connection Pooling

Batching Requests

Serialization

Marshal Performance Characteristics

Binary Formats vs Text

Protobuf, MessagePack, etc.

Part IV: Parallelism and Concurrency (OCaml 5)

This section covers OCaml 5's multicore support, including domains for true parallelism, effects for concurrency, and the libraries and patterns for writing efficient parallel programs.

Domains: True Parallelism

What Is a Domain?

Creating and Managing Domains

Domain.recommended_domain_count()

The Multicore GC

How the GC Changes with Domains

GC Parameters for Multicore

Releasing Memory Back to the OS

Effects for Concurrency

Effects vs Traditional Concurrency

Performance Characteristics

Observing Multicore Behavior

Using USDT Probes for STW Synchronization

Identifying Domain Coordination Bottlenecks

Task Pools and Parallel Loops

parallel_for and Variants

Granularity Tuning

Async/Await Patterns

Channels and Communication

Common Pitfalls

Domain Count Antipatterns

Work Distribution Issues

Direct-Style I/O

Fiber Scheduling

Combining Eio and Domainslib

Performance Tuning

Atomic Operations

Lock-Free Data Structures

Avoiding Contention

Cache Coherency Costs

Performance Comparison: Single-threaded

Adapting Multi-process Code

Common Migration Issues

Part V: Interoperability

This section covers the performance implications of OCaml's interoperability with other languages, including C bindings via the FFI and JavaScript compilation with js_of_ocaml.

The Foreign Function Interface

Minimizing Crossing Overhead

Callback Costs

Memory Management Across Boundaries

ctypes vs Hand-Written Stubs

js_of_ocaml Performance

Optimization Flags

Dead Code Elimination

Part VI: Compile Times

This section covers techniques for reducing OCaml compilation times, an often-overlooked aspect of developer productivity and performance.

Understanding What's Slow

Dune's Internal Profiler

Identifying Bottlenecks

Module Structure for Fast Builds

Dependency Management

Interface Files (.mli)

Flambda Compile Time Trade-offs

Parallel Compilation

Incremental Builds and Caching

Part VII: Case Studies

This section contains real-world optimization examples and common performance patterns in OCaml.

Real-World Optimisation Examples

This chapter will contain detailed case studies of real-world OCaml performance optimisation work. Suggested topics include:

  • Optimising solver service - Investigating the scaling performance of the solving service used in OCaml CI infrastructure.

  • Static Analysis performance - Tuning the Infer static analysis service for better OCaml multicore performance. Covering perf CPU and off-CPU profiling, Garbage Collection behaviour and memory usage.

  • Web Server Performance - Tuning an OCaml web server (e.g., Dream, Cohttp). Connection handling, request parsing, response serialisation, and Eio integration for concurrent I/O.

  • JSON Parsing: A Microbenchmark Study - Comparing approaches to JSON parsing in OCaml (Yojson, Jsonm, custom parsers). Allocation profiles, buffer management, and streaming vs tree parsing.

Contributions and suggestions for case studies are welcome.

The Defunctionalization Pattern

Continuation-Passing for Tail Calls

Memoization Strategies

Lazy Evaluation Trade-offs

Data-Oriented Design in OCaml

Quick Reference: Compiler Flags

Complete flag reference with performance implications.

Quick Reference: Dune Configuration

Performance-oriented dune file examples.

Tool Installation Guide

Setting up the complete profiling toolchain.

Glossary

OCaml-Specific Terminology

Performance Terminology

Further Reading

Academic Papers on OCaml Implementation

  • Real World OCaml has a section The Compiler and Runtime System that covers the OCaml 4.14 runtime and garbage collector. Many details carry over into OCaml 5.*