:local: true
The Instrumentor is a highly configurable instrumentation pass for LLVM-IR that allows users to insert custom runtime function calls at various program points. Unlike traditional instrumentation tools that are hardcoded for specific purposes (like sanitizers or profilers), the Instrumentor provides a flexible, configuration-driven approach where users can specify:
The Instrumentor is designed to support a wide variety of use cases including:
To use the Instrumentor it is recommended to run the wizard script located at ./llvm/utils/instrumentor-config-wizard.py. The script will interactively create a configuration file and a stub runtime which is required to be linked into the instrumented program.
The Instrumentor supports instrumentation at multiple levels:
Instruction-level: : - Load instructions: Instrument memory reads with access to pointer, loaded value, alignment, size, atomicity, etc.
Function-level: : - Function entry: Instrument at function start with access to function name, address, arguments, etc.
Future extensions: : - Basic block entry/exit
Each instrumentation opportunity supports two positions:
PRE: Insert instrumentation before the operation occurs
POST: Insert instrumentation after the operation occurs
For each instrumentation opportunity, users can individually enable/disable specific arguments to control:
For example, for load instrumentation, you can choose to pass:
The Instrumentor supports replacing values returned from the runtime:
This enables use cases like:
The Instrumentor provides fine-grained control over what gets instrumented:
x86_64-.*-linux)The Instrumentor supports fine-grained filtering of individual instrumentation opportunities based on their static properties. This allows you to instrument only specific operations that meet certain criteria, such as:
Property filters are specified using the filter field in the configuration JSON for each instrumentation opportunity.
The filter expression language supports:
Integer comparisons: : - == (equal)
!= (not equal)< (less than)> (greater than)<= (less than or equal)>= (greater than or equal)String comparisons: : - == (equal, with quoted string)
!= (not equal, with quoted string).startswith("prefix") (prefix check, with quoted string)Pointer comparisons: : - ==null (null pointer check)
!=null (non-null pointer check)Logical operators: : - && (logical AND)
|| (logical OR)Important notes:
Filter only atomic loads:
{ "instruction_post": { "load": { "enabled": true, "filter": "atomicity_ordering>0", "pointer": true, "value": true } } }
Filter volatile stores or acquire and release operations:
{ "instruction_post": { "store": { "enabled": true, "filter": "is_volatile==1 || atomicity_ordering==6", "pointer": true, "value": true } } }
Filter functions by name prefix:
{ "function_pre": { "function": { "enabled": true, "filter": "name.startswith(\"test_\")", "name": true } } }
Complex filter with multiple conditions:
{ "instruction_post": { "load": { "enabled": true, "filter": "(atomicity_ordering==4 || atomicity_ordering==7) && sync_scope_id==0", "pointer": true, "value": true, "atomicity_ordering": true } } }
The properties available for filtering depend on the instrumentation opportunity type but generally include all values that can be passed to the runtime, filtered using their respective name.
Load/Store instructions: : - atomicity_ordering (integer): 0=non-atomic, 1=Unordered, 2=Monotonic, 4=Acquire, 5=Release, 6=AcquireRelease, 7=SequentiallyConsistent
sync_scope_id (integer): synchronization scope identifieris_volatile (integer): 1 if volatile, 0 otherwisealignment (integer): alignment in bytesvalue_size (integer): size of loaded value in bytesFunction instrumentation: : - name (string): function name
num_arguments (integer): number of function argumentsis_main (integer): 1 if this is the main function, 0 otherwiseAlloca instructions: : - size (integer): allocation size in bytes (if constant)
alignment (integer): allocation alignment in bytesThe Instrumentor uses a JSON-based configuration system that allows users to:
The configuration file is a JSON document with the following structure:
{ "configuration": { "runtime_prefix": "__instrumentor_", "target_regex": "", "host_enabled": true, "gpu_enabled": true }, "function_pre": { "function": { "enabled": true, "address": true, "name": true, "id": true } }, "instruction_pre": { "load": { "enabled": true, "pointer": true, "pointer.replace": false, "value_size": true, "id": true }, "store": { "enabled": true, "pointer": true, "value": true, "value_size": true } }, "instruction_post": { "load": { "enabled": true, "value": true, "value.replace": false } } }
configuration
: Global settings that apply to all instrumentation:
runtime_prefix: Prefix for all runtime function names (default: __instrumentor_)target_regex: Regular expression to filter targets (empty = all targets)host_enabled: Enable instrumentation for CPU targets (default: true)gpu_enabled: Enable instrumentation for GPU targets (default: true)function_pre / function_post
: Function-level instrumentation configuration.
instruction_pre / instruction_post
: Instruction-level instrumentation configuration, with subsections for each instruction type (load, store, alloca, etc.).
For each instrumentation opportunity, arguments are configured with:
<argument_name>: Boolean to enable/disable passing this argument<argument_name>.replace: Boolean to enable value replacement (only for replaceable arguments)<argument_name>.description: Human-readable description of the argumentThe Instrumentor includes an interactive configuration wizard that simplifies the process of creating and modifying configurations.
# Run the wizard interactively ./llvm/utils/instrumentor-config-wizard.py # Specify output location ./llvm/utils/instrumentor-config-wizard.py -o my_config.json # Use specific opt binary ./llvm/utils/instrumentor-config-wizard.py --opt-path /path/to/opt # Load and modify existing configuration ./llvm/utils/instrumentor-config-wizard.py --input existing.json -o modified.json
The wizard guides you through five steps:
Step 1: Select Instrumentation Types
: Choose which types of operations to instrument (load, store, alloca, function, etc.). This is a high-level selection - you can configure individual arguments later.
Step 2: PRE vs POST Configuration
: Decide whether PRE and POST instrumentation should use the same configuration or different configurations. This saves time when you want both positions to have identical settings.
Step 3: Base Configuration
: Configure global settings:
Step 4: Configure Arguments
: For each enabled instrumentation type, select which arguments to pass to the runtime function. You can:
Step 5: Review and Save
: Review your configuration and optionally generate runtime stub implementations. The wizard displays a summary and provides commands for using the configuration with opt and clang.
The wizard can automatically generate C stub implementations of your runtime functions:
<config_name>_stubs.c)The generated stubs are useful as:
Example stub output:
void __instrumentor_pre_load(void *pointer, int32_t pointer_as, uint64_t value_size, int32_t id) { printf("load pre -- pointer: %p, pointer_as: %i, " "value_size: %lu, id: %i\n", pointer, pointer_as, value_size, id); }
Step 1: (Optional) Generate a default configuration
opt -passes=instrumentor \ -instrumentor-write-config-file=config.json \ -disable-output \ input.ll
This creates config.json with all available instrumentation opportunities and their arguments.
Step 2: Customize the configuration
Edit config.json manually or use the wizard (no input needed):
./llvm/utils/instrumentor-config-wizard.py --input config.json -o custom.json
Step 3: Apply instrumentation
opt -passes=instrumentor \ -instrumentor-read-config-file=custom.json \ input.ll -S -o instrumented.ll
The instrumented output contains calls to your runtime functions at the configured program points.
To instrument during compilation:
clang -mllvm -enable-instrumentor \ -mllvm -instrumentor-read-config-file=config.json \ source.c -o program
Here's a complete example for creating a simple memory access profiler:
1. Create configuration with the wizard:
./llvm/utils/instrumentor-config-wizard.py -o memory_profiler.json # In the wizard: # - Enable: load, store # - Use same config for PRE/POST: yes # - Base config: keep defaults # - For load/store: enable pointer, value_size, id # - Generate stubs: yes (memory_profiler_stubs.c)
2. Implement the runtime:
// memory_runtime.c #include <stdio.h> #include <stdint.h> static uint64_t load_count = 0; static uint64_t store_count = 0; void __instrumentor_pre_load(void *pointer, uint64_t value_size, int32_t id) { load_count++; printf("Load from %p (size: %lu, id: %d)\n", pointer, value_size, id); } void __instrumentor_pre_store(void *pointer, uint64_t value_size, int32_t id) { store_count++; printf("Store to %p (size: %lu, id: %d)\n", pointer, value_size, id); } __attribute__((destructor)) void print_stats(void) { printf("Total loads: %lu\n", load_count); printf("Total stores: %lu\n", store_count); }
3. Instrument and compile:
# Instrument the program clang -emit-llvm -S -o program.ll program.c opt -passes=instrumentor \ -instrumentor-read-config-file=memory_profiler.json \ program.ll -S -o program_inst.ll # Compile with runtime clang program_inst.ll memory_runtime.c -o program
4. Run and observe:
./program # Output includes: # Load from 0x7ffc12345678 (size: 4, id: 1) # Store to 0x7ffc12345680 (size: 8, id: 2) # ... # Total loads: 42 # Total stores: 27
Configure alloca instrumentation to track stack allocations:
{ "instruction_pre": { "alloca": { "enabled": true, "size": true, "alignment": true, "id": true } }, "instruction_post": { "alloca": { "enabled": true, "address": true, "size": true } } }
Runtime implementation:
static uint64_t total_stack_usage = 0; static uint64_t peak_stack_usage = 0; static uint64_t current_stack_usage = 0; void __instrumentor_post_alloca(void *address, uint64_t size, int32_t id) { current_stack_usage += size; total_stack_usage += size; if (current_stack_usage > peak_stack_usage) { peak_stack_usage = current_stack_usage; } }
Use value replacement to inject faults:
{ "instruction_post": { "load": { "enabled": true, "value": true, "value.replace": true, "pointer": true } } }
Runtime implementation:
// Replace every 1000th loaded value with zero static uint64_t load_counter = 0; uint64_t __instrumentor_post_load(uint64_t value, void *pointer) { if (++load_counter % 1000 == 0) { printf("Injecting fault at %p\n", pointer); return 0; // Return fault value } return value; // Return original value }
Instrument function entry and exit:
{ "function_pre": { "function": { "enabled": true, "name": true, "address": true, "num_arguments": true } }, "function_post": { "function": { "enabled": true, "name": true } } }
Runtime implementation:
static int call_depth = 0; void __instrumentor_pre_function(char *name, void *address, int32_t num_args, int32_t id) { printf("%*sEntering %s (%p) with %d args\n", call_depth * 2, "", name, address, num_args); call_depth++; } void __instrumentor_post_function(char *name, int32_t id) { call_depth--; printf("%*sExiting %s\n", call_depth * 2, "", name); }
The Instrumentor supports GPU targets (AMDGPU and NVPTX). Configure GPU-specific instrumentation:
{ "configuration": { "runtime_prefix": "__gpu_runtime_", "target_regex": "(amdgcn|nvptx).*", "host_enabled": false, "gpu_enabled": true }, "instruction_pre": { "load": { "enabled": true, "pointer": true, "pointer_as": true } } }
Note that GPU runtime functions must be implemented with appropriate device attributes.
The Instrumentor generates runtime function names following this pattern:
<runtime_prefix><position>_<opportunity_name>[_ind]
Where:
<runtime_prefix>: Configurable prefix (default: __instrumentor_)<position>: Either pre or post<opportunity_name>: Name of the instrumentation opportunity (load, store, function, etc.)_ind: Optional suffix when indirection is used (see below)Examples:
__instrumentor_pre_load__instrumentor_post_store__instrumentor_pre_function__instrumentor_pre_load_ind (with indirection)The Instrumentor uses two modes for passing arguments:
Direct mode (default):
: Arguments are passed by value. This is efficient but requires that all arguments fit in registers or can be passed through the stack efficiently.
Indirect mode:
: Arguments are passed by pointer. This is used automatically when:
When indirect mode is used, a separate function with the _ind suffix is generated:
// Direct mode void __instrumentor_pre_load(void *pointer, uint64_t value_size); // Indirect mode (automatically generated when needed) void __instrumentor_pre_load_ind(void **pointer, uint32_t pointer_size, void *value_size, uint32_t value_size_size);
Users typically don't need to worry about this - the Instrumentor handles it automatically and the wizard-generated stubs show the correct signatures.
When the id argument is enabled, the Instrumentor assigns a unique 32-bit integer to each instrumentation call site:
The Instrumentor caches certain argument values between PRE and POST calls when possible:
Runtime functions must be:
Runtime functions must not:
Instrumentation overhead depends on:
Minimize arguments:
: Only enable arguments you actually need. Passing fewer arguments reduces overhead.
Use PRE or POST, not both:
: If you only need one position, disable the other.
Target filtering:
: Use target_regex to instrument only specific targets or modules.
Efficient runtime:
: Keep runtime functions simple and fast. Consider:
Build with optimizations:
: Use -O2 or -O3 when compiling instrumented code. LLVM can optimize away some overhead.
“Could not find ‘opt’ binary”
: The wizard can't locate the opt binary.
--opt-path /path/to/opt“Indirection needed but not indicated”
: An argument value is too large for direct passing. The Instrumentor handles this automatically, but you might see this warning. It's usually harmless - the indirect version of the function will be generated.
Infinite recursion / stack overflow
: Your runtime function is calling back into instrumented code. Solutions:
Linking errors
: Runtime functions are undefined. You must:
Unexpected instrumentation
: More instrumentation than expected. Check:
enabled flag for each opportunityhost_enabled / gpu_enabled settingstarget_regex matches your targetfilter field) are correctly specifiedLess instrumentation than expected
: Property filters may be excluding instrumentation points:
filter field is set for the instrumentation opportunity"filter": "") or remove the field to disable filteringFilter syntax errors
: Invalid filter expressions will be reported as errors:
"name==\"foo\"" not "name==foo"&& for AND, || for ORView instrumented IR:
opt -passes=instrumentor \ -instrumentor-read-config-file=config.json \ input.ll -S -o output.ll # Examine output.ll to see inserted calls
Print configuration:
opt -passes=instrumentor \ -instrumentor-write-config-file=debug_config.json \ input.ll -disable-output # Examine debug_config.json to see all options
Verify IR:
: The Instrumentor automatically verifies the module after instrumentation. If verification fails, there's a bug in the Instrumentor or the configuration is invalid.
Use debug builds:
: Build LLVM with assertions enabled (-DLLVM_ENABLE_ASSERTIONS=ON) to catch issues early.
The Instrumentor is designed to be extensible. To add new instrumentation opportunities:
InstrumentationOpportunityInstrumentationConfig::populate()llvm/test/Transforms/Instrumentor/See llvm/lib/Transforms/IPO/Instrumentor.cpp and llvm/include/llvm/Transforms/IPO/Instrumentor.h for examples (LoadIO, StoreIO).
Future instrumentation opportunities being considered:
-instrumentor-read-config-file=<path> : Load instrumentation configuration from the specified JSON file.
-instrumentor-write-config-file=<path> : Write the default instrumentation configuration to the specified JSON file (useful for generating templates).
The Instrumentor is more flexible but related to:
The Instrumentor can implement similar functionality with custom runtime code, but specialized passes may have better performance for their specific use cases.
llvm/lib/Transforms/IPO/Instrumentor.cppllvm/include/llvm/Transforms/IPO/Instrumentor.hllvm/utils/instrumentor-config-wizard.py