================================== The LLVM Instrumentor Pass ================================== .. contents:: :local: 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:** .. code-block:: json { "instruction_post": { "load": { "enabled": true, "filter": "atomicity_ordering>0", "pointer": true, "value": true } } } **Filter volatile stores or acquire and release operations:** .. code-block:: json { "instruction_post": { "store": { "enabled": true, "filter": "is_volatile==1 || atomicity_ordering==6", "pointer": true, "value": true } } } **Filter functions by name prefix:** .. code-block:: json { "function_pre": { "function": { "enabled": true, "filter": "name.startswith(\"test_\")", "name": true } } } **Complex filter with multiple conditions:** .. code-block:: 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: .. code-block:: 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 ------------------ .. code-block:: 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: .. code-block:: 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** .. code-block:: 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): .. code-block:: bash ./llvm/utils/instrumentor-config-wizard.py --input config.json -o custom.json **Step 3: Apply instrumentation** .. code-block:: 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: .. code-block:: 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:** .. code-block:: 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:** .. code-block:: 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:** .. code-block:: 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:** .. code-block:: 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: .. code-block:: json { "instruction_pre": { "alloca": { "enabled": true, "size": true, "alignment": true, "id": true } }, "instruction_post": { "alloca": { "enabled": true, "address": true, "size": true } } } Runtime implementation: .. code-block:: 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: .. code-block:: json { "instruction_post": { "load": { "enabled": true, "value": true, "value.replace": true, "pointer": true } } } Runtime implementation: .. code-block:: 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: .. code-block:: json { "function_pre": { "function": { "enabled": true, "name": true, "address": true, "num_arguments": true } }, "function_post": { "function": { "enabled": true, "name": true } } } Runtime implementation: .. code-block:: 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: .. code-block:: 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: .. code-block:: 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: .. code-block:: 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:** .. code-block:: 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:** .. code-block:: 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``