Date: 2026-07-03
Repo inspected: /home/benoit/workspace/TheRock/rocm-systems
- 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
- Tip observed locally:
- 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
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
Instrumentorto mutate one loaded code object in-process.
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:
- intercept a code object as it is being loaded,
- patch the bytes using the existing DBI machinery,
- create/pass a reader over the patched bytes,
- 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_PRELOADshim 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.
Current DBI lives mainly under:
emulation/rocjitsu/docs/dbi-design.mdemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/code_object_patcher.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/code_object_patcher.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/spill_manager.hemulation/rocjitsu/tests/patch/instrumentor_test.cppemulation/rocjitsu/tests/patch/trampoline_builder_test.cppemulation/rocjitsu/tests/patch/spill_manager_test.cppemulation/rocjitsu/tests/dbi/hsa_dbi_smoke_test.cpp
The current shape:
Instrumentoraccepts instrumentation points and emits a patched ELF/code object.- It supports
BeforeInstpoints. - 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
TrampolineBuilderemits:- a branch from the original anchor to a code cave,
- optional
s_nopfiller for an 8-byte anchor, - body words such as inline
s_nop, - the relocated original instruction,
- a return branch.
CodeObjectPatchergrows.text, updates section/program header sizes, adjusts symbols/relocations, and fixes kernel descriptor offsets.SpillManagerexists, 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.
Files to inspect on origin/users/kuhar/waitcheck:
emulation/rocjitsu/docs/waitcheck/README.mdemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/waitcheck_preload.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/CMakeLists.txtemulation/rocjitsu/tools/waitcheck_main.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/analysis/waitcheck.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/analysis/waitcheck.hemulation/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.cppWhat 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_memoryhsa_code_object_reader_create_from_filehsa_ven_amd_loader_code_object_reader_create_from_file_with_offset_sizehsa_system_get_extension_tablehsa_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.
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:
- ROCR hsa_api_trace.h API-table ABI, documenting that API tables start with
ApiTableVersionand that this is the root of the table-passing ABI. - ROCR CoreApiTable code-object entries, showing the exact function-pointer slots for reader creation/destruction and agent code-object load.
- ROCR HsaApiTable layout, showing the root table with core, AMD extension, finalizer, image, tools, and PC-sampling tables.
- ROCR reads HSA_TOOLS_LIB, where the runtime reads the tool-library environment variable.
- ROCR resolves OnLoad, where the runtime looks up
OnLoadin each loaded tool library and calls it with the HSA API table. - hsa_api_trace_minimal.h, rocJITsu's minimal mirror of ROCR's
hsa_api_trace.h. - hsa.h code-object reader docs, documenting that HSA code-object readers load code objects from file or memory.
- hsa.h memory-reader lifetime docs, documenting that the application-owned memory buffer must outlive the associated reader.
- hsa.h load-agent-code-object docs, documenting the
hsa_executable_load_agent_code_objectload call that the rocJITsu hook wraps.
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:
- The test or app runs with
HSA_TOOLS_LIB=/path/to/librocjitsu_hooks.so. - During
hsa_init(), ROCR readsHSA_TOOLS_LIB, loadslibrocjitsu_hooks.so, resolves the exportedOnLoadsymbol, and calls it with the HSA API table. OnLoadparses config such asRJ_DBT_TARGET_ISA, then installs wrappers.- The hook saves original HSA API table entries.
- It replaces the core table entries for:
hsa_code_object_reader_create_from_filehsa_code_object_reader_create_from_memoryhsa_code_object_reader_destroyhsa_executable_load_agent_code_object
hsa_code_object_reader_create_from_memorycalls the original function, then records the reader handle and original ELF byte pointer inCodeObjectReaderRegistry.hsa_executable_load_agent_code_objectlooks up the reader bytes, detects the source ELF target, translates withBinaryTranslator, creates a new memory-backed reader over owned translated bytes, and calls the original load function with that replacement reader.OnUnloadrestores rocJITsu wrappers only if they still point at rocJITsu, which matters for tool chaining.
This is not the same mechanism as LD_PRELOAD:
LD_PRELOADuses dynamic-linker symbol interposition. A shim exports symbols likehsa_code_object_reader_create_from_memoryand chains to the next definition withdlsym(RTLD_NEXT, ...).HSA_TOOLS_LIBuses 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_objectsees 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:
- Add an opt-in mode such as
RJ_DBI=1or a separate DBI hook library. - Keep the existing memory-reader registry.
- In the load wrapper, parse the recorded ELF bytes as
AmdGpuCodeObject. - Filter to one target ISA/kernel/fixture so the first experiment is narrow.
- Run current DBI
Instrumentorwith one inline-nop instrumentation point. - Create a replacement memory-backed reader over the patched byte vector.
- Call the saved original
hsa_executable_load_agent_code_object. - 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.
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:
TrampolinePlanTrampolineBuilderInstrumentor- 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.
PR #7534 extends DBI from inline nop insertion toward actual probe calls.
Files worth reading from origin/pr/7534:
emulation/rocjitsu/docs/dbi-design.mdemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.hemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.cppemulation/rocjitsu/tests/dbi/hsa_dbi_nop_probe_test.cppemulation/rocjitsu/tests/dbi/rj_nop_probe.hipemulation/rocjitsu/tests/dbi/vector_add_probe.hipemulation/rocjitsu/tests/dbi/probe_fixture_test.cpp
Useful command form:
git show origin/pr/7534:emulation/rocjitsu/docs/dbi-design.mdMain ideas in PR #7534:
- A DBI point can either be inline-nop or probe-call:
- no
probe_objand emptyprobe_symbol: inline nop - both
probe_objandprobe_symbol: copied probe call - only one set: fatal validation error
- no
- 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]forrj_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_PRELOADor 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.
| 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 |
-
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.
-
Create a minimal DBI preload shim.
Start from waitcheck's
waitcheck_preload.cppshape. Intercept the same reader creation functions, but instead of only analyzing the bytes, run the currentInstrumentoron 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.
-
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_endpgmors_trap, but only for a known fixture/kernel. -
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.
-
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_b64trampoline emission,- hardware tests that prove the copied probe body is actually called.
-
Shadow memory is a separate design step.
Nothing currently landed, and nothing in PR #7534, solves "allocate and pass in shadow mem." The existing
SpillManageris 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.
- 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 treatingHEAD..origin/pr/7534as a clean PR diff.
-
Current landed DBI design and smoke:
emulation/rocjitsu/docs/dbi-design.mdemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/instrumentor.cppemulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/trampoline_builder.cppemulation/rocjitsu/tests/dbi/hsa_dbi_smoke_test.cpp
-
Jakub's runtime interposition example:
git show origin/users/kuhar/waitcheck:emulation/rocjitsu/docs/waitcheck/README.mdgit show origin/users/kuhar/waitcheck:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/waitcheck_preload.cppgit show origin/users/kuhar/waitcheck:emulation/rocjitsu/tests/tools/waitcheck_preload_smoke_test.cpp
-
Allyson's probe-call PR:
git show origin/pr/7534:emulation/rocjitsu/docs/dbi-design.mdgit show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_callable.hgit show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_clobber.hgit show origin/pr/7534:emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/code/patch/probe_symbol.hgit show origin/pr/7534:emulation/rocjitsu/tests/dbi/hsa_dbi_nop_probe_test.cppgit show origin/pr/7534:emulation/rocjitsu/tests/dbi/rj_nop_probe.hipgit show origin/pr/7534:emulation/rocjitsu/tests/dbi/vector_add_probe.hip
-
Existing alternative runtime hook:
emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/hooks/rj_hsa_dbt_hooks.cpp
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.