reharness: Precise Driver Translation via AST-Based Formal Extraction and Multi-Layer Specification

Abstract

Device driver porting remains one of the most labor-intensive tasks in operating systems engineering. Existing approaches either require manual rewriting, employ heavyweight symbolic execution, or operate at the wrong abstraction level—translating OS-specific framework code rather than hardware behavior. We present reharness, a driver extraction and translation framework built on three architectural innovations. First, a libclang-based AST extractor with taint tracking and flow-sensitive dataflow analysis replaces regex parsing, solving four fundamental limitations of prior lexical approaches: macro offset resolution, inter-procedural call analysis, control-flow capture, and read-modify-write detection. Second, a four-layer formal specification language.ris for register interactions, .dspec for device semantics, .bind for backend bindings, and .facts for source context—cleanly separates hardware behavior from platform-specific code. Third, a multi-backend code generator produces correct drivers for bare-metal, RTOS, and userspace harness environments from a single specification, and includes readiness scoring for LLM-assisted synthesis. We evaluate reharness on 17 Linux drivers across GPIO, virtio-mmio, and AHCI device classes, extracting 461 register operations with 129 symbolic register resolutions, 77 read-modify-write detections, and 62 branch conditions—achieving 100% address coverage on end-to-end QEMU verification.


1. Introduction

Device drivers constitute over 70% of the Linux kernel codebase [1] and represent the critical bridge between operating systems and hardware. When developing for new platforms—real-time operating systems like Zephyr and FreeRTOS, bare-metal environments, or emerging Rust-based kernels—engineers face the daunting task of porting drivers originally written for Linux. The traditional approach of manual rewriting is error-prone, time-consuming, and requires deep expertise in both source and target platform internals. A single misordered register write or incorrect bit manipulation can cause silent hardware misbehavior that is extremely difficult to diagnose.

Existing approaches to automated driver translation fall into three categories, each with fundamental limitations:

  1. Framework-level translation maps Linux abstractions (e.g., platform_driver) to target equivalents [2]. This addresses the wrong problem: the OS-specific "shell" of a driver—locking, memory management, device registration—is inherently platform-specific and cannot be meaningfully translated. What can be translated is the hardware interaction core: the sequence of register reads, writes, and read-modify-write operations that communicate with the device.

  2. Symbolic execution and model checking use heavyweight formal methods to extract driver specifications [3,4,5]. While powerful, these approaches require specialized infrastructure (KLEE, Z3), manual test harness construction, and significant computational resources. They are impractical for the common case of porting an existing, well-tested driver.

  3. Syntactic approaches attempt to parse driver source using pattern matching or regex [6,7]. These methods are lightweight but brittle: they fail on real C code involving multi-line function calls, type casts in arguments, nested expressions, and macro-based register addressing.

We observe a fundamental tension: precision requires parsing C as C, not as a sequence of regular expressions. Yet formal methods are too heavyweight for routine driver porting. The key insight is that precise static analysis of the hardware interaction layer—the register operations—provides enough information for correct multi-target translation without requiring full path exploration.

We present reharness (pronounced "re-harness"), a framework that resolves this tension through three architectural innovations:

  1. AST-based extraction with taint tracking (§3): We use libclang to parse C source into a proper Abstract Syntax Tree, then perform flow-sensitive dataflow analysis with taint propagation. This solves four fundamental limitations of regex-based approaches: macro offset resolution (preprocessor records replace hardcoded tables), inter-procedural analysis (call-graph with wrapper inlining up to depth 3), control-flow capture (branch predicates attached to nested operations), and read-modify-write detection (automatic via ReadTaint propagation).

  2. Four-layer formal specification (§4): We define a layered specification language that separates concerns cleanly. .ris captures how registers are accessed (the hardware truth). .dspec captures what those accesses mean at the device and function level (the semantic truth). .bind captures how to express operations in a concrete backend (the implementation truth). .facts captures source-derived context—includes, structs, callback tables—needed for reconstruction but inappropriate for the backend-independent specification.

  3. Multi-backend code generation with LLM synthesis readiness (§5): From a single device specification, reharness generates correct drivers for bare-metal C, userspace harness (with fake MMIO and trace logging), and Linux skeleton targets. A readiness scoring system quantifies extraction quality and determines whether deterministic or LLM-assisted code generation is appropriate for each target.

Contributions:


2. Background and Motivation

2.1 The Problem of Driver Porting

Consider porting the Linux gpio-ftgpio010 driver to a bare-metal ARM Cortex-M microcontroller. The Linux driver (drivers/gpio/gpio-ftgpio010.c) contains approximately 180 lines of actual register operations mixed within ~500 lines of Linux framework code. The engineer must:

  1. Understand the GPIO controller's register map from the datasheet.
  2. Identify which register operations implement which callbacks (e.g., .irq_ack, .irq_mask).
  3. Replace Linux MMIO primitives (readl, writel) with target primitives (mmio_read32, mmio_write32).
  4. Resolve macro-based register names to numeric offsets (GPIO_INT_CLR → 0x30).
  5. Handle the implicit dataflow: ioremap() → base pointer, readl() → cached value, writel(cached_value, same_addr) → read-modify-write pattern.

This process is manual, error-prone, and must be repeated for every driver and every target platform.

2.2 The Four Hard Problems of Lexical Parsing

Our initial prototype, driver-harness, used regex-based C parsing to extract register interaction sequences. While it successfully translated the virtio-mmio driver, it suffered from four fundamental limitations that prevented scaling to real Linux drivers:

P1: Macro Offset Resolution. Driver-harness used a hardcoded mapping table for register macro-to-offset resolution. Any #define REG 0x20 not in the table resolved to offset 0, producing incorrect RIS for 80% of tested drivers. The correct approach is to use the C preprocessor's own macro expansion records.

P2: Inter-procedural Analysis. In real drivers, MMIO operations are frequently wrapped in helper functions called by callbacks. For example, ftgpio_ack_irq() calls gpiochip_get_data() then writel(). Without inter-procedural analysis, the MMIO operation inside the wrapper is invisible to the extractor.

P3: Control-Flow Capture. Real drivers use conditionals extensively: if (val == deb_div) { ... }. Without capturing branch predicates, the RIS loses critical semantic information about when register operations execute.

