blob: 7c86437882ab5f079729f4825a9a9920e95f5672 [file] [edit]
==================================
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)
- **<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 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: ``<config_name>_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 <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:**
.. 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
<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)
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=<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).
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``