CCTLib
Calling-context and data-object attribution library for Intel Pin
deadspy_integration_test.cpp
Go to the documentation of this file.
1 // GoogleTest-driven integration tests for deadspy_client.
2 //
3 // Semantics deadspy measures: a "dead write" is a store to memory that is
4 // later overwritten (in program order along the same execution path) without
5 // any intervening READ of that location. Every non-first store in a
6 // same-address chain with no reads between is dead.
7 //
8 // Test design: build small victim programs under tests/gtest/apps/ with
9 // -O0 and volatile accessors so the compiler does not collapse the write
10 // patterns we are measuring. Run Pin+deadspy_client against each victim,
11 // parse `GrandTotalDead = N` from the tool output. Assert directional
12 // inequalities (TP > TN, size sweep monotone) rather than absolute counts
13 // -- the absolute count is dominated by process startup / libc noise, and
14 // specifying exact numbers would make the test brittle across libc versions.
15 //
16 // True-positive victims (TP): programs deliberately containing back-to-back
17 // stores to the same address with no read between. deadspy should report
18 // substantially more dead writes than a TN control that reads between the
19 // stores. The GAP between TP and TN is what we assert; it should scale
20 // with the workload the victim performs.
21 
22 #include <cerrno>
23 #include <cstdio>
24 #include <cstdlib>
25 #include <cstring>
26 #include <fcntl.h>
27 #include <fstream>
28 #include <regex>
29 #include <sstream>
30 #include <string>
31 #include <sys/wait.h>
32 #include <unistd.h>
33 #include <vector>
34 
35 #include <gtest/gtest.h>
36 
37 namespace {
38 
39 std::string env(const char* name) { const char* v = getenv(name); return v ? v : ""; }
40 std::string cctlib_root() { std::string r = env("CCTLIB_ROOT"); return r.empty() ? "../.." : r; }
41 std::string pin_root() { return env("PIN_ROOT"); }
42 
43 int run_pin(const std::string& tool, const std::vector<std::string>& args) {
44  std::string pin = pin_root() + "/pin";
45  std::vector<char*> argv;
46  argv.push_back(const_cast<char*>(pin.c_str()));
47  argv.push_back(const_cast<char*>("-t"));
48  argv.push_back(const_cast<char*>(tool.c_str()));
49  argv.push_back(const_cast<char*>("--"));
50  for (auto& a : args) argv.push_back(const_cast<char*>(a.c_str()));
51  argv.push_back(nullptr);
52 
53  pid_t pid = fork();
54  if (pid < 0) return -1;
55  if (pid == 0) {
56  if (!env("CCTLIB_TEST_VERBOSE").size()) {
57  int devnull = open("/dev/null", O_WRONLY);
58  if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); }
59  }
60  execv(pin.c_str(), argv.data());
61  _exit(127);
62  }
63  int status = 0;
64  if (waitpid(pid, &status, 0) < 0) return -1;
65  if (WIFEXITED(status)) return WEXITSTATUS(status);
66  return -2;
67 }
68 
69 std::string find_newest(const std::string& dir, const std::string& prefix) {
70  std::string cmd = "ls -t " + dir + "/" + prefix + "* 2>/dev/null | head -1";
71  FILE* p = popen(cmd.c_str(), "r");
72  if (!p) return {};
73  char buf[4096]; std::string out;
74  if (fgets(buf, sizeof(buf), p)) out = buf;
75  pclose(p);
76  if (!out.empty() && out.back() == '\n') out.pop_back();
77  return out;
78 }
79 std::string read_file(const std::string& path) {
80  std::ifstream in(path); std::stringstream ss; ss << in.rdbuf(); return ss.str();
81 }
82 void cleanup(const std::string& dir, const std::string& prefix) {
83  std::string cmd = "rm -f " + dir + "/" + prefix + "*"; (void)system(cmd.c_str());
84 }
85 
86 // Parses "GrandTotalDead = <N> = <pct>%" from a deadspy report. Returns -1
87 // if the line is not present (which indicates a broken run).
88 long parse_grand_total_dead(const std::string& content) {
89  std::regex re(R"(GrandTotalDead = (\d+))");
90  std::smatch m;
91  if (!std::regex_search(content, m, re)) return -1;
92  return std::stol(m[1]);
93 }
94 
95 long parse_grand_total_writes(const std::string& content) {
96  std::regex re(R"(GrandTotalWrites = (\d+))");
97  std::smatch m;
98  if (!std::regex_search(content, m, re)) return -1;
99  return std::stol(m[1]);
100 }
101 
102 class DeadspyIntegration : public ::testing::Test {
103  protected:
104  std::string root_;
105  std::string tool_;
106  void SetUp() override {
107  root_ = cctlib_root();
108  ASSERT_FALSE(pin_root().empty()) << "PIN_ROOT required";
109  tool_ = root_ + "/clients/obj-intel64/deadspy_client.so";
110  ASSERT_EQ(0, access(tool_.c_str(), F_OK)) << tool_ << " missing";
111  ASSERT_EQ(0, chdir(root_.c_str())) << "chdir(" << root_ << ") failed";
112  }
113 
114  // Run deadspy on `victim`, returning the parsed GrandTotalDead. Also
115  // populates `total_writes` so callers can inspect the ratio.
116  long run_and_parse_dead(const std::string& victim, long* total_writes = nullptr) {
117  cleanup(root_, "deadspy.out.");
118  int rc = run_pin(tool_, {victim});
119  EXPECT_EQ(0, rc) << "deadspy on " << victim << " returned " << rc;
120  std::string out = find_newest(root_, "deadspy.out.");
121  EXPECT_FALSE(out.empty()) << "no deadspy.out.* file produced";
122  std::string content = read_file(out);
123  if (total_writes) *total_writes = parse_grand_total_writes(content);
124  return parse_grand_total_dead(content);
125  }
126 };
127 
128 // Sanity: deadspy runs cleanly on an existing built app and reports a
129 // non-zero total-writes count. This is the same regression coverage that
130 // the top-level TEST5/TEST6 give, but wrapped in gtest.
131 TEST_F(DeadspyIntegration, RunsCleanlyOnLs) {
132  long writes = 0;
133  long dead = run_and_parse_dead("/bin/ls", &writes);
134  ASSERT_GE(dead, 0) << "deadspy report missing GrandTotalDead";
135  EXPECT_GT(writes, 0);
136 }
137 
138 // Existing deadWrites.exe victim (three memset-3-loops-back-to-back).
139 // Since the workload deliberately overwrites the same buffer three times,
140 // dead writes should be a healthy fraction of total writes.
141 TEST_F(DeadspyIntegration, DeadWritesAppReportsDeads) {
142  long writes = 0;
143  long dead = run_and_parse_dead(root_ + "/apps/obj-intel64/deadWrites.exe", &writes);
144  ASSERT_GE(dead, 0);
145  EXPECT_GT(dead, 0) << "deadWrites.exe should produce some dead writes";
146 }
147 
148 // TP vs TN comparison: the TP victim writes to each buffer cell twice with
149 // no read between; the TN victim reads between the two writes so nothing
150 // is dead. Startup / libc noise is the same order of magnitude in both,
151 // but with WORK_COUNT=10000 8-byte pairs the TP victim adds
152 // ~80K bytes of dead-write signal on top -- well above the libc-startup
153 // noise floor of ~15K bytes we observe empirically on this system.
154 // The test asserts (TP-TN)/TN as a fraction, not a raw count, to survive
155 // libc version drift.
156 TEST_F(DeadspyIntegration, TruePositiveMoreDeadsThanTrueNegative) {
157  long tp = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tp_simple");
158  long tn = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tn_simple");
159  ASSERT_GE(tp, 0);
160  ASSERT_GE(tn, 0);
161  // TP adds at least ~10000 * 8 = 80000 dead-byte events from the
162  // workload. Require TP to exceed TN by at least half that (40000)
163  // to allow for scheduling jitter and small libc-startup differences.
164  EXPECT_GT(tp - tn, 40000)
165  << "TP=" << tp << " TN=" << tn
166  << " -- expected TP-TN > 40000 dead-write bytes from the workload";
167 }
168 
169 // Multi-size TP: writes at byte/word/dword/qword granularity to different
170 // buffers. WORK_COUNT=5000 iterations x (1+2+4+8)=15 bytes/iter of dead
171 // writes = 75000 dead-write bytes above baseline.
172 TEST_F(DeadspyIntegration, MultiSizeTruePositiveExceedsTrueNegative) {
173  long tp = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tp_sizes");
174  long tn = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tn_simple");
175  ASSERT_GE(tp, 0);
176  ASSERT_GE(tn, 0);
177  EXPECT_GT(tp - tn, 30000)
178  << "TP-multi-size=" << tp << " TN=" << tn;
179 }
180 
181 // Symbol-attribution test: deadspy's per-context report should list the
182 // victim's `store8` helper as a KILLING context (i.e. the store that turned
183 // out to be dead). Verifies not just "dead writes are counted" but
184 // "deadspy attributes them to the right source function". Uses grep because
185 // the report format is text.
186 TEST_F(DeadspyIntegration, TPReportAttributesToStore8) {
187  cleanup(root_, "deadspy.out.");
188  int rc = run_pin(tool_, {root_ + "/tests/gtest/obj/apps/deadspy_tp_simple"});
189  ASSERT_EQ(0, rc);
190  std::string out = find_newest(root_, "deadspy.out.");
191  ASSERT_FALSE(out.empty());
192  // Count occurrences of `store8` referencing the tp_simple source file.
193  // Any hit means deadspy successfully attributed a dead-write context
194  // to the victim's helper, which is what we want to guarantee.
195  std::string cmd = "grep -c 'store8:.*deadspy_tp_simple.c' " + out;
196  FILE* p = popen(cmd.c_str(), "r");
197  ASSERT_NE(p, nullptr);
198  char buf[64]; std::string s;
199  if (fgets(buf, sizeof(buf), p)) s = buf;
200  pclose(p);
201  long hits = s.empty() ? 0 : std::stol(s);
202  EXPECT_GT(hits, 0) << "deadspy report has no context mentioning store8 "
203  "in deadspy_tp_simple.c";
204 }
205 
206 // ------------------------------------------------------------------
207 // ISA breadth tests. Each victim under tests/gtest/apps/isa/deadspy_*
208 // exercises deadspy against a specific x86 write pattern (SSE 16B,
209 // AVX 32B, partial overlap byte-in-qword, rep stosq, atomic xchg,
210 // various addressing modes). Parameterized so it's trivial to add
211 // more instruction classes.
212 //
213 // Baseline: deadspy_tp_minimal (single-iteration workload). Its
214 // GrandTotalDead ~= libc-startup noise + 1. Every ISA victim's
215 // (V - baseline) closely matches its designed dead-byte workload.
216 
217 struct IsaVictim {
218  const char* name;
219  long min_extra_dead_bytes; // vs tp_minimal baseline
220 };
221 
223  public ::testing::WithParamInterface<IsaVictim> {};
224 
225 TEST_P(DeadspyIsa, ExceedsBaseline) {
226  long baseline = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tp_minimal");
227  long isa = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/isa/" + GetParam().name);
228  ASSERT_GE(baseline, 0);
229  ASSERT_GE(isa, 0);
230  EXPECT_GT(isa - baseline, GetParam().min_extra_dead_bytes)
231  << "victim=" << GetParam().name
232  << " isa-count=" << isa
233  << " baseline=" << baseline
234  << " threshold=" << GetParam().min_extra_dead_bytes;
235 }
236 
238  IsaBreadth, DeadspyIsa,
239  ::testing::Values(
240  // 10000 iters * 16B dead per iter = 160000 dead bytes expected.
241  IsaVictim{"deadspy_sse16_tp", 100000},
242  // 10000 iters * 32B dead per iter = 320000 dead bytes expected.
243  IsaVictim{"deadspy_avx32_tp", 200000},
244  // 20000 iters * 1B dead (byte-in-qword partial overlap) = 20000
245  // dead bytes. Deadspy's per-byte shadow must track the byte-0
246  // overlap correctly to see this.
247  IsaVictim{"deadspy_partial_qword_then_byte_tp", 15000},
248  // Symmetric partial overlap at byte offset 7 (high byte of qword).
249  IsaVictim{"deadspy_partial_qword_then_byte_high_tp", 15000},
250  // 100 iters * 512 qwords * 8B = 409600 dead bytes from rep stosq.
251  IsaVictim{"deadspy_repstos_tp", 200000},
252  // NOTE: deadspy_xchg_tp used to live here with a 100K threshold. That
253  // was wrong -- XCHG is RMW so pairs produce ZERO dead writes at -O2.
254  // The test only passed because -O0 stack noise from unused locals
255  // dominated. Now reclassified as deadspy_xchg_tn.c and asserted
256  // separately by DeadspyIsa.XchgIsNotFalselyDead below.
257  // 20000 iters * 8B dead per iter = 160000 dead bytes with SIB
258  // (register+idx*scale) addressing.
259  IsaVictim{"deadspy_addressing_tp", 100000},
260  // Cross-page qword: 10000 iters * 8B dead per iter = 80000 dead
261  // bytes, all straddling a 4KB page boundary. Exercises the
262  // 2-level shadow-page table's cross-boundary handling.
263  IsaVictim{"deadspy_cross_page_qword_tp", 40000},
264  // PUSH/POP dead pattern: 10000 iters * 8B dead per iter, via
265  // rsp-relative pushes with an rsp adjustment between them.
266  // Exercises implicit RSP arithmetic and rsp-relative addressing.
267  IsaVictim{"deadspy_pushpop_dead_tp", 40000},
268  // Non-temporal MOVNTI stores. 10000 * 8B dead per iter.
269  IsaVictim{"deadspy_movnti_tp", 40000},
270  // PREFETCH must not clear the shadow "was-written" bit. Prime +
271  // (store, prefetch, store) per iter -> 20000 * 16B dead expected.
272  // If a regression makes prefetch behave like a real read, the
273  // dead count collapses to baseline (<20K) and this trips.
274  IsaVictim{"deadspy_prefetch_tp", 200000}),
275  [](const testing::TestParamInfo<IsaVictim>& info) {
276  return info.param.name;
277  });
278 
279 // LOCK CMPXCHG negative test: cmpxchg is a read-modify-write. The read
280 // portion of the second cmpxchg CLEARS deadspy's shadow "was written"
281 // bit that the first cmpxchg set. Deadspy should therefore report
282 // essentially NO additional dead writes from this workload vs baseline.
283 //
284 // This is a correctness/false-positive test: if deadspy STARTS reporting
285 // large numbers of dead writes here, it likely means it lost the RMW
286 // read-then-write ordering and is now over-reporting on atomic ops --
287 // bad for downstream users of the tool.
288 //
289 // Same principle applies to XCHG (see XchgIsNotFalselyDead below).
290 TEST_F(DeadspyIsa, CmpxchgIsNotFalselyDead) {
291  long baseline = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tp_minimal");
292  long isa = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/isa/deadspy_cmpxchg_tn");
293  ASSERT_GE(baseline, 0);
294  ASSERT_GE(isa, 0);
295  // Allow at most 5000 dead bytes above baseline. The workload has
296  // 10000 cmpxchg pairs; if deadspy were falsely reporting them all as
297  // dead we'd see ~80000 extra dead bytes.
298  EXPECT_LT(isa - baseline, 5000)
299  << "cmpxchg pair falsely reported as dead. isa=" << isa
300  << " baseline=" << baseline;
301 }
302 
303 // LOCK XCHG negative test: XCHG r, m is a read-modify-write per Intel
304 // SDM. Pin correctly classifies its memory operand as read+written; the
305 // read side clears deadspy's shadow before the write side checks it,
306 // so two consecutive XCHGs to the same address produce NO dead writes.
307 //
308 // If a Pin update reclassifies XCHG as write-only, or a deadspy change
309 // loses the read-before-write callback ordering, we'd start seeing
310 // ~8B * 20000 = 160000 extra dead bytes here. Threshold at 5000 (same
311 // as CMPXCHG) catches that regression while tolerating baseline noise.
312 TEST_F(DeadspyIsa, XchgIsNotFalselyDead) {
313  long baseline = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/deadspy_tp_minimal");
314  long isa = run_and_parse_dead(root_ + "/tests/gtest/obj/apps/isa/deadspy_xchg_tn");
315  ASSERT_GE(baseline, 0);
316  ASSERT_GE(isa, 0);
317  EXPECT_LT(isa - baseline, 5000)
318  << "xchg pair falsely reported as dead. isa=" << isa
319  << " baseline=" << baseline;
320 }
321 
322 } // namespace
long run_and_parse_dead(const std::string &victim, long *total_writes=nullptr)
static uint64_t buf[ITERS]
volatile uint8_t status
Definition: cctlib.cpp:230
int run_pin(const std::string &tool, const std::vector< std::string > &args)
std::string find_newest(const std::string &dir, const std::string &prefix)
INSTANTIATE_TEST_SUITE_P(IsaBreadth, DeadspyIsa, ::testing::Values(IsaVictim{"deadspy_sse16_tp", 100000}, IsaVictim{"deadspy_avx32_tp", 200000}, IsaVictim{"deadspy_partial_qword_then_byte_tp", 15000}, IsaVictim{"deadspy_partial_qword_then_byte_high_tp", 15000}, IsaVictim{"deadspy_repstos_tp", 200000}, IsaVictim{"deadspy_addressing_tp", 100000}, IsaVictim{"deadspy_cross_page_qword_tp", 40000}, IsaVictim{"deadspy_pushpop_dead_tp", 40000}, IsaVictim{"deadspy_movnti_tp", 40000}, IsaVictim{"deadspy_prefetch_tp", 200000}), [](const testing::TestParamInfo< IsaVictim > &info) { return info.param.name;})
void cleanup(const std::string &dir, const std::string &prefix)
int a[1000]
Definition: testArray.c:5