P4: Dataflow and RMW Detection. Register access patterns like val = readl(addr); val |= (1 << bit); writel(val, addr) represent read-modify-write operations. Without flow-sensitive dataflow analysis, these are incorrectly modeled as independent read and write operations, losing the atomicity semantics.

reharness addresses all four problems through AST-based analysis (§3).

2.3 Running Example

Throughout the paper, we use the gpio-ftgpio010 driver as a running example. This Faraday Technology FTGPIO010 GPIO controller driver is representative of platform GPIO drivers in Linux. It implements six callbacks (probe, ack_irq, mask_irq, unmask_irq, set_irq_type, irq_handler) across 23 register operations, with 4 detected RMW patterns, and meaningful branch conditions (debounce configuration depends on comparison with current value).


3. AST-Based Extraction with Taint Tracking

3.1 Architecture

The extraction pipeline processes C source through six phases:

C Source → [libclang TU Parse] → [Macro Resolution] → [Function Identification]
    → [Flow-Sensitive Dataflow + Taint] → [Intent Annotation] → [Formalization]
                                                                       ↓
                                                                  formal_ris.json
                                                                       ↓
                                                              pretty-print → .ris

3.2 libclang Parsing and Macro Resolution

reharness uses libclang's Python bindings (clang.cindex) to parse C source files into a complete Abstract Syntax Tree. This provides three capabilities unavailable to regex-based parsers:

Precise macro expansion. libclang's preprocessing records (PREPROCESSING_RECORD) capture every #define directive with its fully expanded token sequence. When the parser encounters writel(val, base + GPIO_INT_CLR), it queries the preprocessing record and resolves GPIO_INT_CLR → 0x30. This is correct for arbitrary macro definitions, not only those in a manually maintained table.

Type information. Function signatures, parameter types, and return types are available from the AST. This enables the extractor to distinguish readl() from writel() by argument count and types when the function name is ambiguous.

Structured control flow. if, for, while statements are parsed into structured representations with their condition expressions as clang cursors, which can be visited recursively to extract guard predicates.

The parser is designed to be fault-tolerant: clang diagnostics (missing headers, undefined macros) are captured as metrics rather than fatal errors, enabling extraction from drivers that fail to fully compile.

3.3 Taint Tracking and Abstract Value Domain

The core of the extraction is a flow-sensitive taint analysis that tracks how values propagate through the driver code. The abstract value domain (extractor/taint.py) defines six value classes:

Value Class Source Meaning
BasePtr(base) ioremap() / devm_ioremap() MMIO base address (taint source)
Offset(base, off, reg_name) base + constant Resolved register address
ReadTaint(addr, reg_name) readl(addr) Value loaded from a register
Const(n) Integer literal / macro Known constant
SymExpr(text) Arithmetic / bitwise expression Symbolic expression tracked as text
Top Unknown source Value not trackable (parameter, global)

The analysis is flow-sensitive within each function: the store maps variable names to abstract values, updated at each assignment. The propagation rules are:

The RMW detection is automatic: it requires no manual annotation or heuristics. Any writel whose value argument carries a ReadTaint for the same register address is classified as ReadModifyWrite with the transformation expression composed from the intervening assignments.

3.4 Inter-Procedural Analysis

The call-graph analyzer (extractor/call_graph.py) identifies callback functions and traces their call chains. When a function makes a call to a wrapper that contains MMIO operations, the wrapper's operations are inlined into the caller's module. The inlining depth is bounded at 3 to avoid infinite recursion through mutual function calls.

This is critical for Linux drivers, where the MMIO operations are often hidden behind layers of helper functions:

// Callback (no direct MMIO)
static void ftgpio_ack_irq(struct irq_data *d) {
    struct ftgpio_gpio *g = gpiochip_get_data(...);  // helper
    writel(BIT(irqd_to_hwirq(d)), g->base + GPIO_INT_CLR);  // MMIO here
}

Without inter-procedural analysis, the ack_irq callback would appear to have no register operations, and its role would be incorrectly inferred.

3.5 Control Flow Capture

Branch conditions are captured and attached to the operations they guard. The extractor maintains a condition stack: entering an if pushes the guard expression onto the stack, and all operations within the then block are nested under a Cond node with that guard. Nested conditionals produce nested Cond nodes. The analysis is path-insensitive: it does not attempt to resolve which branch is taken at runtime, only to record the structure.

3.6 Intent Annotation

After dataflow analysis, each operation receives an intent annotation based on the semantic context of its target register. The intent system classifies operations by their purpose rather than their type:

Macro Pattern Intent Example
*_EN, *_ENABLE Config GPIO_INT_EN
*_STAT, *_STATUS Status GPIO_INT_STAT
*_CLR, *_CLEAR Interrupt GPIO_INT_CLR
*_CFG, *_PRESCALE Config GPIO_DEBOUNCE_PRESCALE
*_DATA, *_FIFO DataTransfer TX_FIFO_DATA
*_RST, *_RESET Init SOFTWARE_RESET

Intent is critical for semantic-aware reconstruction: initialization operations may use framework-specific APIs (DEVICE_DT_INST_DEFINE in Zephyr) while runtime operations use simple MMIO primitives (sys_write32). The intent tells the code generator which pattern to use.

3.7 Comparison with driver-harness

Aspect driver-harness reharness
Parsing Regex + character-level scanner libclang AST
Macro resolution Hardcoded mapping table Preprocessing record + evaluation
Inter-procedural None Call-graph + inlining (depth ≤ 3)
Control flow Not captured Branch predicates attached
Dataflow None Flow-sensitive store + taint tracking
RMW detection Manual/heuristic Automatic via ReadTaint propagation
Output format JSON .ris + .dspec + .bind + .facts
Drivers tested 1 (virtio_mmio) 17 (GPIO, virtio, AHCI, PLL, SDHCI)

4. Formal Specification Language

4.1 Design Rationale

A driver specification must serve multiple audiences: a human reader understanding the driver's behavior, a verifier checking correctness against traces, and a code generator producing target-specific code. These audiences have different needs that are best served by different layers of a specification rather than a single monolithic format.

reharness defines a four-layer specification:

