Skip to content

Instantly share code, notes, and snippets.

@bjacob
Created July 3, 2026 19:29
Show Gist options
  • Select an option

  • Save bjacob/6f0cd9508f0d2ff4a0563b1ac7363315 to your computer and use it in GitHub Desktop.

Select an option

Save bjacob/6f0cd9508f0d2ff4a0563b1ac7363315 to your computer and use it in GitHub Desktop.
DBI plan

rocJITsu DBI / waitcheck exploratory digest

Date: 2026-07-03

Repo inspected: /home/benoit/workspace/TheRock/rocm-systems

Refs checked

  • Current local HEAD: 63c36973da
  • Jakub's waitcheck branch: origin/users/kuhar/waitcheck
    • Tip observed locally: c9ede5cae2 Add gfx942 and gfx1100 waitcheck support
    • Linked preload commit: d85089edea7fa47e9f1446733fa6bc2d38d01913
  • Allyson's landed/in-tree DBI commits:
    • b415a966cf [rocjitsu] Added First DBI Instance -- Adding an Inlined nop (#6742)
    • 324d41003f [rocjitsu] DBI: Multiple instrumentation points (#7213)
  • Allyson's not-yet-landed PR #7534:
    • URL: ROCm/rocm-systems#7534
    • Fetched locally as origin/pr/7534
    • Tip observed locally: 20140cc2ea [rocjitsu] Updated tests to use the new test_paths
    • Merge-base used for PR-specific inspection: 828873e0351027b3ae98afcea3f15c2d000d5bfd

Short version

DBI is not just an aspiration anymore. The current tree already has a real static code-object patching path that can insert inline s_nop trampolines at one or more selected instruction offsets. It has unit tests and an HSA smoke test proving patched code objects can execute.

What is missing is the "live tool" integration path: something that catches code objects while an application is loading them, modifies the bytes, and hands the modified reader back to ROCR/loader code. That is the part Jakub is pointing at with waitcheck's LD_PRELOAD shim.

Allyson's PR #7534 goes beyond the landed inline-nop slice. It adds a copied no-op probe body and emits an actual probe call from the trampoline with s_swappc_b64, plus probe validation, clobber analysis, and hardware tests. That PR does not solve the runtime interception problem either; it extends the patching machinery once you already have bytes to patch.

So the pieces line up like this:

  • Current DBI: "Given an ELF/code object and explicit offsets, can rocJITsu rewrite it?" Yes, for inline nop trampolines.
  • PR #7534: "Can rocJITsu call a copied probe from a trampoline?" Mostly yes, with strict constraints.
  • Waitcheck preload: "Can a rocJITsu tool interpose on HSA code-object reader creation in a real process?" Yes, for analysis only.
  • Next useful experiment: combine the waitcheck-style preload path with the current DBI Instrumentor to mutate one loaded code object in-process.

What Jakub is talking about

Jakub's "simple ld_preload thing similar to waitcheck" is about the missing runtime path. The waitcheck branch builds a separate shared object, librocjitsu_waitcheck.so, that is loaded with LD_PRELOAD. It exports wrappers for HSA code-object reader creation APIs, reads the code-object bytes, runs rocJITsu analysis, and then chains to the real HSA function.

For DBI, the analogous first target is not yet "full instrumentation". It is just:

  1. intercept a code object as it is being loaded,
  2. patch the bytes using the existing DBI machinery,
  3. create/pass a reader over the patched bytes,
  4. prove the running application saw the modified binary.

Jakub mentioned inserting a single nop or s_trap. In the current landed DBI path, inserting an inline s_nop is already the supported smoke test. An s_trap or s_endpgm style sabotage can be more visibly observable, but may need an instruction-builder helper or direct encoding and will have different runtime failure behavior.

His question about "whether we still need the same tool-specific ld_preload path" is the design fork:

  • Use a waitcheck-like LD_PRELOAD shim for DBI.
  • Or extend/reuse rocJITsu's existing HSA-tools hook path.

The current DBT hook path exists, but it is DBT-specific and loaded via HSA_TOOLS_LIB. Waitcheck shows Jakub needed a separate tool-specific preload shim to reliably see the reader creation paths, including AMD loader extension-table paths.

Existing DBI in current tree

Current DBI lives mainly under:

  • emulation/rocjitsu/docs/dbi-design.md
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/code_object_patcher.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/code_object_patcher.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/spill_manager.h
  • emulation/rocjitsu/tests/patch/instrumentor_test.cpp
  • emulation/rocjitsu/tests/patch/trampoline_builder_test.cpp
  • emulation/rocjitsu/tests/patch/spill_manager_test.cpp
  • emulation/rocjitsu/tests/dbi/hsa_dbi_smoke_test.cpp

The current shape:

  • Instrumentor accepts instrumentation points and emits a patched ELF/code object.
  • It supports BeforeInst points.
  • It supports multiple points.
  • It currently accepts only the simple inline-nop path:
    • filter_flags == 0
    • no probe_obj
    • empty probe_symbol
    • force_full_exec == false
  • TrampolineBuilder emits:
    • a branch from the original anchor to a code cave,
    • optional s_nop filler for an 8-byte anchor,
    • body words such as inline s_nop,
    • the relocated original instruction,
    • a return branch.
  • CodeObjectPatcher grows .text, updates section/program header sizes, adjusts symbols/relocations, and fixes kernel descriptor offsets.
  • SpillManager exists, but it is future-facing. The inline-nop path does not yet consume spill/fill scratch layout.

The current HSA DBI smoke test manually patches fixture bytes and then loads those patched bytes. That proves the patcher works, but not that rocJITsu can interpose in a normal app without the test explicitly calling the patcher.

Important limitation: docs/dbi-design.md in current HEAD is partly stale relative to the landed code and PR #7534. For example, it still reads like a first slice in places, while the current landed code already supports multiple instrumentation points.

Jakub's waitcheck branch

Files to inspect on origin/users/kuhar/waitcheck:

  • emulation/rocjitsu/docs/waitcheck/README.md
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/waitcheck_preload.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/CMakeLists.txt
  • emulation/rocjitsu/tools/waitcheck_main.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/analysis/waitcheck.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/analysis/waitcheck.h
  • emulation/rocjitsu/tests/tools/waitcheck_preload_smoke_test.cpp

Useful command form:

git show origin/users/kuhar/waitcheck:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/waitcheck_preload.cpp

What waitcheck does:

  • Provides an offline CLI, rj_waitcheck.
  • Provides a runtime shim, librocjitsu_waitcheck.so.
  • The shim is used with LD_PRELOAD.
  • It intercepts:
    • hsa_code_object_reader_create_from_memory
    • hsa_code_object_reader_create_from_file
    • hsa_ven_amd_loader_code_object_reader_create_from_file_with_offset_size
    • hsa_system_get_extension_table
    • hsa_system_get_major_extension_table
  • The extension-table wrappers patch returned AMD loader extension tables so calls through the extension table are also checked.
  • It uses dlsym(RTLD_NEXT, ...) to chain to the real HSA functions.
  • It has a recursion guard, g_in_waitcheck.
  • It can read full files and file ranges via pread.
  • It parses the code object with AmdGpuCodeObject, runs waitcnt hazard analysis, prints diagnostics, and optionally fails reader creation.

What it does not do:

  • It does not rewrite code objects.
  • It does not allocate alternate buffers for patched binaries.
  • It does not invoke DBI Instrumentor.

Why it matters for DBI:

  • It is a proven template for reaching the runtime code-object loading path.
  • Its coverage of memory/file/file-offset creation APIs is exactly the surface a DBI preload smoke would need.
  • Its extension-table handling is probably the non-obvious part worth copying rather than rediscovering.
  • Its smoke tests show how to test enabled/disabled/fail behavior and extension-table routing.

HSA_TOOLS_LIB / HSA tools ABI

When this digest says "HSA tools", it means ROCR's in-process tool ABI, not rocJITsu command-line tools. In this mode, ROCR loads a shared library named by HSA_TOOLS_LIB during hsa_init(). The library exports OnLoad and OnUnload; ROCR calls OnLoad with an HSA API table, and the tool can replace selected function pointers in that table.

rocJITsu's current implementation is:

The most direct local HSA/ROCR documentation and source anchors are:

The full ROCR hsa_api_trace.h is the source-of-truth table layout. The local hsa_api_trace_minimal.h comments are useful because they capture the subset rocJITsu relies on: ROCR passes tools a table whose layout is defined by hsa_api_trace.h; rocJITsu mirrors only the table header and core API entries through hsa_executable_load_agent_code_object; and the hook validates the table size before touching tail fields. That is why the current hook can avoid vendoring all extension table definitions while still patching the core reader/load APIs.

Current DBT hook flow:

  1. The test or app runs with HSA_TOOLS_LIB=/path/to/librocjitsu_hooks.so.
  2. During hsa_init(), ROCR reads HSA_TOOLS_LIB, loads librocjitsu_hooks.so, resolves the exported OnLoad symbol, and calls it with the HSA API table.
  3. OnLoad parses config such as RJ_DBT_TARGET_ISA, then installs wrappers.
  4. The hook saves original HSA API table entries.
  5. It replaces the core table entries for:
    • hsa_code_object_reader_create_from_file
    • hsa_code_object_reader_create_from_memory
    • hsa_code_object_reader_destroy
    • hsa_executable_load_agent_code_object
  6. hsa_code_object_reader_create_from_memory calls the original function, then records the reader handle and original ELF byte pointer in CodeObjectReaderRegistry.
  7. hsa_executable_load_agent_code_object looks up the reader bytes, detects the source ELF target, translates with BinaryTranslator, creates a new memory-backed reader over owned translated bytes, and calls the original load function with that replacement reader.
  8. OnUnload restores rocJITsu wrappers only if they still point at rocJITsu, which matters for tool chaining.

This is not the same mechanism as LD_PRELOAD:

  • LD_PRELOAD uses dynamic-linker symbol interposition. A shim exports symbols like hsa_code_object_reader_create_from_memory and chains to the next definition with dlsym(RTLD_NEXT, ...).
  • HSA_TOOLS_LIB uses ROCR's tool ABI. The tool does not need to win dynamic symbol lookup for every call site; ROCR explicitly hands it the API table and the tool patches function pointers.
  • Waitcheck's preload also wraps AMD extension-table discovery so it can catch the offset-size reader path: hsa_ven_amd_loader_code_object_reader_create_from_file_with_offset_size. The current rocJITsu DBT HSA-tools hook intentionally treats extension tables as opaque in its minimal API-table mirror, so it does not currently provide the same offset-size coverage.

Why this matters for DBI:

  • The HSA-tools path already has a useful replacement-reader pattern. It owns translated bytes in a vector, creates a memory-backed reader over those bytes, calls the original load function, then destroys/removes the temporary reader. DBI will need the same kind of ownership discipline for patched bytes.
  • The current hook performs mutation at load time: hsa_executable_load_agent_code_object sees an opaque reader handle, so rocJITsu has to remember the bytes captured earlier at reader creation.
  • For a first DBI prototype through this path, the DBT translation step could be swapped for DBI instrumentation: record memory-backed reader bytes, select a known kernel/anchor, run Instrumentor, create a patched memory reader, and call the original load function with that reader.
  • File-backed readers are a gap today. The DBT hook currently warns that file-backed readers are not translated because the later load callback only receives an opaque reader. A DBI version would need to either read and own the file bytes at create time, extend the registry model, or use the waitcheck preload coverage instead.
  • If both DBT and DBI live in librocjitsu_hooks.so, there needs to be an ordering policy: DBT then DBI, DBI then DBT, or mutually exclusive modes.

Concrete DBI sketch with HSA_TOOLS_LIB:

  1. Add an opt-in mode such as RJ_DBI=1 or a separate DBI hook library.
  2. Keep the existing memory-reader registry.
  3. In the load wrapper, parse the recorded ELF bytes as AmdGpuCodeObject.
  4. Filter to one target ISA/kernel/fixture so the first experiment is narrow.
  5. Run current DBI Instrumentor with one inline-nop instrumentation point.
  6. Create a replacement memory-backed reader over the patched byte vector.
  7. Call the saved original hsa_executable_load_agent_code_object.
  8. Add an observable sabotage mode, once useful, to prove the app loaded the patched object rather than the original object.

The practical choice between HSA-tools and waitcheck-style LD_PRELOAD is about coverage versus reuse. HSA-tools reuses rocJITsu's existing load-time replacement machinery. Waitcheck-style preload has broader reader-creation coverage today, especially around file and AMD loader extension-table paths.

Allyson's landed DBI commits

The current DBI originated in:

  • b415a966cf [rocjitsu] Added First DBI Instance -- Adding an Inlined nop (#6742)
  • 324d41003f [rocjitsu] DBI: Multiple instrumentation points (#7213)

The first commit introduced the core pieces:

  • TrampolinePlan
  • TrampolineBuilder
  • Instrumentor
  • code-object patching workflow
  • end-to-end gfx90a tests for inserting an inline nop

The second commit removed the "exactly one point" restriction and added tests for multiple instrumentation points.

This means the baseline "insert a single nop" target Jakub mentioned has effectively landed in static/test form. The part that still needs a first hack is doing it through an app-facing runtime hook rather than a test harness manually calling the patcher.

Allyson's PR #7534

PR #7534 extends DBI from inline nop insertion toward actual probe calls.

Files worth reading from origin/pr/7534:

  • emulation/rocjitsu/docs/dbi-design.md
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.cpp
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.h
  • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.cpp
  • emulation/rocjitsu/tests/dbi/hsa_dbi_nop_probe_test.cpp
  • emulation/rocjitsu/tests/dbi/rj_nop_probe.hip
  • emulation/rocjitsu/tests/dbi/vector_add_probe.hip
  • emulation/rocjitsu/tests/dbi/probe_fixture_test.cpp

Useful command form:

git show origin/pr/7534:emulation/rocjitsu/docs/dbi-design.md

Main ideas in PR #7534:

  • A DBI point can either be inline-nop or probe-call:
    • no probe_obj and empty probe_symbol: inline nop
    • both probe_obj and probe_symbol: copied probe call
    • only one set: fatal validation error
  • The no-op probe fixture is rj_nop_probe.
  • The supported calling convention is effectively:
    • no args
    • no return value
    • return through s_setpc_b64 s[30:31]
  • Probe bodies are copied into the code cave.
  • The trampoline calls the copied probe with s_swappc_b64.
  • Probe validation rejects unsafe probe bodies:
    • relocations
    • nested calls
    • scratch access
    • malformed instruction tiling
    • missing expected return instruction
    • wrong symbol properties
  • Probe clobber analysis is conservative and callee-focused.
  • The trampoline planner tries to find dead registers for call materialization.

Important constraints in PR #7534:

  • It relies on fixed link pair s[30:31] for rj_nop_probe.
  • The kernel must already allocate through s31.
  • It does not auto-grow SGPR allocation yet.
  • Non-empty spill sets still fail closed.
  • It does not pass arguments to probes.
  • It does not solve shadow-memory allocation or pointer passing.
  • It does not add a runtime LD_PRELOAD or HSA-tools integration path.

The vector_add_probe.hip fixture is notable because it deliberately forces SGPR allocation through s31 with an empty inline asm clobber:

asm volatile("" ::: "s30", "s31");

That fixture exists because a normal small vector-add kernel would allocate far fewer SGPRs, making s[30:31] outside the kernel's declared register allocation. In that case the probe call can fault or hang the GPU.

The PR's hardware test proves the call is real by sabotaging the copied probe body, for example overwriting the first probe instruction with s_endpgm and checking that the output changes. That is the same spirit as Jakub's "single nop or s_trap" suggestion, but applied after a real probe-call trampoline exists.

Interplay table

Piece What it proves What it lacks How it helps next
Current DBI inline nop rocJITsu can grow .text, build trampolines, patch anchors, and run patched code objects Runtime interception, automatic instruction selection, probes, shadow memory Use as the mutation engine for first live preload experiment
PR #7534 probe-call DBI rocJITsu can copy a validated no-op probe and call it from a trampoline under strict constraints Not landed, no runtime hook, no args, no shadow memory, no SGPR auto-growth, no spills Rebase/borrow once live patching works; useful tests and validation design
Waitcheck preload rocJITsu can interpose on HSA code-object reader creation in a normal process No rewriting, no patched buffer lifetime handling Template for a DBI preload shim
Current DBT HSA hook rocJITsu can use HSA tools hooks to replace code-object readers for DBT DBT-specific, may not cover all reader paths, not wired to DBI Possible alternative to LD_PRELOAD after the first smoke

Suggested first hacking path

  1. Reproduce the current DBI baseline.

    Build and run the existing DBI/patch tests, especially the inline nop smoke. This confirms the local environment and fixture paths before adding runtime interposition.

  2. Create a minimal DBI preload shim.

    Start from waitcheck's waitcheck_preload.cpp shape. Intercept the same reader creation functions, but instead of only analyzing the bytes, run the current Instrumentor on one known-safe anchor and hand HSA a reader over the patched bytes.

    Practical details to solve:

    • preserve the patched byte buffer until ROCR is done with the reader,
    • avoid recursive calls when creating a replacement reader,
    • handle memory, file, and file-with-offset inputs,
    • make failure behavior explicit with an env var,
    • log target, kernel, and selected anchor clearly,
    • do not patch random code objects by default.
  3. First observable mutation.

    The least risky starting point is the existing inline-nop path, because the builder already supports it. For a louder proof, add a controlled sabotage mode that emits an instruction with obvious behavior, such as s_endpgm or s_trap, but only for a known fixture/kernel.

  4. Add instruction discovery.

    Current DBI points are explicit offsets. The next block Jakub calls out is identifying all instructions of interest, such as ds_*.

    Likely implementation route:

    • parse kernels with the existing code object and decoder APIs,
    • iterate decoded instructions/basic blocks,
    • classify DS/LDS and other target instruction families from opcode/metadata,
    • filter to anchors the relocator can safely move,
    • emit InstrumentationPoints for each accepted offset.
  5. Pull in PR #7534 concepts after the runtime smoke works.

    The probe-call machinery is more complex than the preload smoke. It is probably easier to first prove "live inline nop patching works" and then rebase or port:

    • probe symbol resolution,
    • probe-callable validation,
    • probe clobber summaries,
    • s_swappc_b64 trampoline emission,
    • hardware tests that prove the copied probe body is actually called.
  6. Shadow memory is a separate design step.

    Nothing currently landed, and nothing in PR #7534, solves "allocate and pass in shadow mem." The existing SpillManager is about scratch layout/spill planning, not host-side shadow allocation or probe argument passing.

    The open question is how a probe will learn the shadow-memory base:

    • pass a pointer through kernargs,
    • patch kernel descriptors or metadata,
    • materialize a pointer in the trampoline,
    • use a global symbol,
    • reserve SGPRs,
    • or use another runtime-managed channel.

    This choice is coupled to SGPR pressure, calling convention, loader integration, and how intrusive the DBI tool is allowed to be.

Open questions and risks

  • LD_PRELOAD versus HSA tools: waitcheck proves preload coverage, but rocJITsu already has an HSA-tools DBT hook. It is worth deciding whether the first DBI runtime prototype should deliberately copy waitcheck or instead adapt the DBT hook.
  • Reader lifetime: waitcheck can pass original bytes onward. DBI will need to own patched bytes long enough for the created reader/load path.
  • Extension-table paths: waitcheck's wrappers for AMD loader extension tables are probably essential for full coverage.
  • SGPR allocation: PR #7534's fixed s[30:31] convention only works when the kernel already declares enough SGPRs.
  • Spilling: non-empty spill sets still fail closed in PR #7534.
  • Probe ABI: PR #7534 supports a no-arg/no-return probe. Shadow memory will require a richer ABI.
  • Instruction safety: not every target instruction is relocatable. Branches, PC-relative forms, and certain control-flow or special-register cases need filtering.
  • Branch range/layout: current trampolines use local code caves and branches. Branch reach and cave placement remain part of the patching contract.
  • Test visibility: inline nop proves safe mutation only indirectly. A sabotage mode is useful for proving the loaded binary changed.
  • Branch drift: PR #7534 is not landed and has diverged from current HEAD, so inspect it relative to its merge-base rather than treating HEAD..origin/pr/7534 as a clean PR diff.

Recommended reading order

  1. Current landed DBI design and smoke:

    • emulation/rocjitsu/docs/dbi-design.md
    • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.cpp
    • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.cpp
    • emulation/rocjitsu/tests/dbi/hsa_dbi_smoke_test.cpp
  2. Jakub's runtime interposition example:

    • git show origin/users/kuhar/waitcheck:emulation/rocjitsu/docs/waitcheck/README.md
    • git show origin/users/kuhar/waitcheck:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/waitcheck_preload.cpp
    • git show origin/users/kuhar/waitcheck:emulation/rocjitsu/tests/tools/waitcheck_preload_smoke_test.cpp
  3. Allyson's probe-call PR:

    • git show origin/pr/7534:emulation/rocjitsu/docs/dbi-design.md
    • git show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.h
    • git show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.h
    • git show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.h
    • git show origin/pr/7534:emulation/rocjitsu/tests/dbi/hsa_dbi_nop_probe_test.cpp
    • git show origin/pr/7534:emulation/rocjitsu/tests/dbi/rj_nop_probe.hip
    • git show origin/pr/7534:emulation/rocjitsu/tests/dbi/vector_add_probe.hip
  4. Existing alternative runtime hook:

    • emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/hooks/rj_hsa_dbt_hooks.cpp

Concrete takeaway

The most productive next DBI slice is probably not designing shadow memory yet. It is a small runtime interposer that reuses waitcheck's preload surface and current DBI's inline-nop Instrumentor path. Once that proves "the loaded binary was modified in a real app," PR #7534's probe-call machinery becomes the next layer to rebase or port, followed by automatic ds_* site selection and then the larger shadow-memory ABI problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment