# The LLVM Instrumentor Pass


## 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)
  - `runtime_bitcode`: Links the provided runtime library in during instrumentation (default: empty)
  - `inline_runtime`: Inline runtime calls if a runime is provided via `inline_runtime` (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

```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:

```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 <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:**

```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
<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:

```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=<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`