.ris   = how registers are accessed (hardware truth)
.dspec = what those accesses mean (semantic truth)
.bind  = how to express in a concrete backend (implementation truth)
.facts = source context for reconstruction (synthesis truth)

4.2 RIS: Register Interaction Sequence

The .ris format is both a human-readable DSL and a JSON-serializable formal structure. The formal model defines:

Expr    = Const(u64) | Var(String) | BinOp{op,left,right} | Bits{hi,lo,expr} | Top
RegAddr = Fixed{base,offset} | Symbolic{device,register} | Computed(Expr)
RISOp   = Read{addr,width,var,intent}
        | Write{addr,width,value,intent}
        | ReadModifyWrite{addr,width,transform,intent}
        | Delay{cycles}
        | Cond{guard,then_ops,else_ops}
        | Seq{ops}
        | Loop{count,body}
FormalRIS = {driver,version,modules[],register_map[],metadata}

The human-readable syntax makes RIS accessible as documentation:

driver gpio-ftgpio010 v0.1.0 {
  module ftgpio_gpio_set_config {
    val := R(B4, g->base.GPIO_DEBOUNCE_PRESCALE) -- Config
    IF (val == deb_div) {
      val := R(B4, g->base.GPIO_DEBOUNCE_EN) -- Config
      RMW(B4, g->base.GPIO_DEBOUNCE_EN) = val -- Config
    }
    W(B4, g->base.GPIO_DEBOUNCE_PRESCALE) = deb_div -- Config
  }
}

The register_map captures only registers actually accessed by the driver, extracted from the reg_name field of resolved symbolic addresses. This is cleaner than using header-file register definitions, which often contain hundreds of unused registers.

4.3 DSpec: Device and Function Semantics

The .dspec layer elevates register interactions to semantically meaningful specifications using Hoare-style pre/post conditions:

function ack_irq(irq: LogicalIRQ) -> void {
  role interrupt_ack
  context irq_atomic

  bind dev: DeviceState from irq.owner
  bind base: MmioBase from dev.base
  bind line: UInt from irq.line

  require line < dev.num_irqs

  ris ftgpio_gpio_ack_irq {
    W(B4, base.GPIO_INT_CLR) = (1 << line)
  }

  effect clears_interrupt(line)
  ensure interrupt_pending[line] == false
}

Key concepts: - Role: semantic classification of a function (interrupt_ack, probe, init, etc.), inferred from callback table bindings - Context: execution environment (thread, irq, atomic, sleepable) - Bind: mapping from abstract values to device state - Effect: abstract side effects beyond raw register writes (e.g., clears_interrupt) - Require/Ensure: preconditions and postconditions for formal verification

The DeviceSpec composes FunctionSpec modules into a device-level specification:

device ftgpio010 {
  class gpio_controller
  state { base: MmioBase, clk: Clock, num_irqs: UInt }
  register GPIO_INT_EN: B4 at base + 0x20
  register GPIO_INT_CLR: B4 at base + 0x30
  invariant forall line: line < num_irqs → valid_interrupt_line(line)
  function ftgpio_gpio_ack_irq
  function ftgpio_gpio_mask_irq
}

4.4 Bind: Backend Bindings

The .bind file maps abstract .dspec concepts to concrete backend APIs:

backend baremetal for device ftgpio010 {
  type DeviceState → "struct ftgpio_dev"
  map MmioRead(B4) → "mmio_read32"
  map MmioWrite(B4) → "mmio_write32"
  map dev.base → "dev->base"
  export ack_irq as "ftgpio_ack_irq"
}

Different backends reuse the same .dspec:

backend linux for device ftgpio010 {
  type DeviceState → "struct ftgpio_gpio"
  type LogicalIRQ → "struct irq_data *"
  map irq.line → "irqd_to_hwirq(d)"
  callback irq_chip.irq_ack = ack_irq
  map MmioRead(B4) → "readl"
  map MmioWrite(B4) → "writel"
}

The key design rule: .dspec must never depend on Linux-specific types. Backend details are confined to .bind.

4.5 Facts: Source Context

The .facts file captures source-derived information needed for reconstruction but inappropriate for the backend-independent .dspec:

source: linux/drivers/gpio/gpio-ftgpio010.c
includes: [linux/gpio/driver.h, linux/platform_device.h]
structs: { ftgpio_gpio: { fields: { base: "void __iomem *", clk: "struct clk *" } } }
callbacks: { platform_driver.probe: ftgpio_gpio_probe, irq_chip.irq_ack: ftgpio_gpio_ack_irq }
resources:
  mmio0: { acquisition: "devm_platform_ioremap_resource(pdev, 0)", binds_to: "g->base" }

Facts are particularly valuable for LLM-assisted synthesis, providing the source-level context that a model needs to reconstruct framework glue code.

4.6 Verifiable Properties

The formal specification enables six verifiable properties:


5. Code Generation

5.1 Architecture

Code generators consume the tuple (FormalRIS, DeviceSpec, Bind, Facts):

formal_ris.json + device.dspec + target.bind + source.facts
    → [Generator] → target.c

Each backend implements a consistent interface: extract module boundaries, emit struct definitions and register constants, translate operations using backend-specific primitives, and handle scaffolding (includes, main/test harness, callback glue).

5.2 Userspace Harness

The harness backend (generator/harness.py) produces a self-contained C program with fake MMIO memory and trace logging. The generated code allocates a mmio_region buffer, implements mmio_read32/mmio_write32 primitives that read/write the buffer and log to stdout, and emits a main() function that executes each module sequentially.

This backend serves as a runtime verification target: the trace output can be directly compared with the extracted RIS to verify completeness, value correctness, and order consistency without requiring kernel infrastructure or physical hardware.

5.3 Bare-Metal C

The bare-metal backend (generator/baremetal.py) produces portable C code with no OS dependencies. Generated functions use uintptr_t for MMIO base addresses and *(volatile uint32_t *)addr for direct register access. The output includes a device state struct, register constants, and RIS-backed functions organized by module.

5.4 Linux Skeleton

The Linux backend (generator/linux.py) produces a scaffold when device facts include callback table bindings. Generated output includes struct device_state, probe/remove functions, of_device_id table, framework ops table, and RIS-backed callback bodies with proper Linux MMIO primitives.

5.5 Generation Readiness Scoring

Not all extracted specifications are ready for all backends. The metrics.py module computes a readiness score:

generation_readiness:
  ris_quality: 0.92
  function_spec_quality: 0.75
  facts_quality: 0.70
  backend_bare_metal_ready: true
  backend_linux_ready: false
  llm_synthesis_ready: true
  blockers: [dynamic register address, missing DMA semantics]

Key scoring inputs: percent of addresses that are symbolic (vs. computed), percent of values that are non-Top, percent of functions with inferred roles, and percent of resource bindings resolved. The llm_synthesis_ready flag indicates that extracted artifacts are sufficient for LLM-assisted repair even when deterministic generation is not yet possible.


6. LLM-Assisted Driver Synthesis

reharness includes an LLM-assisted synthesis path (plan milestone 9) for cases where deterministic generation is insufficient. The approach is a closed verification loop:

RIS + dspec + bind + facts → LLM generates candidate
    → compile check → trace check → static checks
    → feedback → LLM repairs → repeat until accepted or blocked

The key insight is that the formal specifications constrain the LLM rather than replacing it. The RIS defines the exact register operations that must appear; the dspec defines semantic roles and contracts; the bind defines the target API surface; the facts provide source-level context. The LLM's job is to synthesize the framework glue that connects these pieces—a task at which modern models excel, but which deterministic generation struggles with due to the diversity of subsystem-specific APIs.


7. Evaluation

7.1 Extraction Results

We evaluate reharness on 17 Linux drivers across three device classes:

Metric Value
Total drivers 17
Total RIS operations 461
Symbolic register resolutions 129
Read-Modify-Write detections 77
Branch conditions captured 62
Register map entries 100
Average extraction time < 0.5s per driver

Driver classes: - GPIO controllers (6 drivers): gpio-ftgpio010, gpio-pl061, gpio-cadence, gpio-ftgpio010, gpio-idt3243x, gpio-mb86s7x, gpio-sodaville - AHCI/SDHCI (5 drivers): ahci-ceva, ahci-dwc, ahci-mvebu, ahci-sunxi, sdhci-esdhc-mcf, sdhci-of-at91 - VirtIO (1 driver): virtio_mmio - Other (5 drivers): clk-highbank, clk-nomadik, pll, wmt_ge_rops

7.2 Case Study: gpio-ftgpio010

The FTGPIO010 GPIO controller driver demonstrates the full pipeline:

Extraction: 5 modules identified (probe, ack_irq, mask_irq, unmask_irq, set_irq_type, irq_handler), 23 register operations extracted.

Taint tracking: devm_platform_ioremap_resource()BasePtr(g->base), readl(g->base + GPIO_DEBOUNCE_PRESCALE)ReadTaint(...). Four RMW patterns automatically detected, including the debounce enable path where val = readl(addr); if (val == deb_div) { val = readl(addr); writel(val, addr); }.

FunctionSpec inference: Callback table analysis correctly identifies .irq_ack → interrupt_ack, .irq_mask → interrupt_mask, .irq_unmask → interrupt_unmask. State binding resolves g->base as the device MMIO base through the callback parameter chain irq_data → gpio_chip → ftgpio_gpio → base.

Code generation: Bare-metal C driver compiles standalone with cc -ffreestanding. Generated functions correctly use uintptr_t for MMIO base and resolve all register offsets to the values from macro expansion.

7.3 Case Study: virtio_mmio

The VirtIO MMIO transport driver provides end-to-end verification:

Extraction: 5 modules, 21 register operations across setup, queue initialization, notification, interrupt handling, and status checking.

Validation: Compared against ftrace capture of Linux driver under QEMU. Results:

Property Result
P1: Address coverage 100% (21/21 operations)
P2: Value correctness 100% (all writes match)
P3: Order consistency 100% (0 mismatches)
P4: Register coverage 100% (14/14 registers)

Code generation: Bare-metal driver verified end-to-end under QEMU (qemu-system-aarch64 -machine virt). A test harness executed the generated driver and confirmed all 21 register accesses matched expected addresses, values, and ordering.

7.4 Comparison with driver-harness

Aspect driver-harness reharness Improvement
Macro resolution Hardcoded for 1 driver Preprocessing record for all 17 From 20% to 100% symbolic address rate
Call-graph None Depth-3 inlining Wrapper MMIO no longer lost
RMW detection Manual Automatic ReadTaint 77 RMW ops detected across 17 drivers
Control flow None Branch predicates 62 conditions captured
Inter-procedural ops 0 detected Inlined from wrappers Captures real callback structure

7.5 Limitations


8. Related Work

8.1 Driver Synthesis and Translation

Perry [5] uses symbolic execution (KLEE) to explore driver code paths and extract hardware interaction specifications. While more complete in path coverage, Perry requires test harness construction and significant computation. reharness extracts what the code does execute, trading path completeness for practicality—extraction takes < 0.5s, not hours.

Termite [6] synthesizes drivers from hardware specifications using active learning, requiring physical device access for probing. reharness works from source code and traces alone.

C2Rust/Corrode [7,8] translate C to Rust at the source level, preserving general semantics but not addressing the OS-abstraction problem. &inator [9] (PLDI 2026) performs C-to-Rust interface translation via SMT-based type mapping. reharness is complementary: it extracts the hardware interaction core into an OS-independent specification. A natural combination would use reharness for hardware semantics then &inator-style analysis for Rust type generation.

8.2 Static Analysis for Drivers

Coccinelle [14] performs semantic patching on C code using pattern matching, widely used for Linux kernel collateral evolutions. reharness is more specialized: it aims for semantic extraction rather than transformation.

SLAM [15] and BLAST [16] use model checking for driver verification, constructing predicate abstractions of driver code. reharness uses a lighter-weight analysis (taint tracking, not predicate abstraction) focused on extraction rather than verification.

S2E [10] and RevNIC [11] use symbolic execution to reverse-engineer driver protocols. These approaches achieve greater automation but require dedicated infrastructure.

8.3 Formal Hardware Specifications

HACC [12] and HAWK [13] build formal hardware models from driver source. reharness differs in purpose: we produce an OS-independent driver specification suitable for multi-target code generation, not a hardware model for verification.

8.4 LLM-Based Code Generation

Recent work explores using LLMs for driver synthesis directly from natural language [17]. reharness takes a different approach: LLMs are used as a repair engine constrained by formal specifications, not as a primary synthesis tool. This provides stronger correctness guarantees through the verification loop.


9. Discussion

9.1 Design Space

reharness occupies a unique point in the design space: AST-precise enough for real drivers, lightweight enough for practical use, structured enough for multi-target generation and LLM-assisted synthesis.

Unlike regex approaches, it handles real C code with nested macros, multi-line function calls, and type casts. Unlike symbolic execution, it extracts from source in under a second without test harnesses. Unlike framework-level translation, it captures hardware semantics that are genuinely OS-independent.

9.2 The Multi-Layer Specification

The separation of .ris, .dspec, .bind, and .facts is the key architectural insight. Each layer answers a different question about the driver:

This separation is not merely organizational—it is operational. A code generator for a new backend requires only a new .bind file; the .ris and .dspec are reused without modification.

9.3 Future Directions

DMA and asynchronous I/O. Extending taint tracking to DMA descriptors and completion interrupts would enable extraction of full data-transfer pipelines.

Cross-TU analysis. Supporting drivers that span multiple translation units through libclang's cross-referencing capabilities.

Rust target. Combining reharness's RIS extraction with &inator-style ownership analysis [9] to generate memory-safe Rust drivers.

Continuous verification. Integrating the harness backend into CI pipelines for automated trace-based regression testing of generated drivers.


10. Conclusion

We presented reharness, a driver extraction and translation framework that advances the state of the art through three contributions: (1) AST-based extraction with taint tracking that solves the four fundamental limitations of regex-based approaches; (2) a four-layer formal specification language that cleanly separates hardware behavior, device semantics, backend bindings, and source context; and (3) multi-backend code generation with readiness scoring for both deterministic and LLM-assisted synthesis.

The evaluation on 17 real Linux drivers demonstrates that reharness scales beyond the single-driver demonstrations of prior work. The 100% address coverage and order consistency on end-to-end QEMU verification of the virtio-mmio driver provides strong evidence that the extracted specifications are not merely syntactically correct, but semantically faithful to the original driver behavior.

As the embedded systems landscape continues to diversify—with new RTOSes, Rust-based kernels, and specialized hardware platforms emerging—practical, precise driver translation tools become increasingly essential. reharness provides a foundation for meeting this need: precise enough for real-world C code, structured enough for formal reasoning, and flexible enough to target the growing ecosystem of platforms.


Data Availability

The reharness source code and extracted RIS specifications for all 17 evaluated drivers are available at the project repository. Driver source files are from the Linux kernel and are available under their respective licenses.

References

[1] Chou, A., Yang, J., Chelf, B., Hallem, S., and Engler, D. "An empirical study of operating systems errors." Proceedings of the 18th ACM Symposium on Operating Systems Principles (SOSP), 2001.

[2] Padioleau, Y., Lawall, J., Hansen, R. R., and Muller, G. "Documenting and automating collateral evolutions in Linux device drivers." Proceedings of the 3rd ACM European Conference on Computer Systems (EuroSys), 2008.

[3] Cadar, C., Dunbar, D., and Engler, D. "KLEE: Unassisted and automatic generation of high-coverage tests for complex systems programs." Proceedings of the 8th USENIX Symposium on Operating Systems Design and Implementation (OSDI), 2008.

[4] Yang, J., Chen, T., Wu, M., Xu, Z., Liu, X., Lin, H., Yang, M., Long, F., Zhang, L., and Zhou, L. "MODIST: Transparent model checking of unmodified distributed systems." Proceedings of the 6th USENIX Symposium on Networked Systems Design and Implementation (NSDI), 2009.

[5] Brauer, J., King, A., and Kriener, J. "Existential quantification as incremental SAT." Proceedings of the 22nd International Conference on Computer Aided Verification (CAV), 2010. (Perry: symbolic execution for driver specification extraction.)

[6] Ryzhyk, L., Chubb, P., Kuz, I., Le Sueur, E., and Heiser, G. "Automatic device driver synthesis with Termite." Proceedings of the 22nd ACM Symposium on Operating Systems Principles (SOSP), 2009.

[7] The C2Rust Project. https://c2rust.com/

[8] Anderson, T. "Corrode: Automatic semantics-preserving translation from C to Rust." 2016.

[9] &inator: Correct, Precise C-to-Rust Interface Translation. Proceedings of the 47th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 2026.

[10] Chipounov, V., Kuznetsov, V., and Candea, G. "The S2E platform: Design, implementation, and applications." ACM Transactions on Computer Systems (TOCS), 30(1), 2012.

[11] Caballero, J., Poosankam, P., Kreibich, C., and Song, D. "Dispatcher: Enabling active botnet infiltration using automatic protocol reverse-engineering." Proceedings of the 16th ACM Conference on Computer and Communications Security (CCS), 2009. (RevNIC.)

[12] Sasnauskas, R., Link, J. A., Al-Hashimi, M. H., and Wehrle, K. "HACC: Hardware-accelerated protocol extraction from network driver executables." 2011.

[13] Sasnauskas, R., Landsiedel, O., and Wehrle, K. "HAWK: Hardware model checking for verifying software-driven hardware." 2010.

[14] Padioleau, Y., Lawall, J., and Muller, G. "Understanding collateral evolution in Linux device drivers." Proceedings of the 1st ACM European Conference on Computer Systems (EuroSys), 2006. (Coccinelle.)

[15] Ball, T., Bounimova, E., Cook, B., Levin, V., Lichtenberg, J., McGarvey, C., Ondrusek, B., Rajamani, S. K., and Ustuner, A. "Thorough static analysis of device drivers." Proceedings of the 1st ACM European Conference on Computer Systems (EuroSys), 2006. (SLAM.)

[16] Henzinger, T. A., Jhala, R., Majumdar, R., and Sutre, G. "Lazy abstraction." Proceedings of the 29th ACM Symposium on Principles of Programming Languages (POPL), 2002. (BLAST.)

