# The LLVM Instrumentor Pass ```{contents} :local: true ``` ## Introduction 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: - **What** to instrument (loads, stores, allocations, function calls, etc.) - **Where** to instrument (before or after operations) - **What information** to pass to the runtime (pointers, values, sizes, types, etc.) - **Whether** to modify program behavior (replace values, redirect pointers, etc.) The Instrumentor is designed to support a wide variety of use cases including: - Custom memory profilers and trackers - Performance analysis tools - Dynamic program analysis - Debugging and tracing utilities - Stack usage monitoring - Custom sanitizers and checkers 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. ## Key Features ### Configurable Instrumentation Opportunities The Instrumentor supports instrumentation at multiple levels: **Instruction-level:** : - **Load instructions**: Instrument memory reads with access to pointer, loaded value, alignment, size, atomicity, etc. - **Store instructions**: Instrument memory writes with access to pointer, stored value, alignment, size, atomicity, etc. - **Alloca instructions**: Instrument stack allocations with access to size, alignment, and allocated address **Function-level:** : - **Function entry**: Instrument at function start with access to function name, address, arguments, etc. - **Function exit**: Instrument at function return **Future extensions:** : - Basic block entry/exit - Module-level initialization - Global variable access ### PRE and POST Instrumentation Each instrumentation opportunity supports two positions: - **PRE**: Insert instrumentation **before** the operation occurs - For loads: can inspect/modify the pointer before reading - For stores: can inspect/modify the pointer and value before writing - For allocas: can modify the allocation size - For functions: instrument at function entry, inspect/replace arguments - **POST**: Insert instrumentation **after** the operation occurs - For loads: can inspect/modify the loaded value - For stores: instrument after the write completes - For allocas: can inspect/modify the allocated address - For functions: instrument at function exit ### Selective Argument Passing For each instrumentation opportunity, users can individually enable/disable specific arguments to control: - What information is passed to the runtime function - The signature of the generated runtime function - Performance overhead (fewer arguments = faster calls) For example, for load instrumentation, you can choose to pass: - Pointer address - Pointer address space - Loaded value - Value size - Alignment - Value type ID - Atomicity ordering - Synchronization scope - Volatility flag - Unique instrumentation ID ### Value Replacement The Instrumentor supports **replacing** values returned from the runtime: - **Load replacement**: The runtime can provide a different value than what was loaded from memory - **Store replacement**: The runtime can modify the pointer or value being stored - **Alloca replacement**: The runtime can provide a different allocation size or replace the allocated address - **Argument replacement**: The runtime can modify the arguments passed to a function This enables use cases like: - Value redirection for debugging - Custom memory allocators - Fault injection - Taint tracking ### Instrumentation Filtering The Instrumentor provides fine-grained control over what gets instrumented: - **Target regex**: Match against the target triple (e.g., `x86_64-.*-linux`) - **Host/GPU toggle**: Separately enable/disable CPU and GPU instrumentation - **Function filtering**: Exclude runtime functions from instrumentation via a regular expression - **Property filters**: Filter individual instrumentation points based on static properties (see Property Filtering below) ### Property Filtering 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: - Only atomic loads with specific orderings - Only volatile memory accesses - Functions with specific name patterns - Allocations above a certain size threshold Property filters are specified using the `filter` field in the configuration JSON for each instrumentation opportunity. #### Filter Syntax 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:** - If a property value is dynamic (not a compile-time constant), the filter is assumed to pass - Empty filters always pass - String literals must be enclosed in double quotes (with proper escaping) - Property names are specific to each instrumentation opportunity (see Available Properties below) #### Filter Examples **Filter only atomic loads:** ```json { "instruction_post": { "load": { "enabled": true, "filter": "atomicity_ordering>0", "pointer": true, "value": true } } } ``` **Filter volatile stores or acquire and release operations:** ```json { "instruction_post": { "store": { "enabled": true, "filter": "is_volatile==1 || atomicity_ordering==6", "pointer": true, "value": true } } } ``` **Filter functions by name prefix:** ```json { "function_pre": { "function": { "enabled": true, "filter": "name.startswith(\"test_\")", "name": true } } } ``` **Complex filter with multiple conditions:** ```json { "instruction_post": { "load": { "enabled": true, "filter": "(atomicity_ordering==4 || atomicity_ordering==7) && sync_scope_id==0", "pointer": true, "value": true, "atomicity_ordering": true } } } ``` #### Available Properties 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 identifier - `is_volatile` (integer): 1 if volatile, 0 otherwise - `alignment` (integer): alignment in bytes - `value_size` (integer): size of loaded value in bytes **Function instrumentation:** : - `name` (string): function name - `num_arguments` (integer): number of function arguments - `is_main` (integer): 1 if this is the main function, 0 otherwise **Alloca instructions:** : - `size` (integer): allocation size in bytes (if constant) - `alignment` (integer): allocation alignment in bytes ## Configuration System The Instrumentor uses a JSON-based configuration system that allows users to: 1. Generate a default configuration showing all available options 2. Interactively customize the configuration using the wizard 3. Load and modify existing configurations 4. Generate runtime stub implementations ### Configuration File Format The configuration file is a JSON document with the following structure: ```json { "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 Sections **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.). ### Argument Configuration For each instrumentation opportunity, arguments are configured with: - **enabled**: Boolean to enable/disable the entire opportunity - **filter**: Optional string expression to filter instrumentation based on static properties (see Property Filtering) - **``**: Boolean to enable/disable passing this argument - **`.replace`**: Boolean to enable value replacement (only for replaceable arguments) - **`.description`**: Human-readable description of the argument ## The Configuration Wizard The Instrumentor includes an interactive configuration wizard that simplifies the process of creating and modifying configurations. ### Running the Wizard ```bash # 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 ``` ### Wizard Workflow 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: - Runtime prefix for function names - Target regex for filtering - Enable/disable host (CPU) instrumentation - Enable/disable GPU instrumentation **Step 4: Configure Arguments** : For each enabled instrumentation type, select which arguments to pass to the runtime function. You can: - Toggle individual arguments on/off - Enable value replacement for replaceable arguments - Enable all or disable all arguments - Configure PRE and POST separately (if selected in Step 2) **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`. ### Generating Runtime Stubs The wizard can automatically generate C stub implementations of your runtime functions: 1. In Step 5, select 'g' to generate stubs 2. Specify the output file path (default: `_stubs.c`) 3. The wizard creates a C file with stub implementations that print their arguments The generated stubs are useful as: - Starting templates for implementing your runtime - Documentation of the expected function signatures - Quick prototypes for testing instrumentation Example stub output: ```c 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); } ``` ## Usage Examples ### Basic Usage with opt **Step 1: (Optional) Generate a default configuration** ```bash 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): ```bash ./llvm/utils/instrumentor-config-wizard.py --input config.json -o custom.json ``` **Step 3: Apply instrumentation** ```bash 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. ### Using with Clang To instrument during compilation: ```bash clang -mllvm -enable-instrumentor \ -mllvm -instrumentor-read-config-file=config.json \ source.c -o program ``` ### Complete Workflow Example Here's a complete example for creating a simple memory access profiler: **1. Create configuration with the wizard:** ```bash ./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:** ```c // memory_runtime.c #include #include 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:** ```bash # 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:** ```bash ./program # Output includes: # Load from 0x7ffc12345678 (size: 4, id: 1) # Store to 0x7ffc12345680 (size: 8, id: 2) # ... # Total loads: 42 # Total stores: 27 ``` ## Advanced Use Cases ### Stack Usage Profiling Configure alloca instrumentation to track stack allocations: ```json { "instruction_pre": { "alloca": { "enabled": true, "size": true, "alignment": true, "id": true } }, "instruction_post": { "alloca": { "enabled": true, "address": true, "size": true } } } ``` Runtime implementation: ```c 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; } } ``` ### Value Replacement for Fault Injection Use value replacement to inject faults: ```json { "instruction_post": { "load": { "enabled": true, "value": true, "value.replace": true, "pointer": true } } } ``` Runtime implementation: ```c // 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 } ``` ### Function-Level Tracing Instrument function entry and exit: ```json { "function_pre": { "function": { "enabled": true, "name": true, "address": true, "num_arguments": true } }, "function_post": { "function": { "enabled": true, "name": true } } } ``` Runtime implementation: ```c 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); } ``` ### GPU Instrumentation The Instrumentor supports GPU targets (AMDGPU and NVPTX). Configure GPU-specific instrumentation: ```json { "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. ## Implementation Details ### Generated Runtime Function Signatures The Instrumentor generates runtime function names following this pattern: ```text _[_ind] ``` Where: - ``: Configurable prefix (default: `__instrumentor_`) - ``: Either `pre` or `post` - ``: 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) ### Direct vs Indirect Arguments 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: - Multiple replaceable arguments are enabled (requires indirection for all replaceable args) - An argument's value is too large (aggregate types, large values) When indirect mode is used, a separate function with the `_ind` suffix is generated: ```c // 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. ### Unique IDs When the `id` argument is enabled, the Instrumentor assigns a unique 32-bit integer to each instrumentation call site: - PRE positions get positive IDs (1, 2, 3, ...) - POST positions get negative IDs (-1, -2, -3, ...) - IDs are consistent across multiple runs ### Caching The Instrumentor caches certain argument values between PRE and POST calls when possible: - Values computed in PRE are reused in POST (e.g., pointer value) - This reduces overhead and ensures consistency ### Runtime Function Requirements Runtime functions must be: - Defined with external linkage - Fast and non-blocking (to minimize instrumentation overhead) - Thread-safe if the program is multi-threaded Runtime functions **must not**: - Call back into instrumented code (to avoid infinite recursion) ## Performance Considerations ### Overhead Factors Instrumentation overhead depends on: 1. **Number of instrumentation points**: More instrumented operations = more overhead 2. **Number of arguments passed**: Each argument adds instructions and register pressure 3. **Runtime function complexity**: Complex runtime logic increases overhead 4. **Frequency of instrumented operations**: Instrumenting hot loops has high impact ### Optimization Tips **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: - Lock-free data structures - Thread-local storage - Batching outputs instead of per-call I/O - Sampling (instrument 1 in N calls) **Build with optimizations:** : Use `-O2` or `-O3` when compiling instrumented code. LLVM can optimize away some overhead. ## Troubleshooting ### Common Issues **"Could not find 'opt' binary"** : The wizard can't locate the opt binary. - Specify the path: `--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: - Ensure runtime functions don't trigger more instrumentation **Linking errors** : Runtime functions are undefined. You must: - Implement all enabled runtime functions - Link the runtime implementation with your program - Use the exact function signatures (check generated stubs) **Unexpected instrumentation** : More instrumentation than expected. Check: - The `enabled` flag for each opportunity - `host_enabled` / `gpu_enabled` settings - `target_regex` matches your target - Runtime functions aren't being instrumented (they should be automatically excluded) - Property filters (`filter` field) are correctly specified **Less instrumentation than expected** : Property filters may be excluding instrumentation points: - Check if the `filter` field is set for the instrumentation opportunity - Remember that filters only apply to static (compile-time constant) properties - Dynamic values always pass the filter - Use empty filter (`"filter": ""`) or remove the field to disable filtering - Test your filter expression by examining the IR properties **Filter syntax errors** : Invalid filter expressions will be reported as errors: - Ensure string literals are quoted: `"name==\"foo\""` not `"name==foo"` - Use correct operators: `&&` for AND, `||` for OR - Property names must match exactly - Close all parentheses and quotes ### Debugging Instrumented Code **View instrumented IR:** ```bash opt -passes=instrumentor \ -instrumentor-read-config-file=config.json \ input.ll -S -o output.ll # Examine output.ll to see inserted calls ``` **Print configuration:** ```bash 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. ## Extending the Instrumentor The Instrumentor is designed to be extensible. To add new instrumentation opportunities: 1. **Define the opportunity class** inheriting from `InstrumentationOpportunity` 2. **Implement getter/setter functions** for the arguments 3. **Add initialization** to populate the opportunity with arguments 4. **Register** the opportunity in `InstrumentationConfig::populate()` 5. **Add tests** in `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: - Basic block entry/exit - Branch instrumentation - Call instructions - Atomic operations - Vector operations - Exception handling - Global variable access ## Reference ### Command-Line Options **`-instrumentor-read-config-file=`** : Load instrumentation configuration from the specified JSON file. **`-instrumentor-write-config-file=`** : Write the default instrumentation configuration to the specified JSON file (useful for generating templates). ### Related Passes The Instrumentor is more flexible but related to: - **AddressSanitizer**: Specialized memory error detector - **ThreadSanitizer**: Race condition detector - **MemorySanitizer**: Uninitialized memory detector - **DataFlowSanitizer**: Taint tracking - **XRay**: Function call tracing with low overhead The Instrumentor can implement similar functionality with custom runtime code, but specialized passes may have better performance for their specific use cases. ### Further Reading - Source code: `llvm/lib/Transforms/IPO/Instrumentor.cpp` - Header: `llvm/include/llvm/Transforms/IPO/Instrumentor.h` - Configuration wizard: `llvm/utils/instrumentor-config-wizard.py`