diff --git a/valgrind/fjalar/kvasir/dyncomp_translate.c b/valgrind/fjalar/kvasir/dyncomp_translate.c index 2341be5d..fa63add0 100644 --- a/valgrind/fjalar/kvasir/dyncomp_translate.c +++ b/valgrind/fjalar/kvasir/dyncomp_translate.c @@ -107,6 +107,36 @@ static void setHelperAnns_DC ( DCEnv* dce, IRDirty* di ) { di->fxState[1].repeatLen = 0; } +// Merge the tags vatom1 and vatom2, for an operation whose result +// gets tag 0 (such as a comparison). The merge is a dirty call +// rather than a clean one. VEX would remove a clean call as dead +// code whenever the operations that use the result discard its tag +// (for example, a shift discards the tag of its shift amount, and a +// unary operation that gives tag 0 discards the tag of its operand), +// and the merge would be lost. +static void mergeTagsReturn0_DC ( DCEnv* dce, + IRAtom* vatom1, IRAtom* vatom2 ) { + IRDirty* di = unsafeIRDirty_0_N(2, + "MC_(helperc_MERGE_TAGS_RETURN_0)", + &MC_(helperc_MERGE_TAGS_RETURN_0), + mkIRExprVec_2( vatom1, vatom2 )); + setHelperAnns_DC( dce, di ); + stmt_DC('V', dce, IRStmt_Dirty(di)); +} + +// 'Anchor' the tag vatom by passing it to a NOP dirty call, so that +// the IR optimizer does not delete the computation of vatom, along +// with any tag merges (clean helper calls) that it performs, even +// though the caller discards vatom. +static void anchorTag_DC ( DCEnv* dce, IRAtom* vatom ) { + IRDirty* di = unsafeIRDirty_0_N(1, + "MC_(helperc_TAG_NOP)", + &MC_(helperc_TAG_NOP), + mkIRExprVec_1( vatom )); + setHelperAnns_DC( dce, di ); + stmt_DC('V', dce, IRStmt_Dirty(di)); +} + // A PUT stores a value into the guest state void do_shadow_PUT_DC ( DCEnv* dce, Int offset, IRAtom* atom, IRAtom* vatom ) @@ -256,8 +286,6 @@ IRAtom* handleCCall_DC ( DCEnv* dce, IRAtom* first = expr2tags_DC(dce, exprvec[0]); Int i; IRAtom* cur; - IRDirty* di; - IRTemp datatag; for (i = 1; exprvec[i]; i++) { tl_assert(i < 32); @@ -271,19 +299,7 @@ IRAtom* handleCCall_DC ( DCEnv* dce, } else { /* merge the tags of first and current arguments */ cur = expr2tags_DC(dce, exprvec[i]); - - // (comment added 2006) - // TODO: Why is this dirty rather than clean? - pgbovine - // Because it has side effects? - smcc - datatag = newTemp(dce->mce, Ity_Word, DC); - di = unsafeIRDirty_1_N(datatag, - 2, - "MC_(helperc_MERGE_TAGS_RETURN_0)", - &MC_(helperc_MERGE_TAGS_RETURN_0), - mkIRExprVec_2( first, cur )); - - setHelperAnns_DC( dce, di ); - stmt_DC('V', dce, IRStmt_Dirty(di)); + mergeTagsReturn0_DC(dce, first, cur); } } // Return the tag of the first argument, if there is one @@ -1724,6 +1740,11 @@ IRAtom* expr2tags_Binop_DC ( DCEnv* dce, } } + if (helper == &MC_(helperc_MERGE_TAGS_RETURN_0)) { + mergeTagsReturn0_DC(dce, vatom1, vatom2); + return IRExpr_Const(IRConst_UWord(0)); + } + // Let's try a clean call. It seems to be correct // because of the fact that merging the same 2 things more than // once (in close proximity) doesn't hurt @@ -1754,13 +1775,90 @@ IRAtom* expr2tags_Binop_DC ( DCEnv* dce, } static -IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) +IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IROp op, IRAtom* atom ) { IRAtom* vatom = expr2tags_DC( dce, atom ); tl_assert(isOriginalAtom_DC(dce,atom)); - // Do nothing with unary ops. Just evaluate the - // sub-expression and return it: + switch (op) { + + // ---------------------------------------------------------- + // The result is a boolean, or a mask or count computed from + // the bits of the operand, rather than a value of the same kind + // as the operand. As for binary comparisons (see + // expr2tags_Binop_DC), the result gets tag 0. + // ---------------------------------------------------------- + + case Iop_Clz32: + case Iop_Clz64: + case Iop_ClzNat32: // only used by ppc + case Iop_ClzNat64: // only used by ppc + case Iop_Clz16x4: // only used by arm + case Iop_Clz16x8: // only used by arm ppc arm64 mips s390 + case Iop_Clz32x2: // only used by arm + case Iop_Clz32x4: // only used by arm ppc arm64 mips s390 + case Iop_Clz64x2: // only used by ppc mips s390 + case Iop_Clz8x16: // only used by arm ppc arm64 mips s390 + case Iop_Clz8x8: // only used by arm + case Iop_Cls16x4: // only used by arm + case Iop_Cls16x8: // only used by arm arm64 + case Iop_Cls32x2: // only used by arm + case Iop_Cls32x4: // only used by arm arm64 + case Iop_Cls8x16: // only used by arm arm64 + case Iop_Cls8x8: // only used by arm + case Iop_CmpNEZ128x1: // unused + case Iop_CmpNEZ16: // unused + case Iop_CmpNEZ16x16: // unused + case Iop_CmpNEZ16x2: // unused + case Iop_CmpNEZ16x4: // only used by arm + case Iop_CmpNEZ16x8: // only used by arm + case Iop_CmpNEZ32: // unused + case Iop_CmpNEZ32x2: // only used by arm + case Iop_CmpNEZ32x4: // only used by arm + case Iop_CmpNEZ32x8: // unused + case Iop_CmpNEZ64: // unused + case Iop_CmpNEZ64x2: // only used by arm + case Iop_CmpNEZ64x4: // unused + case Iop_CmpNEZ8: // unused + case Iop_CmpNEZ8x16: // only used by arm + case Iop_CmpNEZ8x32: // unused + case Iop_CmpNEZ8x4: // unused + case Iop_CmpNEZ8x8: // only used by arm + case Iop_CmpwNEZ32: // unused + case Iop_CmpwNEZ64: // only used by arm + case Iop_Cnt8x16: // only used by arm arm64 + case Iop_Cnt8x8: // only used by arm + case Iop_Ctz16x8: + case Iop_Ctz8x16: + case Iop_Ctz32: + case Iop_CtzNat32: + case Iop_Ctz32x4: + case Iop_Ctz64: + case Iop_CtzNat64: + case Iop_Ctz64x2: + case Iop_GetMSBs8x16: + case Iop_GetMSBs8x8: + case Iop_PopCount32: // only used by ppc + case Iop_PopCount64: // only used by ppc + // The result discards vatom, so anchor it to preserve the tag + // merges that computed it, such as the merge of a and b in + // 'ctz(a + b)'. In dyncomp_dataflow_only_mode and + // dyncomp_dataflow_comparisons_mode, no binary operation merges + // with a clean call (comparisons merge with a dirty call; see + // mergeTagsReturn0_DC), so the anchor is not worth the cost of + // a helper call. + if (atom->tag != Iex_Const + && !dyncomp_dataflow_only_mode + && !dyncomp_dataflow_comparisons_mode) { + anchorTag_DC(dce, vatom); + } + return IRExpr_Const(IRConst_UWord(0)); + + // ---------------------------------------------------------- + // Return the tag of the operand: + // ---------------------------------------------------------- + + default: // pgbovine: Actually, when you widen stuff, don't you want to // create new tags for the new bytes and merge them? // But you can't do that because you only have the word-sized @@ -1769,7 +1867,8 @@ IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) // we only consider the tag of the first bytes of each // operand anyways. // - // For documentation purposes, here is a list of all the unary ops. + // For documentation purposes, here is a list of all the unary ops + // that return the tag of the operand. // Iop_128HIto64 // Iop_128to64 // Iop_16HIto8 @@ -1824,53 +1923,6 @@ IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) // Iop_BCDtoDPB: // only used by ppc // Iop_BCD128toI128S: // only used by ppc // Iop_CipherSV128: // only used by ppc - // Iop_Cls16x4: // only used by arm - // Iop_Cls16x8: // only used by arm arm64 - // Iop_Cls32x2: // only used by arm - // Iop_Cls32x4: // only used by arm arm64 - // Iop_Cls8x16: // only used by arm arm64 - // Iop_Cls8x8: // only used by arm - // Iop_Clz32 - // Iop_Clz64 - // Iop_ClzNat32 // only used by ppc - // Iop_ClzNat64 // only used by ppc - // Iop_Clz16x4: // only used by arm - // Iop_Clz16x8: // only used by arm ppc arm64 mips s390 - // Iop_Clz32x2: // only used by arm - // Iop_Clz32x4: // only used by arm ppc arm64 mips s390 - // Iop_Clz64x2: // only used by ppc mips s390 - // Iop_Clz8x16: // only used by arm ppc arm64 mips s390 - // Iop_Clz8x8: // only used by arm - // Iop_CmpNEZ128x1: // unused - // Iop_CmpNEZ16: // unused - // Iop_CmpNEZ16x16: // unused - // Iop_CmpNEZ16x2: // unused - // Iop_CmpNEZ16x4: // only used by arm - // Iop_CmpNEZ16x8: // only used by arm - // Iop_CmpNEZ32: // unused - // Iop_CmpNEZ32x2: // only used by arm - // Iop_CmpNEZ32x4: // only used by arm - // Iop_CmpNEZ32x8: // unused - // Iop_CmpNEZ64: // unused - // Iop_CmpNEZ64x2: // only used by arm - // Iop_CmpNEZ64x4: // unused - // Iop_CmpNEZ8: // unused - // Iop_CmpNEZ8x16: // only used by arm - // Iop_CmpNEZ8x32: // unused - // Iop_CmpNEZ8x4: // unused - // Iop_CmpNEZ8x8: // only used by arm - // Iop_CmpwNEZ32: // unused - // Iop_CmpwNEZ64: // only used by arm - // Iop_Cnt8x16: // only used by arm arm64 - // Iop_Cnt8x8: // only used by arm - // Iop_Ctz16x8: - // Iop_Ctz8x16: - // Iop_Ctz32: - // Iop_CtzNat32: - // Iop_Ctz32x4: - // Iop_Ctz64: - // Iop_CtzNat64: - // Iop_Ctz64x2: // Iop_D128HItoD64: // only used by ppc s390 // Iop_D128LOtoD64: // only used by ppc s390 // Iop_D32toD64: // only used by ppc s390 @@ -1901,8 +1953,6 @@ IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) // Iop_F32toI32Ux2_RZ: // only used by arm mips // Iop_F32toI32Ux4_RZ: // only used by arm mips // Iop_F64toF128: // only used by s390 - // Iop_GetMSBs8x16 - // Iop_GetMSBs8x8 // Iop_I32StoD128: // only used by s390 // Iop_I32StoD64: // only used by s390 // Iop_I32StoF128: // only used by s390 @@ -1947,8 +1997,6 @@ IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) // Iop_Not64: // Iop_NotV128: // Iop_NotV256: - // Iop_PopCount32: // only used by ppc - // Iop_PopCount64: // only used by ppc // Iop_PwAddL16Sx4: // only used by arm // Iop_PwAddL16Sx8: // only used by arm // Iop_PwAddL16Ux4: // only used by arm @@ -2050,7 +2098,8 @@ IRExpr* expr2tags_Unop_DC ( DCEnv* dce, IRAtom* atom ) // Iop_Widen8Sto16x8: // only used by arm // Iop_Widen8Uto16x8: // only used by arm - return vatom; + return vatom; + } } /* @@ -2374,7 +2423,7 @@ IRExpr* expr2tags_DC ( DCEnv* dce, IRExpr* e ) ); case Iex_Unop: - return expr2tags_Unop_DC( dce, e->Iex.Unop.arg ); + return expr2tags_Unop_DC( dce, e->Iex.Unop.op, e->Iex.Unop.arg ); case Iex_Load: return expr2tags_LDle_DC( dce, e->Iex.Load.ty, diff --git a/valgrind/fjalar/tests/dyncomp-comparability-test.sh b/valgrind/fjalar/tests/dyncomp-comparability-test.sh new file mode 100755 index 00000000..08c1c8e0 --- /dev/null +++ b/valgrind/fjalar/tests/dyncomp-comparability-test.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Tests DynComp's comparability: whether the variables at each exit +# program point are comparable to each other. The expected comparability is +# in dyncomp-comparability.goal, under both --dyncomp-interactions=all and +# --dyncomp-interactions=units. + +set -e + +test_dir="$(cd "$(dirname "$0")" && pwd)" +valgrind="${test_dir}/../../inst/bin/valgrind" + +if [ ! -x "${valgrind}" ]; then + echo "$0: Fjalar is not built; run \"make build\" at the top level." >&2 + exit 2 +fi + +output_dir="$(mktemp -d)" +trap 'rm -rf "${output_dir}"' EXIT + +# Fjalar does not read DWARF 5, and it recognizes a function's entry point +# only at the address in the debugging information, so the executable must not +# be position-independent. +gcc -gdwarf-4 -no-pie -O0 -o "${output_dir}/dyncomp-comparability" \ + "${test_dir}/dyncomp-comparability.c" + +for interactions in all units; do + decls="${output_dir}/dyncomp-comparability.decls" + status=0 + "${valgrind}" --tool=fjalar --dyncomp \ + --dyncomp-interactions="${interactions}" \ + --decls-only --decls-file="${decls}" \ + "${output_dir}/dyncomp-comparability" \ + > "${output_dir}/dyncomp-comparability.out" 2>&1 || status=$? + + if [ "${status}" -ne 0 ]; then + echo "$0: FAILED: under --dyncomp-interactions=${interactions}," \ + "Fjalar exited with status ${status}" >&2 + cat "${output_dir}/dyncomp-comparability.out" >&2 + exit 1 + fi + + # One line per variable at each exit program point, other than main's: + # " ". + actual="${output_dir}/dyncomp-comparability.comparability" + awk '/^ppt / { ppt = $2 } + /^ variable / { var = $2 } + /^ comparability / { if (ppt ~ /:::EXIT/ && ppt !~ /^\.\.main\(/) + print ppt, var, $2 }' "${decls}" > "${actual}" + + if ! diff -u "${test_dir}/dyncomp-comparability.goal" "${actual}" >&2; then + echo "$0: FAILED: under --dyncomp-interactions=${interactions}," \ + "comparability differs from dyncomp-comparability.goal" >&2 + exit 1 + fi +done + +echo "$0: PASSED" diff --git a/valgrind/fjalar/tests/dyncomp-comparability.c b/valgrind/fjalar/tests/dyncomp-comparability.c new file mode 100644 index 00000000..8f7c95ba --- /dev/null +++ b/valgrind/fjalar/tests/dyncomp-comparability.c @@ -0,0 +1,81 @@ +/* + This file is part of Fjalar, a dynamic analysis framework for C/C++ + programs. + + Copyright (C) 2026 University of Washington Computer Science & Engineering Department, + Programming Languages and Software Engineering Group + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. +*/ + +// Functions whose parameters DynComp should make comparable, or not. +// See dyncomp-comparability-test.sh. + +#include + +// a and b are comparable, because they are compared, even though the shift +// discards the tag of the comparison's result; x and the result are +// comparable. +int shift_by_lt(int a, int b, int x) { + return x << (a < b); +} + +// The following functions compute their results with unary operations that +// give tag 0: a count (Ctz) or a mask (GetMSBs) computed from the bits of +// the operand. The result of each function should not be comparable to its +// parameters, but the parameters that interact (via an addition) should be +// comparable to each other. + +// a and b are comparable; the result is comparable to neither. +int ctz_of_sum(unsigned int a, unsigned int b) { + return __builtin_ctz(a + b); +} + +// The result is not comparable to x. +int ctz(unsigned int x) { + return __builtin_ctz(x); +} + +// The result is comparable to neither a nor b. +int movemask(long a, long b) { + __m128i v = _mm_set_epi64x(a, b); + return _mm_movemask_epi8(v); +} + +// a_lo and b_lo are comparable, and a_hi and b_hi are comparable, because +// the two 128-bit integers are compared as a compiler compares two __int128 +// values: a cmp instruction compares the low halves, an sbb instruction +// subtracts the high halves and the borrow, and a setl instruction reads the +// result. Because the cmp and the sbb are in the same superblock, VEX +// computes the borrow with a CmpLT64U of the low halves. Under +// --dyncomp-interactions=units, the tag of the borrow (the comparison's +// result, masked with 1) is 0, so nothing uses the result of the merge. +int less_wide(long a_lo, long a_hi, long b_lo, long b_hi) { + unsigned char result; + long hi = a_hi; + __asm__ volatile ("cmpq %3, %2\n\t" + "sbbq %4, %1\n\t" + "setl %0" + : "=q" (result), "+r" (hi) + : "r" (a_lo), "r" (b_lo), "r" (b_hi) : "cc"); + return result; +} + +int main(void) { + int r = 0; + // j is computed independently of i, so ctz_of_sum's a and b become + // comparable only through the addition within ctz_of_sum. + unsigned int j = 7; + for (int i = 1; i < 5; i++) { + j = j * 3 + 1; + r += shift_by_lt(i, 3, 5); + r += ctz_of_sum(i, j); + r += ctz(i * 4); + r += movemask(i, -i); + } + r += less_wide(20, 21, 22, 23); + return r == 12345; +} diff --git a/valgrind/fjalar/tests/dyncomp-comparability.goal b/valgrind/fjalar/tests/dyncomp-comparability.goal new file mode 100644 index 00000000..2bc0e68a --- /dev/null +++ b/valgrind/fjalar/tests/dyncomp-comparability.goal @@ -0,0 +1,17 @@ +..less_wide():::EXIT0 a_lo 1 +..less_wide():::EXIT0 a_hi 2 +..less_wide():::EXIT0 b_lo 1 +..less_wide():::EXIT0 b_hi 2 +..less_wide():::EXIT0 return 3 +..movemask():::EXIT0 a 1 +..movemask():::EXIT0 b 1 +..movemask():::EXIT0 return 2 +..ctz():::EXIT0 x 1 +..ctz():::EXIT0 return 2 +..ctz_of_sum():::EXIT0 a 1 +..ctz_of_sum():::EXIT0 b 1 +..ctz_of_sum():::EXIT0 return 2 +..shift_by_lt():::EXIT0 a 1 +..shift_by_lt():::EXIT0 b 1 +..shift_by_lt():::EXIT0 x 2 +..shift_by_lt():::EXIT0 return 2