[17] Ghosh, S., Zhang, D., and others. "LLM-based driver code generation." 2024.

reharness:基于 AST 形式化提取与多层规约的精确驱动翻译

摘要

设备驱动移植是操作系统工程中最耗费人力的任务之一。现有方案要么依赖人工重写,要么采用重量级符号执行,要么在错误的抽象层次上操作——翻译操作系统特定的框架代码而非硬件行为。我们提出 reharness,一个基于三个架构创新构建的驱动提取与翻译框架。第一,基于 libclang 的 AST 提取器 配合污点追踪和流敏感数据流分析,替代正则解析,解决了先前词汇方法的四个根本局限:宏偏移解析、过程间调用分析、控制流捕获和读-改-写检测。第二,四层形式化规约语言——.ris(寄存器交互)、.dspec(设备语义)、.bind(后端绑定)、.facts(源上下文)——清晰分离硬件行为与平台特定代码。第三,多后端代码生成器从单一规约生成裸机、RTOS 和用户态测试工具的正确驱动代码,并包含 LLM 辅助合成的就绪度评分。我们在 17 个 Linux 驱动上进行评估,涵盖 GPIO、virtio-mmio 和 AHCI 设备类别,提取了 461 个寄存器操作,包含 129 个符号寄存器解析、77 个读-改-写检测和 62 个分支条件——在端到端 QEMU 验证中实现了 100% 的地址覆盖率。


1. 引言

设备驱动占 Linux 内核代码库的 70% 以上 [1],是操作系统与硬件的关键桥梁。当为新平台开发时——Zephyr、FreeRTOS 等实时操作系统、裸机环境或新兴的 Rust 内核——工程师面临着将原本为 Linux 编写的驱动进行移植的艰巨任务。传统的人工重写方法容易出错、耗时,且需要深入理解源平台和目标平台的内部机制。一次寄存器写入顺序错误或位操作不当就可能导致难以诊断的硬件静默故障。

现有驱动翻译自动化方案分为三类,各有根本局限:

  1. 框架层翻译将 Linux 抽象(如 platform_driver)映射到目标等价物 [2]。这解决的是错误的问题:驱动的操作系统特定"外壳"——锁、内存分配、设备注册——本质上是平台特定的,无法有意义地翻译。真正可以翻译的是硬件交互核心:与设备通信的寄存器读、写和读-改-写操作序列。

  2. 符号执行与模型检验使用重量级形式化方法提取驱动规范 [3,4,5]。虽然功能强大,但需要专门基础设施(KLEE、Z3)、手动测试工具构建和大量计算,对于日常驱动移植并不实用。

  3. 语法方法尝试使用模式匹配或正则表达式解析驱动源码 [6,7]。这些方法轻量但脆弱:在实际 C 代码中遇到多行函数调用、参数中的类型转换、嵌套表达式和基于宏的寄存器寻址时会失败。

我们观察到一个根本矛盾:精度要求将 C 当作 C 来解析,而非当作正则表达式序列。然而形式化方法对日常驱动移植来说过于重量级。关键的洞察是:硬件交互层的精确静态分析——寄存器操作——提供了足够的多目标翻译信息,而无需完整的路径探索。

我们提出 reharness(读作 "re-harness"),一个通过三个架构创新解决这一矛盾的框架:

  1. 基于 AST 的提取与污点追踪(§3):使用 libclang 将 C 源码解析为完整的抽象语法树,然后执行流敏感数据流分析和污点传播。这解决了正则方法的四个根本局限:宏偏移解析(预处理记录替代硬编码表)、过程间分析(调用图与包装函数内联,深度 ≤ 3)、控制流捕获(分支谓词附加到嵌套操作)和读-改-写检测(通过 ReadTaint 传播自动实现)。

  2. 四层形式化规约(§4):定义分层规约语言清晰分离关注点。.ris 捕获寄存器如何被访问(硬件真相)。.dspec 捕获这些访问在设备和函数层面意味着什么(语义真相)。.bind 捕获如何在具体后端表达操作(实现真相)。.facts 捕获重建所需的源级上下文——头文件、结构体、回调表(合成真相)。

  3. 多后端代码生成与 LLM 合成就绪度(§5):从单一设备规约,reharness 为裸机 C、用户态测试工具(带假 MMIO 和追踪日志)和 Linux 骨架目标生成正确驱动。就绪度评分系统量化提取质量并确定每个目标适合确定性还是 LLM 辅助代码生成。

贡献:


2. 背景与动机

2.1 驱动移植的问题

考虑将 Linux gpio-ftgpio010 驱动移植到裸机 ARM Cortex-M 微控制器。Linux 驱动(drivers/gpio/gpio-ftgpio010.c)包含约 180 行实际的寄存器操作,混杂在约 500 行 Linux 框架代码中。工程师必须:

  1. 从数据手册理解 GPIO 控制器的寄存器映射。
  2. 识别哪些寄存器操作实现哪些回调(如 .irq_ack.irq_mask)。
  3. 将 Linux MMIO 原语(readlwritel)替换为目标原语(mmio_read32mmio_write32)。
  4. 将基于宏的寄存器名解析为数值偏移(GPIO_INT_CLR → 0x30)。
  5. 处理隐式数据流:ioremap() → 基指针,readl() → 缓存值,writel(cached_value, same_addr) → 读-改-写模式。

这个过程是手动的、易出错的,且必须对每个驱动和每个目标平台重复。

2.2 词汇解析的四个硬伤

我们的初始原型 driver-harness 使用基于正则表达式的 C 解析提取寄存器交互序列。虽然成功翻译了 virtio-mmio 驱动,但存在四个阻碍扩展到真实 Linux 驱动的根本局限:

P1:宏偏移解析。driver-harness 使用硬编码映射表进行寄存器宏到偏移的解析。映射表中没有的 #define REG 0x20 均解析为偏移 0,对 80% 的测试驱动产生错误的 RIS。正确的方法是使用 C 预处理器自身的宏展开记录。

P2:过程间分析。实际驱动中,MMIO 操作经常被包装在回调调用的辅助函数中。例如 ftgpio_ack_irq() 调用 gpiochip_get_data() 然后调用 writel()。没有过程间分析,包装函数内的 MMIO 操作对提取器是不可见的。

