count_instructions/count_instructions.rs
1//! Counts the instructions executed within a single function.
2//!
3//! When developing constant-time code, it can be helpful to validate that several executions of a
4//! given function have the same number of instructions, and that the same instructions were used.
5//!
6//! The approach used by this crate is to single-step the function using the operating system debug
7//! API, optionally recording the address of each instruction. This is currently implemented only
8//! for Linux; for other operating systems, it will fail with an error.
9//!
10//! Using the debug API to single-step the function has several drawbacks:
11//!
12//! - It can be very slow, especially when not compiled in release mode;
13//! - It cannot be used while another debugger is attached to the process;
14//! - Its use might be restricted by several system hardening mechanisms.
15//!
16//! On the other hand, using the debug API has the advantage of tracing the real code executed by
17//! the CPU, as generated by the compiler, instead of symbolic execution of the source code, or
18//! emulation on another architecture.
19//!
20//! # Examples
21//!
22//! ```
23//! # #[cfg(target_os = "linux")]
24//! # {
25//! use count_instructions::count_instructions;
26//!
27//! # #[inline(never)]
28//! fn add(left: usize, right: usize) -> usize {
29//! left + right
30//! }
31//!
32//! let mut count = 0;
33//! let result = count_instructions(|| add(2, 2), |instruction| count += 1)?;
34//! assert_eq!(result, 4);
35//! assert_ne!(count, 0);
36//! # }
37//! # Ok::<(), std::io::Error>(())
38//! ```
39//!
40//! For a stronger test, you can store and later compare the instruction addresses:
41//!
42//! ```
43//! # #[cfg(target_os = "linux")]
44//! # {
45//! use count_instructions::count_instructions;
46//!
47//! # #[inline(never)]
48//! fn add(left: usize, right: usize) -> usize {
49//! left + right
50//! }
51//!
52//! let mut addresses = Vec::new();
53//! let result = count_instructions(
54//! || add(2, 2),
55//! |instruction| addresses.push(instruction.address())
56//! )?;
57//! assert_eq!(result, 4);
58//! assert!(!addresses.is_empty());
59//! # }
60//! # Ok::<(), std::io::Error>(())
61//! ```
62//!
63//! Note that, due to monomorphization and inlining, a separate monomorphic function must be used
64//! whenever the instruction addresses from more than one execution will be compared later:
65//!
66//! ```
67//! # #[cfg(target_os = "linux")]
68//! # {
69//! use count_instructions::count_instructions;
70//! use count_instructions::Address;
71//!
72//! # #[inline(never)]
73//! fn add(left: usize, right: usize) -> usize {
74//! left + right
75//! }
76//!
77//! #[inline(never)]
78//! fn count(left: usize, right: usize, capacity: usize) -> std::io::Result<Vec<Address>> {
79//! let mut addresses = Vec::with_capacity(capacity);
80//! let result = count_instructions(
81//! || add(left, right),
82//! |instruction| addresses.push(instruction.address())
83//! )?;
84//! assert_eq!(result, 4);
85//! Ok(addresses)
86//! }
87//!
88//! let expected = count(2, 2, 0)?;
89//! let addresses = count(3, 1, expected.len())?;
90//! assert_eq!(addresses, expected);
91//! # }
92//! # Ok::<(), std::io::Error>(())
93//! ```
94
95use std::hint::black_box;
96
97#[cfg(target_os = "linux")]
98mod unix;
99
100/// Represents the address of a machine instruction.
101pub type Address = usize;
102
103/// Information about an instruction executed by the function being traced.
104#[derive(Debug)]
105pub struct Instruction {
106 address: Address,
107}
108
109impl Instruction {
110 #[inline]
111 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
112 fn new(address: Address) -> Self {
113 Self {
114 address,
115 }
116 }
117
118 /// The address of the executed instruction.
119 #[inline]
120 #[must_use]
121 pub fn address(&self) -> Address {
122 self.address
123 }
124}
125
126/// Runs the function being tested, calling a counter function for each instruction executed.
127///
128/// The `counter` will be called once for each instruction executed while running `f`. Note that
129/// `counter` might be called a few more times, immediately before and after running `f`, due to a
130/// few extra instructions necessary to start the trace, copy the result, and stop the trace.
131///
132/// See the top-level crate documentation for examples.
133pub fn count_instructions<F, T, C>(f: F, mut counter: C) -> std::io::Result<T>
134where
135 F: FnOnce() -> T,
136 C: FnMut(&Instruction) + Send,
137{
138 let mut function = Some(f);
139 let mut result = None;
140
141 count_instructions_impl(
142 &mut || {
143 let f = black_box(function.take().unwrap());
144 result = Some(black_box(f()));
145 },
146 &mut counter,
147 )?;
148
149 Ok(result.unwrap())
150}
151
152#[cfg(target_os = "linux")]
153use unix::count_instructions as count_instructions_impl;
154
155#[cfg(not(target_os = "linux"))]
156fn count_instructions_impl(
157 _f: &mut dyn FnMut(),
158 _counter: &mut (dyn FnMut(&Instruction) + Send),
159) -> std::io::Result<()> {
160 Err(std::io::Error::new(
161 std::io::ErrorKind::Unsupported,
162 "instruction tracing not implemented for this platform",
163 ))
164}