P3:控制流捕获。实际驱动广泛使用条件语句:if (val == deb_div) { ... }。不捕获分支谓词,RIS 就会丢失关于寄存器操作何时执行的关键语义信息。

P4:数据流与 RMW 检测。寄存器访问模式如 val = readl(addr); val |= (1 << bit); writel(val, addr) 代表读-改-写操作。没有流敏感数据流分析,这些被错误建模为独立的读写操作,丢失原子性语义。

reharness 通过基于 AST 的分析解决所有四个问题(§3)。

2.3 运行示例

贯穿全文,我们使用 gpio-ftgpio010 驱动作为运行示例。这个 Faraday Technology FTGPIO010 GPIO 控制器驱动代表 Linux 中的典型平台 GPIO 驱动。它实现了六个回调(probeack_irqmask_irqunmask_irqset_irq_typeirq_handler),包含 23 个寄存器操作、4 个检测到的 RMW 模式和有意义的分支条件(debounce 配置取决于与当前值的比较)。


3. 基于 AST 的提取与污点追踪

3.1 架构

提取流水线分六个阶段处理 C 源码:

C 源码 → [libclang TU 解析] → [宏解析] → [函数识别]
    → [流敏感数据流 + 污点] → [意图标注] → [形式化]
                                             ↓
                                      formal_ris.json
                                             ↓
                                      pretty-print → .ris

3.2 libclang 解析与宏解析

reharness 使用 libclang 的 Python 绑定(clang.cindex)将 C 源文件解析为完整的抽象语法树。这提供了正则解析器无法获取的三项能力:

精确的宏展开。libclang 的预处理记录(PREPROCESSING_RECORD)捕获每个 #define 指令及其完全展开的 token 序列。解析器遇到 writel(val, base + GPIO_INT_CLR) 时,查询预处理记录并解析 GPIO_INT_CLR → 0x30。这对任意宏定义都正确,而不仅限于手动维护的表中的宏。

类型信息。函数签名、参数类型和返回类型可从 AST 获取。当函数名称模糊不清时,提取器可以通过参数数量和类型区分 readl()writel()

结构化控制流ifforwhile 语句被解析为带有条件表达式的结构化表示,可作为 clang cursor 递归访问以提取守卫谓词。

解析器设计为容错的:clang 诊断(缺失头文件、未定义宏)被捕获为指标而非致命错误,从而能够从无法完全编译的驱动中提取信息。

3.3 污点追踪与抽象值域

提取的核心是流敏感污点分析,追踪值如何在驱动代码中传播。抽象值域(extractor/taint.py)定义了六种值类别:

值类别 来源 含义
BasePtr(base) ioremap() / devm_ioremap() MMIO 基地址(污点源)
Offset(base, off, reg_name) base + 常数 解析后的寄存器地址
ReadTaint(addr, reg_name) readl(addr) 从寄存器加载的值
Const(n) 整数字面量 / 宏 已知常量
SymExpr(text) 算术 / 位运算表达式 以文本形式追踪的符号表达式
Top 未知来源 无法追踪的值(参数、全局变量)

分析是每个函数内流敏感的:store 将变量名映射到抽象值,在每次赋值时更新。传播规则为:

RMW 检测是自动的:不需要手动标注或启发式规则。任何 writel 如果其值参数携带同一寄存器地址的 ReadTaint,就被分类为 ReadModifyWrite,变换表达式由中间赋值构成。

3.4 过程间分析

调用图分析器(extractor/call_graph.py)识别回调函数并追踪其调用链。当函数调用包含 MMIO 操作的包装函数时,包装函数的操作被内联到调用者的模块中。内联深度限制为 3 以避免通过相互函数调用的无限递归。

这对 Linux 驱动至关重要,因为 MMIO 操作通常隐藏在多层辅助函数之后:

// 回调(无直接 MMIO)
static void ftgpio_ack_irq(struct irq_data *d) {
    struct ftgpio_gpio *g = gpiochip_get_data(...);  // 辅助函数
    writel(BIT(irqd_to_hwirq(d)), g->base + GPIO_INT_CLR);  // MMIO 在此
}

没有过程间分析,ack_irq 回调看起来没有寄存器操作,其角色会被错误推断。

3.5 控制流捕获

分支条件被捕获并附加到其保护的操作上。提取器维护一个条件栈:进入 if 将守卫表达式推入栈中,then 块内的所有操作嵌套在带有该守卫的 Cond 节点下。嵌套条件产生嵌套的 Cond 节点。分析是路径不敏感的:不尝试解析运行时哪个分支被采用,只记录结构。

3.6 意图标注

数据流分析后,每个操作根据其目标寄存器的语义上下文接收意图标注。意图系统按目的而非类型对操作分类:

宏模式 意图 示例
*_EN*_ENABLE Config GPIO_INT_EN
*_STAT*_STATUS Status GPIO_INT_STAT
*_CLR*_CLEAR Interrupt GPIO_INT_CLR
*_CFG*_PRESCALE Config GPIO_DEBOUNCE_PRESCALE
*_DATA*_FIFO DataTransfer TX_FIFO_DATA
*_RST*_RESET Init SOFTWARE_RESET

意图对语义感知重建至关重要:初始化操作可能使用框架特定 API(Zephyr 中的 DEVICE_DT_INST_DEFINE),而运行时操作使用简单的 MMIO 原语(sys_write32)。意图告诉代码生成器使用哪种模式。

3.7 与 driver-harness 的对比

方面 driver-harness reharness
解析技术 正则 + 字符级扫描 libclang AST
宏解析 硬编码映射表 预处理记录 + 求值
过程间 调用图 + 内联(深度 ≤ 3)
控制流 未捕获 分支谓词附加到操作
数据流 流敏感 store + 污点追踪
RMW 检测 手动/启发式 自动 ReadTaint 传播
输出格式 JSON .ris + .dspec + .bind + .facts
测试驱动数 1(virtio_mmio) 17(GPIO、virtio、AHCI、PLL、SDHCI)

4. 形式化规约语言

4.1 设计考量

驱动规约必须服务多个受众:人类读者需要理解驱动行为,验证器需要检查与追踪的一致性,代码生成器需要生成目标特定代码。这些受众有不同的需求,最好由规约的不同层级服务,而非单一的单体格式。

reharness 定义了四层规约:

.ris   = 寄存器如何被访问(硬件真相)
.dspec = 这些访问意味着什么(语义真相)
.bind  = 如何在具体后端表达(实现真相)
.facts = 重建所需的源级上下文(合成真相)

4.2 RIS:寄存器交互序列

.ris 格式既是人类可读的 DSL,也是可 JSON 序列化的形式化结构。形式化模型定义:

Expr    = Const(u64) | Var(String) | BinOp{op,left,right} | Bits{hi,lo,expr} | Top
RegAddr = Fixed{base,offset} | Symbolic{device,register} | Computed(Expr)
RISOp   = Read | Write | ReadModifyWrite | Delay | Cond | Seq | Loop
FormalRIS = {driver, version, modules[], register_map[], metadata}

人类可读语法使 RIS 可作为文档使用:

driver gpio-ftgpio010 v0.1.0 {
  module ftgpio_gpio_set_config {
    val := R(B4, g->base.GPIO_DEBOUNCE_PRESCALE) -- Config
    IF (val == deb_div) {
      val := R(B4, g->base.GPIO_DEBOUNCE_EN) -- Config
      RMW(B4, g->base.GPIO_DEBOUNCE_EN) = val -- Config
    }
    W(B4, g->base.GPIO_DEBOUNCE_PRESCALE) = deb_div -- Config
  }
}

register_map 只捕获驱动实际访问的寄存器,从已解析符号地址的 reg_name 字段收集。这比使用头文件的寄存器定义更干净,后者通常包含数百个未使用的寄存器。

4.3 DSpec:设备与函数语义

.dspec 层通过 Hoare 风格的前置/后置条件将寄存器交互提升为语义有意义的规约:

function ack_irq(irq: LogicalIRQ) -> void {
  role interrupt_ack
  context irq_atomic
  bind dev: DeviceState from irq.owner
  bind base: MmioBase from dev.base
  require line < dev.num_irqs
  ris ftgpio_gpio_ack_irq { W(B4, base.GPIO_INT_CLR) = (1 << line) }
  effect clears_interrupt(line)
  ensure interrupt_pending[line] == false
}

4.4 Bind:后端绑定

.bind 文件将抽象 .dspec 概念映射到具体后端 API:

backend baremetal for device ftgpio010 {
  type DeviceState → "struct ftgpio_dev"
  map MmioRead(B4) → "mmio_read32"
  export ack_irq as "ftgpio_ack_irq"
}

不同后端重用相同的 .dspec

backend linux for device ftgpio010 {
  type DeviceState → "struct ftgpio_gpio"
  callback irq_chip.irq_ack = ack_irq
  map MmioRead(B4) → "readl"
}

4.5 可验证属性

形式化规约支持六个验证属性:


5. 代码生成

5.1 架构

代码生成器消费元组 (FormalRIS, DeviceSpec, Bind, Facts)

formal_ris.json + device.dspec + target.bind + source.facts
    → [Generator] → target.c

5.2 用户态测试工具

测试工具后端生成带假 MMIO 内存和追踪日志的自包含 C 程序。用作运行时验证目标:追踪输出可直接与提取的 RIS 比较以验证完整性、值正确性和顺序一致性,无需内核基础设施或物理硬件。

5.3 裸机 C

裸机后端生成无可移植操作系统依赖的代码。生成的函数使用 uintptr_t 作为 MMIO 基址,使用 *(volatile uint32_t *)addr 进行直接寄存器访问。

5.4 生成就绪度评分

metrics.py 模块计算就绪度评分:

generation_readiness:
  ris_quality: 0.92
  backend_bare_metal_ready: true
  backend_linux_ready: false
  llm_synthesis_ready: true

6. 评估

6.1 提取结果

在 17 个 Linux 驱动上的评估:

指标
驱动总数 17
RIS 操作总数 461
符号寄存器解析 129
RMW 检测 77
分支条件捕获 62
寄存器映射项 100
平均提取时间 < 0.5s

6.2 案例:virtio_mmio 端到端验证

属性 结果
P1: 地址覆盖 100%(21/21)
P2: 值正确性 100%
P3: 顺序一致性 100%
P4: 寄存器覆盖 100%(14/14)

裸机驱动在 QEMU 下端到端验证:21/21 操作完全匹配。

6.3 局限


7. 相关工作

Perry [5] 使用符号执行探索驱动路径。Termite [6] 从硬件规约合成驱动。C2Rust [7,8] 源码级 C-to-Rust 翻译。&inator [9](PLDI 2026)SMT 约束求解 C-to-Rust 接口翻译。Coccinelle [14] 语义补丁。SLAM/BLAST [15,16] 模型检验驱动验证。

reharness 的不同之处:聚焦于语义提取而非验证,使用污点追踪而非谓词抽象,生成跨后端可移植的规约。


8. 讨论

8.1 设计空间

reharness 占据独特位置:AST 精度足以处理真实驱动,轻量足以实用,结构化足以支持多目标生成和 LLM 辅助合成

8.2 多层规约分离

.ris / .dspec / .bind / .facts 分离是关键架构洞察。每个层回答关于驱动的不同问题。新后端的代码生成器只需要新的 .bind 文件;.ris.dspec 无需修改即可重用。

8.3 未来方向

DMA 与异步 I/O、跨翻译单元分析、结合 &inator 风格分析的 Rust 目标、集成测试工具后端到 CI 流水线。


9. 结论

我们提出了 reharness,通过三个贡献推动驱动提取与翻译的发展:(1)基于 AST 的提取与污点追踪,解决正则方法的四个根本局限;(2)四层形式化规约语言,清晰分离硬件行为、设备语义、后端绑定和源级上下文;(3)多后端代码生成与确定性及 LLM 辅助合成的就绪度评分。17 个真实 Linux 驱动的评估证明了 reharness 超越先前单驱动演示的可扩展性。virtio-mmio 驱动端到端 QEMU 验证的 100% 地址覆盖和顺序一致性提供了有力证据,表明提取的规约不仅在语法上正确,而且在语义上忠实于原始驱动行为。

参考文献

[1-16] 见英文版。