[llvm] [sancov] Add -fsanitize-coverage=trace-args, trace-ret (PR #201410)

Yunseong Kim via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 30 01:07:38 PDT 2026


https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/201410

>From 92883bfc4b9d2307f8659cc8dd81d87ade5acee3 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 23 Aug 2026 12:27:09 +0200
Subject: [PATCH] [SanitizerCoverage] Add trace-args and trace-ret
 instrumentation modes

Add two SanitizerCoverage modes that report the values flowing in and out
of instrumented functions, selected through SanitizerCoverageOptions
(TraceArgs/TraceRet) and, for opt, -sanitizer-coverage-trace-args and
-sanitizer-coverage-trace-ret. trace-args emits a call per source-level
parameter in the entry block, trace-ret one before every return. When
used alone, either mode defaults the SanitizerCoverage level to edge.

  void __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
                                  u64 *offsets, u32 num_fields);
  void __sanitizer_cov_trace_ret (u64 pc, u32 size, u64 val,
                                  u64 *offsets, u32 num_fields);

`pc` is the address of the instrumented function. `num_fields` says what
`val` holds:

- 0: `val` is the value itself, its low `size` bytes, zero-extended. A
  pointer is reported as the address it holds, a floating-point value as
  its bit pattern, and a value wider than 64 bits by its low half. This
  is nearly everything, and it needs no memory of its own.

- non-zero: `val` is the address of a `size` byte object, and `offsets`
  points at a constant table of `num_fields` {byte offset, byte size}
  pairs describing its fields, so a consumer can read them out of it.
  Only an object that already lives in memory is reported this way: a
  pointer to a struct, a by-value struct the ABI passes indirectly, the
  buffer of an indirect struct return. The table is shared by every value
  of the same struct type.

`size == 0` means there was nothing to report: a parameter the optimizer
removed, a void return, a value of a type that cannot be widened.

The consumers are the Linux kernel's KCOV dataflow subsystem and libFuzzer
value profiling, which use the values as coverage: a call reaching a
function with an argument or a struct field value never seen there before
is new coverage, which drives a fuzzer towards the state a bug needs
rather than merely towards the code containing it.

Reporting the value rather than an address to read it from is what keeps
both modes target-independent. The pass never creates memory to report a
value from, so nothing escapes to the stack and no frame is realigned on
the instrumentation's account. Spilling instead would not work
everywhere: the slot escapes, because its address is handed to the
callback, so (K)ASan gives it a redzone and 32-byte alignment; that
realigns the frame, which makes the backend reserve a base pointer. On
x86-64 the base pointer is RBX, and a function whose inline assembly also
claims RBX - a cpuid with an "=b" operand, an rdtsc clobbering it - then
fails to compile with "Interference usage of base pointer/frame pointer".
trace-no-spill.ll pins down that such a function is instrumented like any
other.

Two consequences worth stating:

- A pointer whose pointee has no known field layout - void *, int * - is
  reported as the pointer value rather than as an address to read
  through. Reporting the pointee of an arbitrary pointer is a read the
  instrumentation has no business making.

- A struct the ABI passed or returned in registers has no address, so it
  is reported as one call per register piece, all carrying the same
  arg_idx. Its field values are still observed, they are just not split
  along source field boundaries; a struct passed indirectly still is.

`arg_idx` is the source-level parameter index, recovered by mapping IR
values back through DILocalVariable::getArg(). The frontend assigns those
numbers before ABI lowering, so they keep naming the same source
parameter after the ABI has rewritten the signature: a hidden sret or
`this` pointer is not counted, and a parameter the optimizer removed
entirely is still reported, with size 0, so a consumer sees the full
parameter list. A struct return lowered to an indirect return leaves an
IR function returning void; trace-ret reports the caller-provided sret
buffer rather than dropping the return.

Debug info is not required: without usable debug records the pass reports
the IR arguments positionally, which keeps both modes usable on optimized
or -g-less code at the price of exposing the ABI's view of the arguments.

Points worth noting, all from instrumenting a KASAN kernel with both
modes enabled:

- The calls carry a synthetic !dbg location, from
  InstrumentationIRBuilder. Without one the verifier rejects a function
  built with -g once inlining runs ("inlinable function call ...
  requires a !dbg location").

- Debug records are exempt from SSA dominance, so a record may name a
  value defined after the trace call. Reporting it produced IR failing
  "Instruction does not dominate all uses" and, with the verifier
  disabled as kernel builds do, crashed RegisterCoalescer. Such a
  parameter is reported with size 0, as is a parameter interprocedural
  optimization proved dead and described as poison.

- Only records belonging to the instrumented function may drive
  trace-args. An inlined callee's parameters are numbered too and its
  records can name our arguments -- kmalloc(size, flags) inlined into
  f(ptr, size) leaves #dbg_value(%size, "size", arg: 1) behind -- which
  would otherwise be reported as the caller's first parameter.

- A return forwarding a musttail call is left alone: the call has to stay
  adjacent to the return, so there is nowhere to put the trace call.

Tests cover both callbacks, source-level parameter numbering across sret
insertion and aggregate splitting, field offset tables and their reuse,
structs reported in pieces, indirect struct returns, the reported
parameter list staying complete, the debug-record filters above, the
no-debug-info fallback, and that no configuration emits an alloca.

Clang driver support (-fsanitize-coverage=trace-args,trace-ret) and the
compiler-rt runtime land in follow-up patches.

Assisted-by: Kiro CLI (Claude Opus 5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../llvm/Transforms/Utils/Instrumentation.h   |   2 +
 .../Instrumentation/SanitizerCoverage.cpp     | 535 +++++++++++++++++-
 .../SanitizerCoverage/trace-args-abi.ll       | 127 +++++
 .../SanitizerCoverage/trace-args-dominance.ll |  67 +++
 .../SanitizerCoverage/trace-args-inlined.ll   |  52 ++
 .../SanitizerCoverage/trace-args-no-debug.ll  |  35 ++
 .../SanitizerCoverage/trace-args.ll           | 130 +++++
 .../SanitizerCoverage/trace-no-spill.ll       |  67 +++
 .../SanitizerCoverage/trace-ret.ll            | 138 +++++
 9 files changed, 1152 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll

diff --git a/llvm/include/llvm/Transforms/Utils/Instrumentation.h b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
index 95a985ba3f0c4..8a4324175b075 100644
--- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h
+++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
@@ -163,6 +163,8 @@ struct SanitizerCoverageOptions {
   bool StackDepth = false;
   bool TraceLoads = false;
   bool TraceStores = false;
+  bool TraceArgs = false;
+  bool TraceRet = false;
   bool CollectControlFlow = false;
   bool GatedCallbacks = false;
   int StackDepthCallbackMin = 0;
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index df9675a02824e..a5dca75050378 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -15,14 +15,18 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Analysis/GlobalsModRef.h"
 #include "llvm/Analysis/PostDominators.h"
+#include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/Constant.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DebugProgramInstruction.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
 #include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InstIterator.h"
 #include "llvm/IR/IntrinsicInst.h"
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/LLVMContext.h"
@@ -31,6 +35,7 @@
 #include "llvm/IR/Type.h"
 #include "llvm/IR/ValueSymbolTable.h"
 #include "llvm/Support/CommandLine.h"
+#include "llvm/Support/MathExtras.h"
 #include "llvm/Support/SpecialCaseList.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/TargetParser/Triple.h"
@@ -46,6 +51,8 @@ const char SanCovTracePCIndirName[] = "__sanitizer_cov_trace_pc_indir";
 const char SanCovTracePCName[] = "__sanitizer_cov_trace_pc";
 const char SanCovTracePCEntryName[] = "__sanitizer_cov_trace_pc_entry";
 const char SanCovTracePCExitName[] = "__sanitizer_cov_trace_pc_exit";
+const char SanCovTraceArgsName[] = "__sanitizer_cov_trace_args";
+const char SanCovTraceRetName[] = "__sanitizer_cov_trace_ret";
 const char SanCovTraceCmp1[] = "__sanitizer_cov_trace_cmp1";
 const char SanCovTraceCmp2[] = "__sanitizer_cov_trace_cmp2";
 const char SanCovTraceCmp4[] = "__sanitizer_cov_trace_cmp4";
@@ -156,6 +163,14 @@ static cl::opt<bool> ClGEPTracing("sanitizer-coverage-trace-geps",
                                   cl::desc("Tracing of GEP instructions"),
                                   cl::Hidden);
 
+static cl::opt<bool> ClTraceArgs("sanitizer-coverage-trace-args",
+                                 cl::desc("Tracing of function arguments"),
+                                 cl::Hidden);
+
+static cl::opt<bool> ClTraceRet("sanitizer-coverage-trace-ret",
+                                cl::desc("Tracing of return values"),
+                                cl::Hidden);
+
 static cl::opt<bool>
     ClPruneBlocks("sanitizer-coverage-prune-blocks",
                   cl::desc("Reduce the number of instrumented blocks"),
@@ -226,10 +241,13 @@ SanitizerCoverageOptions OverrideFromCL(SanitizerCoverageOptions Options) {
                                            ClStackDepthCallbackMin.getValue());
   Options.TraceLoads |= ClLoadTracing;
   Options.TraceStores |= ClStoreTracing;
+  Options.TraceArgs |= ClTraceArgs;
+  Options.TraceRet |= ClTraceRet;
   Options.GatedCallbacks |= ClGatedCallbacks;
   if (!Options.TracePCGuard && !Options.TracePC && !Options.TracePCEntryExit &&
       !Options.Inline8bitCounters && !Options.StackDepth &&
-      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores)
+      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores &&
+      !Options.TraceArgs && !Options.TraceRet)
     Options.TracePCGuard = true; // TracePCGuard is default.
   Options.CollectControlFlow |= ClCollectCF;
   return Options;
@@ -265,6 +283,8 @@ class ModuleSanitizerCoverage {
   void InjectTraceForLoadsAndStores(Function &F, ArrayRef<LoadInst *> Loads,
                                     ArrayRef<StoreInst *> Stores);
   void InjectTraceForExits(Function &F);
+  void InjectTraceForArgs(Function &F);
+  void InjectTraceForRet(Function &F);
   void InjectTraceForSwitch(Function &F,
                             ArrayRef<Instruction *> SwitchTraceTargets,
                             Value *&FunctionGateCmp);
@@ -290,6 +310,17 @@ class ModuleSanitizerCoverage {
   std::string getSectionStart(const std::string &Section) const;
   std::string getSectionEnd(const std::string &Section) const;
 
+  /// The `offsets` and `num_fields` arguments of the trace-args/trace-ret
+  /// callbacks: a constant table holding one {byte offset, byte size} pair per
+  /// field of a struct, and the size of that struct. Table is null when the
+  /// field layout is unknown, in which case NumFields and ObjectSize are 0.
+  struct FieldOffsets {
+    Constant *Table = nullptr;
+    unsigned NumFields = 0;
+    uint64_t ObjectSize = 0;
+  };
+  FieldOffsets getFieldOffsets(DIType *Ty);
+
   Module &M;
   DomTreeCallback DTCallback;
   PostDomTreeCallback PDTCallback;
@@ -298,6 +329,7 @@ class ModuleSanitizerCoverage {
   FunctionCallee SanCovTracePCIndir;
   FunctionCallee SanCovTracePC, SanCovTracePCGuard;
   FunctionCallee SanCovTracePCEntry, SanCovTracePCExit;
+  FunctionCallee SanCovTraceArgsFunc, SanCovTraceRetFunc;
   std::array<FunctionCallee, 4> SanCovTraceCmpFunction;
   std::array<FunctionCallee, 4> SanCovTraceConstCmpFunction;
   std::array<FunctionCallee, 5> SanCovLoadFunction;
@@ -321,6 +353,11 @@ class ModuleSanitizerCoverage {
   SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed;
   SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed;
 
+  /// Field offset tables are shared by every value of the same struct type: a
+  /// type used by hundreds of functions must not emit hundreds of identical
+  /// tables.
+  DenseMap<const DICompositeType *, FieldOffsets> FieldOffsetsCache;
+
   SanitizerCoverageOptions Options;
 
   const SpecialCaseList *Allowlist;
@@ -542,6 +579,18 @@ bool ModuleSanitizerCoverage::instrumentModule() {
   SanCovTracePCGuard =
       M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);
 
+  // See the "Argument and return value tracing" section below for the meaning
+  // of the arguments.
+  // void __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
+  //                                 u64 *offsets, u32 num_fields)
+  SanCovTraceArgsFunc =
+      M.getOrInsertFunction(SanCovTraceArgsName, VoidTy, Int64Ty, Int32Ty,
+                            Int32Ty, Int64Ty, PtrTy, Int32Ty);
+  // void __sanitizer_cov_trace_ret(u64 pc, u32 size, u64 val,
+  //                                u64 *offsets, u32 num_fields)
+  SanCovTraceRetFunc = M.getOrInsertFunction(
+      SanCovTraceRetName, VoidTy, Int64Ty, Int32Ty, Int64Ty, PtrTy, Int32Ty);
+
   SanCovStackDepthCallback =
       M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
 
@@ -767,6 +816,12 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) {
 
   if (Options.TracePCEntryExit)
     InjectTraceForExits(F);
+
+  if (Options.TraceArgs)
+    InjectTraceForArgs(F);
+
+  if (Options.TraceRet)
+    InjectTraceForRet(F);
 }
 
 GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
@@ -1266,3 +1321,481 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
       ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
   FunctionCFsArray->setConstant(true);
 }
+
+//===----------------------------------------------------------------------===//
+// Argument and return value tracing.
+//===----------------------------------------------------------------------===//
+//
+// -sanitizer-coverage-trace-args reports every source-level parameter of an
+// instrumented function on entry, -sanitizer-coverage-trace-ret reports the
+// value of every return:
+//
+//   void __sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 size, u64 val,
+//                                   u64 *offsets, u32 num_fields);
+//   void __sanitizer_cov_trace_ret (u64 pc, u32 size, u64 val,
+//                                   u64 *offsets, u32 num_fields);
+//
+// `pc` is the address of the instrumented function. What `val` holds depends on
+// `num_fields`:
+//
+// - `num_fields == 0`: `val` is the value itself, the low `size` bytes of it,
+//   zero-extended. Integers, pointers and floating-point values are reported
+//   this way, which is nearly everything, and needs no memory of its own.
+//
+// - `num_fields != 0`: `val` is the address of a `size` byte object and
+//   `offsets` points at a constant table of `num_fields` {byte offset, byte
+//   size} pairs describing its fields, so that a consumer can read them out of
+//   it. Only an object that already lives in memory is reported this way: a
+//   pointer to a struct, a by-value struct the ABI passes indirectly, or the
+//   buffer of an indirect struct return. The pass never creates memory to
+//   report a value from, so no stack slot escapes and no frame is realigned on
+//   its account.
+//
+// `size == 0` means the pass had nothing to report - a parameter the optimizer
+// removed, a value of a type it cannot widen, a void return.
+//
+// `arg_idx` is the zero-based *source-level* parameter index. Argument values
+// are recovered from debug records rather than from the IR argument list,
+// because the two disagree as soon as the ABI rewrites the signature; see
+// collectSourceParams(). A function without debug records falls back to the IR
+// argument list, which keeps both modes usable on code built without -g, at
+// the price of exposing the ABI's view of the arguments.
+
+/// Peel the typedefs and qualifiers off \p Ty to reach the type they decorate.
+static DIType *stripTypedefsAndQualifiers(DIType *Ty) {
+  while (auto *Derived = dyn_cast_or_null<DIDerivedType>(Ty)) {
+    switch (Derived->getTag()) {
+    case dwarf::DW_TAG_typedef:
+    case dwarf::DW_TAG_const_type:
+    case dwarf::DW_TAG_volatile_type:
+    case dwarf::DW_TAG_restrict_type:
+    case dwarf::DW_TAG_atomic_type:
+    case dwarf::DW_TAG_immutable_type:
+      Ty = Derived->getBaseType();
+      continue;
+    default:
+      return Ty;
+    }
+  }
+  return Ty;
+}
+
+/// Return the aggregate whose fields describe \p Ty: either \p Ty itself, for
+/// a by-value struct, or its pointee, for a pointer to one. Returns null for
+/// any other type.
+static DICompositeType *getTracedStructType(DIType *Ty) {
+  Ty = stripTypedefsAndQualifiers(Ty);
+  if (auto *Derived = dyn_cast_or_null<DIDerivedType>(Ty))
+    if (Derived->getTag() == dwarf::DW_TAG_pointer_type)
+      Ty = stripTypedefsAndQualifiers(Derived->getBaseType());
+
+  auto *Composite = dyn_cast_or_null<DICompositeType>(Ty);
+  if (!Composite)
+    return nullptr;
+  switch (Composite->getTag()) {
+  case dwarf::DW_TAG_structure_type:
+  case dwarf::DW_TAG_class_type:
+    return Composite;
+  default:
+    return nullptr;
+  }
+}
+
+/// Number of parameters \p SP declares in source, or 0 if that is unknown.
+static unsigned getNumDeclaredParams(DISubprogram *SP) {
+  if (!SP || !SP->getType())
+    return 0;
+  // The type array is {return type, parameter types...}.
+  unsigned Size = SP->getType()->getTypeArray().size();
+  return Size ? Size - 1 : 0;
+}
+
+/// Declared type of \p SP's \p Idx-th parameter, numbered from 1 as DWARF
+/// numbers parameters, or null if it is unknown.
+static DIType *getDeclaredParamType(DISubprogram *SP, unsigned Idx) {
+  if (Idx == 0 || Idx > getNumDeclaredParams(SP))
+    return nullptr;
+  return SP->getType()->getTypeArray()[Idx];
+}
+
+/// Declared return type of \p SP, or null if it is unknown or void.
+static DIType *getDeclaredReturnType(DISubprogram *SP) {
+  if (!SP || !SP->getType() || SP->getType()->getTypeArray().empty())
+    return nullptr;
+  return SP->getType()->getTypeArray()[0];
+}
+
+ModuleSanitizerCoverage::FieldOffsets
+ModuleSanitizerCoverage::getFieldOffsets(DIType *Ty) {
+  DICompositeType *Composite = getTracedStructType(Ty);
+  if (!Composite)
+    return {};
+  if (auto It = FieldOffsetsCache.find(Composite);
+      It != FieldOffsetsCache.end())
+    return It->second;
+
+  SmallVector<Constant *, 16> Pairs;
+  for (DINode *Element : Composite->getElements()) {
+    auto *Member = dyn_cast<DIDerivedType>(Element);
+    if (!Member || Member->getTag() != dwarf::DW_TAG_member ||
+        Member->isStaticMember())
+      continue;
+    uint64_t SizeInBits = Member->getSizeInBits();
+    if (!SizeInBits)
+      continue; // A flexible array member or an empty base class.
+    // The callbacks describe fields in bytes, so widen a bitfield to the bytes
+    // that hold it instead of reporting a zero-sized field.
+    uint64_t FirstByte = Member->getOffsetInBits() / 8;
+    uint64_t EndByte = divideCeil(Member->getOffsetInBits() + SizeInBits, 8);
+    Pairs.push_back(ConstantInt::get(Int64Ty, FirstByte));
+    Pairs.push_back(ConstantInt::get(Int64Ty, EndByte - FirstByte));
+  }
+
+  FieldOffsets Offsets;
+  if (!Pairs.empty()) {
+    ArrayType *TableTy = ArrayType::get(Int64Ty, Pairs.size());
+    auto *Table = new GlobalVariable(
+        M, TableTy, /*isConstant=*/true, GlobalVariable::PrivateLinkage,
+        ConstantArray::get(TableTy, Pairs), "__sancov_offsets_");
+    Table->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+    Offsets = {Table, static_cast<unsigned>(Pairs.size() / 2),
+               divideCeil(Composite->getSizeInBits(), 8)};
+  }
+  FieldOffsetsCache[Composite] = Offsets;
+  return Offsets;
+}
+
+namespace {
+
+/// One of the IR values that hold a source-level object, together with its bit
+/// offset in that object. A scalar has a single piece at offset 0.
+using ValuePiece = std::pair<Value *, uint64_t>;
+
+/// A source-level parameter of a function and the entry-block values that hold
+/// it. The ABI may coerce one parameter into several values, each described by
+/// a DW_OP_LLVM_fragment debug record.
+struct SourceParam {
+  DIType *Ty = nullptr;
+  SmallVector<ValuePiece, 2> Pieces;
+  /// Whether Pieces holds fragments of the parameter rather than the whole of
+  /// it. A parameter can lose a fragment (see collectSourceParams), so the
+  /// number of pieces alone does not answer this.
+  bool Fragmented = false;
+  /// Whether Pieces holds incoming Arguments rather than derived values.
+  bool FromArguments = false;
+
+  /// Record that \p V holds this parameter's bits from \p BitOffset on.
+  ///
+  /// A record naming an incoming Argument describes the parameter as it was
+  /// passed, so it wins over records naming values derived from it. Exact
+  /// duplicates are dropped: a repeated record would otherwise make a scalar
+  /// look like a multi-piece aggregate and be needlessly reassembled.
+  void addPiece(Value *V, uint64_t BitOffset, bool IsFragment) {
+    bool IsArgument = isa<Argument>(V);
+    if (IsArgument && !FromArguments) {
+      Pieces.clear();
+      Fragmented = false;
+      FromArguments = true;
+    } else if (!IsArgument && FromArguments) {
+      return;
+    }
+    if (is_contained(Pieces, ValuePiece(V, BitOffset)))
+      return;
+    Pieces.emplace_back(V, BitOffset);
+    Fragmented |= IsFragment;
+  }
+};
+
+using SourceParamMap = SmallDenseMap<unsigned, SourceParam, 8>;
+
+} // namespace
+
+/// Map the source-level parameters of \p F to the values that hold them on
+/// entry, keyed by the DWARF parameter number (counted from 1).
+///
+/// The frontend numbers parameters, in DILocalVariable::getArg(), before ABI
+/// lowering, so the number keeps naming the same source parameter even when
+/// the ABI inserts a hidden argument or splits an aggregate across several.
+/// Debug records are the only link back to that numbering, which is why they,
+/// and not the IR argument list, drive trace-args.
+///
+/// A parameter is left out of \p Params when it has no location the trace call
+/// can use; the caller reports those with a null value pointer.
+static void collectSourceParams(Function &F, SourceParamMap &Params) {
+  BasicBlock &EntryBB = F.getEntryBlock();
+  DISubprogram *SP = F.getSubprogram();
+
+  for (Instruction &I : instructions(F)) {
+    for (DbgVariableRecord &DVR : filterDbgVars(I.getDbgRecordRange())) {
+      DILocalVariable *Var = DVR.getVariable();
+      if (!Var || !Var->getArg())
+        continue;
+      // Only this function's own parameters are numbered for us. An inlined
+      // callee's parameters are numbered too, and its records may name our
+      // Arguments: kmalloc(size, flags) inlined into f(ptr, size) leaves
+      // #dbg_value(%size, "size", arg: 1) behind, which would otherwise be
+      // taken as a description of f's first parameter.
+      if (DVR.getDebugLoc() && DVR.getDebugLoc().getInlinedAt())
+        continue;
+      if (SP && Var->getScope() && Var->getScope()->getSubprogram() != SP)
+        continue;
+      // A #dbg_declare names the parameter's storage rather than its incoming
+      // value. That storage is written by the prologue, which follows the trace
+      // call, so reading it here would report whatever the stack held.
+      if (DVR.isDbgDeclare())
+        continue;
+
+      // Only a location that is the value itself can be reported. An
+      // expression that computes the value - a field shifted out of a wider
+      // register, an offset applied to a pointer - cannot be replayed into the
+      // spill slot, and storing the value it starts from would write the wrong
+      // bytes and overrun the piece.
+      DIExpression *Expr = DVR.getExpression();
+      std::optional<DIExpression::FragmentInfo> Fragment =
+          Expr->getFragmentInfo();
+      if (Expr->getNumElements() != (Fragment ? 3u : 0u))
+        continue;
+
+      Value *V = DVR.getValue();
+      if (!V)
+        continue;
+      // A parameter that interprocedural optimization proved dead is described
+      // as poison, and callers pass poison for it: there is nothing to report.
+      if (isa<UndefValue>(V))
+        continue;
+      // Debug records are exempt from SSA dominance, so a record may name a
+      // value that is not available where the trace call goes. Reporting it
+      // would produce IR failing the verifier with "Instruction does not
+      // dominate all uses".
+      if (auto *Def = dyn_cast<Instruction>(V))
+        if (Def->getParent() != &EntryBB || Def->isTerminator())
+          continue;
+
+      SourceParam &Param = Params[Var->getArg()];
+      Param.Ty = Var->getType();
+      Param.addPiece(V, Fragment ? Fragment->OffsetInBits : 0,
+                     Fragment.has_value());
+    }
+  }
+}
+
+/// Widen \p V to the 64-bit value the callbacks take: a pointer is reported as
+/// the address it holds, a floating-point value as its bit pattern, and a value
+/// wider than 64 bits by its low half. Returns the value together with the
+/// number of bytes of it that are meaningful, or {nullptr, 0} for a type that
+/// cannot be widened - a vector, or an aggregate the ABI passes in registers.
+static std::pair<Value *, uint64_t> getReportedValue(IRBuilderBase &IRB,
+                                                     Value *V) {
+  const DataLayout &DL = IRB.GetInsertBlock()->getDataLayout();
+  Type *Ty = V->getType();
+  IntegerType *Int64Ty = IRB.getInt64Ty();
+
+  // ptrtoint widens or narrows to the requested type, so this also holds for a
+  // target whose pointers are narrower than 64 bits.
+  if (Ty->isPointerTy())
+    return {IRB.CreatePtrToInt(V, Int64Ty), DL.getPointerSize()};
+
+  if (Ty->isFloatingPointTy()) {
+    unsigned Bits = Ty->getPrimitiveSizeInBits();
+    V = IRB.CreateBitCast(V, IRB.getIntNTy(Bits));
+    Ty = V->getType();
+  }
+  if (!Ty->isIntegerTy())
+    return {nullptr, 0};
+
+  uint64_t Size = divideCeil(Ty->getIntegerBitWidth(), 8);
+  if (Size > sizeof(uint64_t)) {
+    // A value that does not fit is reported by its low bytes rather than not at
+    // all: they are what a comparison against it looks at first.
+    V = IRB.CreateTrunc(V, Int64Ty);
+    Size = sizeof(uint64_t);
+  } else {
+    V = IRB.CreateZExt(V, Int64Ty);
+  }
+  return {V, Size};
+}
+
+/// Widen \p V for the callbacks, splitting a value that the ABI passed as a
+/// first-class aggregate into its elements: those live in separate registers
+/// and share no address, so each is reported on its own. Appends at least one
+/// entry, {nullptr, 0} for a value that cannot be reported at all.
+static void
+getReportedValues(IRBuilderBase &IRB, Value *V,
+                  SmallVectorImpl<std::pair<Value *, uint64_t>> &Out) {
+  size_t First = Out.size();
+  Type *Ty = V->getType();
+  unsigned NumElements = Ty->isStructTy()  ? Ty->getStructNumElements()
+                         : Ty->isArrayTy() ? Ty->getArrayNumElements()
+                                           : 0;
+  if (NumElements)
+    for (unsigned I = 0; I != NumElements; ++I)
+      Out.push_back(getReportedValue(IRB, IRB.CreateExtractValue(V, I)));
+  else
+    Out.push_back(getReportedValue(IRB, V));
+
+  if (Out.size() == First)
+    Out.emplace_back(nullptr, 0);
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  DISubprogram *SP = F.getSubprogram();
+  BasicBlock &EntryBB = F.getEntryBlock();
+
+  SourceParamMap Params;
+  collectSourceParams(F, Params);
+
+  // Trace as early as the reported values allow: at the first insertion point
+  // of the entry block, pushed down past the definition of every value that is
+  // reported so that it dominates the call.
+  BasicBlock::iterator IP = EntryBB.getFirstInsertionPt();
+  for (const auto &[Idx, Param] : Params)
+    for (const auto &[V, BitOffset] : Param.Pieces)
+      if (auto *Def = dyn_cast<Instruction>(V); Def && !Def->comesBefore(&*IP))
+        IP = std::next(Def->getIterator());
+
+  // InstrumentationIRBuilder gives the calls a synthetic !dbg location. A
+  // plain IRBuilder would leave them without one, which fails the verifier
+  // ("inlinable function call ... requires a !dbg location") once a function
+  // built with -g is inlined.
+  InstrumentationIRBuilder IRB(&*IP);
+  Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+  auto trace = [&](unsigned Idx, Value *Val, uint64_t Size,
+                   FieldOffsets Offsets) {
+    IRB.CreateCall(
+        SanCovTraceArgsFunc,
+        {PC, ConstantInt::get(Int32Ty, Idx - 1),
+         ConstantInt::get(Int32Ty, Size),
+         Val ? Val : ConstantInt::get(Int64Ty, 0),
+         Offsets.Table ? Offsets.Table : ConstantPointerNull::get(PtrTy),
+         ConstantInt::get(Int32Ty, Offsets.NumFields)});
+  };
+
+  // Report \p V as the value of the \p Idx-th parameter, declared as \p Ty.
+  // A pointer to a struct whose fields are known is reported as the address of
+  // that struct, so that a consumer can read the fields out of it; every other
+  // value is reported as the value itself. \p WholeObject says whether \p V is
+  // the entire parameter, which a fragment of a struct the ABI split across
+  // registers is not - such a fragment may well be a pointer, but it is not the
+  // address of the struct its type describes.
+  auto traceValue = [&](unsigned Idx, Value *V, DIType *Ty, bool WholeObject) {
+    // Ask for the field table only on the path that uses it: building one for a
+    // struct that ends up reported by value would leave an unreferenced global
+    // behind in every object file.
+    if (WholeObject && V->getType()->isPointerTy())
+      if (FieldOffsets Offsets = getFieldOffsets(Ty); Offsets.Table) {
+        trace(Idx, IRB.CreatePtrToInt(V, Int64Ty), Offsets.ObjectSize, Offsets);
+        return;
+      }
+    SmallVector<std::pair<Value *, uint64_t>, 2> Values;
+    getReportedValues(IRB, V, Values);
+    for (auto [Val, Size] : Values)
+      trace(Idx, Val, Size, {});
+  };
+
+  // Without debug records there is nothing to map the IR arguments back to, so
+  // report them positionally and let the declared parameter types, if there
+  // are any, describe their fields. ABI lowering is visible to the consumer in
+  // this mode: a coerced aggregate is reported as the values it was coerced
+  // into, and the indices of the parameters after it shift accordingly.
+  if (Params.empty()) {
+    unsigned Idx = 1;
+    for (Argument &Arg : F.args()) {
+      // A struct-return pointer has no source-level counterpart.
+      if (Arg.hasStructRetAttr())
+        continue;
+      traceValue(Idx, &Arg, getDeclaredParamType(SP, Idx),
+                 /*WholeObject=*/true);
+      ++Idx;
+    }
+    return;
+  }
+
+  // One call per source-level parameter, in source order, except that a
+  // parameter the ABI split across registers is reported once per piece: the
+  // pieces have no common address to report them from. A parameter with no
+  // usable location is still reported, with size 0, so that a consumer sees
+  // every parameter the function declares.
+  unsigned NumParams = getNumDeclaredParams(SP);
+  for (const auto &[Idx, Param] : Params)
+    NumParams = std::max(NumParams, Idx);
+
+  for (unsigned Idx = 1; Idx <= NumParams; ++Idx) {
+    auto It = Params.find(Idx);
+    if (It == Params.end() || It->second.Pieces.empty()) {
+      trace(Idx, nullptr, 0, {});
+      continue;
+    }
+    const SourceParam &Param = It->second;
+    bool WholeObject = Param.Pieces.size() == 1 && !Param.Fragmented;
+    for (const auto &[V, BitOffset] : Param.Pieces)
+      traceValue(Idx, V, Param.Ty, WholeObject);
+  }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+  DIType *RetTy = getDeclaredReturnType(F.getSubprogram());
+
+  // A struct returned by value may be lowered to an indirect return: the IR
+  // function returns void and writes the result through a hidden struct-return
+  // pointer. Report that buffer, so an indirect return is not dropped. This
+  // mirrors the argument side, which skips the same pointer.
+  Argument *SRetArg = nullptr;
+  for (Argument &Arg : F.args())
+    if (Arg.hasStructRetAttr()) {
+      SRetArg = &Arg;
+      break;
+    }
+
+  for (BasicBlock &BB : F) {
+    // Only a return carries a value to report; unwinding leaves the function
+    // without one.
+    auto *RI = dyn_cast<ReturnInst>(BB.getTerminator());
+    if (!RI)
+      continue;
+    // A musttail call has to stay adjacent to the return that forwards it, so
+    // there is nowhere to put the call.
+    if (auto *CI = dyn_cast_or_null<CallInst>(RI->getPrevNode());
+        CI && CI->isMustTailCall())
+      continue;
+
+    InstrumentationIRBuilder IRB(RI);
+    Value *PC = IRB.CreatePtrToInt(&F, Int64Ty);
+
+    auto trace = [&](Value *Val, uint64_t Size, FieldOffsets Offsets) {
+      IRB.CreateCall(
+          SanCovTraceRetFunc,
+          {PC, ConstantInt::get(Int32Ty, Size),
+           Val ? Val : ConstantInt::get(Int64Ty, 0),
+           Offsets.Table ? Offsets.Table : ConstantPointerNull::get(PtrTy),
+           ConstantInt::get(Int32Ty, Offsets.NumFields)});
+    };
+
+    Value *RetVal = RI->getReturnValue();
+    // The value is reported by address when it is one: a pointer whose pointee
+    // fields are known, or the buffer of an indirect struct return. A struct
+    // returned in registers has no address, so it is reported as its elements.
+    if (RetVal ? RetVal->getType()->isPointerTy() : SRetArg != nullptr) {
+      Value *Object = RetVal ? RetVal : SRetArg;
+      if (FieldOffsets Offsets = getFieldOffsets(RetTy); Offsets.Table) {
+        trace(IRB.CreatePtrToInt(Object, Int64Ty), Offsets.ObjectSize, Offsets);
+        continue;
+      }
+      // An indirect return whose field layout is unknown is a struct in memory,
+      // not a value that fits in a register, so there is nothing to report.
+      if (!RetVal) {
+        trace(nullptr, 0, {});
+        continue;
+      }
+    }
+
+    if (!RetVal) {
+      trace(nullptr, 0, {}); // A void return.
+      continue;
+    }
+    SmallVector<std::pair<Value *, uint64_t>, 2> Values;
+    getReportedValues(IRB, RetVal, Values);
+    for (auto [Val, Size] : Values)
+      trace(Val, Size, {});
+  }
+}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
new file mode 100644
index 0000000000000..eac49ee3bd1a3
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-abi.ll
@@ -0,0 +1,127 @@
+; Argument tracing across ABI lowering. A by-value struct the ABI passed in
+; registers has no address, so it is reported as one call per register piece,
+; all carrying the same source-level parameter index. A struct the ABI passed
+; indirectly does have an address, and is reported through it with its field
+; offset table.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.big = type { i64, i64, i64, i64, i64 }
+
+; Only struct big needs a field table: it is the only one reported by address.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [10 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8, i64 32, i64 8]
+
+; int use_pair(struct pair p, int z) with struct pair { int x; int y; } coerced
+; into a single i64 and split back into two i32 fragments. Both pieces are
+; reported under index 0, and `z` keeps index 1.
+define i32 @use_pair(i64 %0, i32 %1) !dbg !13 {
+entry:
+  %2 = trunc i64 %0 to i32
+  %3 = lshr i64 %0, 32
+  %4 = trunc nuw i64 %3 to i32
+    #dbg_value(i32 %2, !17, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !19)
+    #dbg_value(i32 %4, !17, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !19)
+    #dbg_value(i32 %1, !18, !DIExpression(), !19)
+  %5 = add i32 %2, %4
+  ret i32 %5
+}
+; Nothing is spilled, so no field table is built for struct pair either.
+; CHECK-LABEL: define i32 @use_pair(
+; CHECK-NOT: alloca
+; CHECK: %[[LO:[0-9]+]] = zext i32 %2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 0, i32 4, i64 %[[LO]], ptr null, i32 0)
+; CHECK: %[[HI:[0-9]+]] = zext i32 %4 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 0, i32 4, i64 %[[HI]], ptr null, i32 0)
+; CHECK: %[[Z:[0-9]+]] = zext i32 %1 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_pair to i64), i32 1, i32 4, i64 %[[Z]], ptr null, i32 0)
+
+; A fragment whose location is an expression that *computes* the value - here a
+; field shifted out of a wider register - is not a value that can be reported,
+; so it is dropped while the fragments that are values are still reported.
+;
+; The dropped fragment is also defined before the one that follows it, so this
+; doubles as a check that the calls are placed after every value they report:
+; opt runs the verifier, which rejects a use that does not dominate.
+define i32 @use_trio([2 x i64] %0) !dbg !30 {
+entry:
+  %1 = extractvalue [2 x i64] %0, 0
+  %2 = trunc i64 %1 to i32
+  %3 = extractvalue [2 x i64] %0, 1
+    #dbg_value(i32 %2, !34, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !35)
+    #dbg_value(i64 %1, !34, !DIExpression(DW_OP_constu, 32, DW_OP_shr, DW_OP_LLVM_convert, 64, DW_ATE_unsigned, DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_stack_value, DW_OP_LLVM_fragment, 32, 32), !35)
+    #dbg_value(i64 %3, !34, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !35)
+  ret i32 %2
+}
+; CHECK-LABEL: define i32 @use_trio(
+; CHECK: %[[A:[0-9]+]] = zext i32 %2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 0, i32 4, i64 %[[A]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 0, i32 8, i64 %3, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_trio to i64), i32 1
+
+; void use_big(struct big b) passed indirectly: the parameter is a pointer to
+; the caller's copy, described by the struct's own type, so it is reported as
+; the address of that copy with its five fields.
+define void @use_big(ptr byval(%struct.big) align 8 %0) !dbg !20 {
+entry:
+    #dbg_value(ptr %0, !24, !DIExpression(), !25)
+  ret void
+}
+; CHECK-LABEL: define void @use_big(
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %0 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @use_big to i64), i32 0, i32 40, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 5)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "abi.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; struct pair { int x; int y; }
+!7 = !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !3, size: 64, elements: !8)
+!8 = !{!9, !10}
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !7, file: !3, baseType: !4, size: 32)
+!10 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !7, file: !3, baseType: !4, size: 32, offset: 32)
+
+; struct big { long a, b, c, d, e; }
+!11 = !DICompositeType(tag: DW_TAG_structure_type, name: "big", file: !3, size: 320, elements: !12)
+!12 = !{!41, !42, !43, !44, !45}
+
+!13 = distinct !DISubprogram(name: "use_pair", scope: !3, file: !3, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !16)
+!14 = !DISubroutineType(types: !15)
+!15 = !{!4, !7, !4}
+!16 = !{!17, !18}
+!17 = !DILocalVariable(name: "p", arg: 1, scope: !13, file: !3, line: 1, type: !7)
+!18 = !DILocalVariable(name: "z", arg: 2, scope: !13, file: !3, line: 1, type: !4)
+!19 = !DILocation(line: 1, column: 1, scope: !13)
+
+!20 = distinct !DISubprogram(name: "use_big", scope: !3, file: !3, line: 6, type: !21, scopeLine: 6, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !23)
+!21 = !DISubroutineType(types: !22)
+!22 = !{null, !11}
+!23 = !{!24}
+!24 = !DILocalVariable(name: "b", arg: 1, scope: !20, file: !3, line: 6, type: !11)
+!25 = !DILocation(line: 6, column: 1, scope: !20)
+
+; struct trio { int a; int b; long c; }
+!29 = !DICompositeType(tag: DW_TAG_structure_type, name: "trio", file: !3, size: 128, elements: !39)
+!30 = distinct !DISubprogram(name: "use_trio", scope: !3, file: !3, line: 11, type: !31, scopeLine: 11, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !33)
+!31 = !DISubroutineType(types: !32)
+!32 = !{!4, !29}
+!33 = !{!34}
+!34 = !DILocalVariable(name: "t", arg: 1, scope: !30, file: !3, line: 11, type: !29)
+!35 = !DILocation(line: 11, column: 1, scope: !30)
+!39 = !{!36, !37, !38}
+!36 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !29, file: !3, baseType: !4, size: 32)
+!37 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !29, file: !3, baseType: !4, size: 32, offset: 32)
+!38 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !29, file: !3, baseType: !5, size: 64, offset: 64)
+!41 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !11, file: !3, baseType: !5, size: 64)
+!42 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !11, file: !3, baseType: !5, size: 64, offset: 64)
+!43 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !11, file: !3, baseType: !5, size: 64, offset: 128)
+!44 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !11, file: !3, baseType: !5, size: 64, offset: 192)
+!45 = !DIDerivedType(tag: DW_TAG_member, name: "e", scope: !11, file: !3, baseType: !5, size: 64, offset: 256)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
new file mode 100644
index 0000000000000..3e5c8a87a4d95
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-dominance.ll
@@ -0,0 +1,67 @@
+; A debug record may name a value that the trace call cannot use: debug records
+; are exempt from SSA dominance, so the value may be defined after the call, and
+; a parameter that interprocedural optimization proved dead is described as
+; poison. Neither can be reported, and using the first would produce IR that
+; fails the verifier with "Instruction does not dominate all uses". Such
+; parameters are reported with size 0.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; The only location of `a` is %later, defined in a block that the entry block
+; branches to. `b` is a plain pointer argument and is reported normally.
+define void @location_does_not_dominate(ptr %a, ptr %b) !dbg !6 {
+entry:
+    #dbg_value(ptr %later, !10, !DIExpression(), !12)
+    #dbg_value(ptr %b, !11, !DIExpression(), !12)
+  br label %bb, !dbg !12
+bb:
+  %later = getelementptr i8, ptr %a, i64 128, !dbg !12
+  ret void, !dbg !12
+}
+; CHECK-LABEL: define void @location_does_not_dominate(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @location_does_not_dominate to i64), i32 0, i32 0, i64 0, ptr null, i32 0)
+; CHECK: %[[B:[0-9]+]] = ptrtoint ptr %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @location_does_not_dominate to i64), i32 1, i32 8, i64 %[[B]], ptr null, i32 0)
+
+; `a` is dead and described as poison; `b` is live.
+define void @poison_location(ptr %b) !dbg !13 {
+entry:
+    #dbg_value(ptr poison, !15, !DIExpression(), !17)
+    #dbg_value(ptr %b, !16, !DIExpression(), !17)
+  ret void
+}
+; CHECK-LABEL: define void @poison_location(
+; CHECK-NOT: ptrtoint ptr poison
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @poison_location to i64), i32 0, i32 0, i64 0, ptr null, i32 0)
+; CHECK: %[[PB:[0-9]+]] = ptrtoint ptr %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @poison_location to i64), i32 1, i32 8, i64 %[[PB]], ptr null, i32 0)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "dominance.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+
+!6 = distinct !DISubprogram(name: "location_does_not_dominate", scope: !3, file: !3, line: 1, type: !7, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !9)
+!7 = !DISubroutineType(types: !8)
+!8 = !{null, !5, !5}
+!9 = !{!10, !11}
+!10 = !DILocalVariable(name: "a", arg: 1, scope: !6, file: !3, line: 1, type: !5)
+!11 = !DILocalVariable(name: "b", arg: 2, scope: !6, file: !3, line: 1, type: !5)
+!12 = !DILocation(line: 1, column: 1, scope: !6)
+
+!13 = distinct !DISubprogram(name: "poison_location", scope: !3, file: !3, line: 6, type: !7, scopeLine: 6, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !14)
+!14 = !{!15, !16}
+!15 = !DILocalVariable(name: "a", arg: 1, scope: !13, file: !3, line: 6, type: !5)
+!16 = !DILocalVariable(name: "b", arg: 2, scope: !13, file: !3, line: 6, type: !5)
+!17 = !DILocation(line: 6, column: 1, scope: !13)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll
new file mode 100644
index 0000000000000..f1dc642b0f79a
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-inlined.ll
@@ -0,0 +1,52 @@
+; An inlined callee's parameters are numbered just like the caller's, and the
+; records describing them may name the caller's own arguments. Only records
+; belonging to the function being instrumented may drive trace-args, otherwise
+; an inlined callee's first parameter would be reported as the caller's.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; void caller(long size) calling an inlined callee(long n): the inlined
+; parameter `n` is also arg 1 and is described by a record naming %size, but it
+; belongs to @callee, so only @caller's own record for `size` is used.
+define void @caller(i64 %size, ptr %p) !dbg !6 {
+entry:
+    #dbg_value(i64 %size, !14, !DIExpression(), !16)
+    #dbg_value(ptr %p, !15, !DIExpression(), !16)
+    #dbg_value(i64 %size, !12, !DIExpression(), !17)
+  ret void
+}
+; Both of @caller's parameters are reported from their own records. Nothing is
+; reported for the inlined `n`.
+; CHECK-LABEL: define void @caller(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 0, i32 8, i64 %size, ptr null, i32 0)
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 1, i32 8, i64 %[[P]], ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @caller to i64), i32 2
+
+declare void @callee(i64)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "inlined.c", directory: "/")
+!4 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!5 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+
+!6 = distinct !DISubprogram(name: "caller", scope: !3, file: !3, line: 10, type: !7, scopeLine: 10, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !13)
+!7 = !DISubroutineType(types: !8)
+!8 = !{null, !4, !5}
+!9 = distinct !DISubprogram(name: "callee", scope: !3, file: !3, line: 1, type: !10, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !11)
+!10 = !DISubroutineType(types: !{null, !4})
+!11 = !{!12}
+!12 = !DILocalVariable(name: "n", arg: 1, scope: !9, file: !3, line: 1, type: !4)
+!13 = !{!14, !15}
+!14 = !DILocalVariable(name: "size", arg: 1, scope: !6, file: !3, line: 10, type: !4)
+!15 = !DILocalVariable(name: "p", arg: 2, scope: !6, file: !3, line: 10, type: !5)
+!16 = !DILocation(line: 10, column: 1, scope: !6)
+!17 = !DILocation(line: 1, column: 1, scope: !9, inlinedAt: !16)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
new file mode 100644
index 0000000000000..80f50e58aedb4
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-debug.ll
@@ -0,0 +1,35 @@
+; Argument tracing without debug info: there is nothing to map the IR arguments
+; back to, so they are reported positionally and without field offset tables.
+; This keeps trace-args usable on code built without -g; the ABI's view of the
+; arguments is then what a consumer sees.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i64, i64, i64 }
+
+define void @no_debug(ptr %p, i32 %x) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @no_debug(
+; With no type to describe its pointee, a pointer is reported as its own value.
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 0, i32 8, i64 %[[P]], ptr null, i32 0)
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug to i64), i32 1, i32 4, i64 %[[X]], ptr null, i32 0)
+
+; A struct-return pointer is ABI-inserted and has no source-level counterpart,
+; so it is skipped here as well and %x keeps index 0.
+define void @no_debug_sret(ptr sret(%struct.S) %0, i32 %x) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @no_debug_sret(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug_sret to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @no_debug_sret to i64), i32 1
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 0000000000000..2af4899bad284
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,130 @@
+; Argument tracing: one __sanitizer_cov_trace_args call per source-level
+; parameter. A scalar is reported as its value, a pointer to a struct as the
+; address of that struct together with its field offset table, and hidden ABI
+; arguments are left out.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i32, i64 }
+
+; struct S { int a; long b; } is described by one {byte offset, byte size} pair
+; per field. The table is shared by every value of that type, so both functions
+; below referring to struct S must produce exactly one.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [4 x i64] [i64 0, i64 4, i64 8, i64 8]
+; CHECK-NOT: = private unnamed_addr constant [{{.*}} x i64]
+
+; void two_params(struct S *s, int x)
+define void @two_params(ptr %s, i32 %x) !dbg !11 {
+entry:
+    #dbg_value(ptr %s, !15, !DIExpression(), !17)
+    #dbg_value(i32 %x, !16, !DIExpression(), !17)
+  ret void
+}
+; The pointer is reported as the address of the 16-byte struct it points at, so
+; that a consumer can read the two fields out of it. Nothing is spilled.
+; CHECK-LABEL: define void @two_params(
+; CHECK-NOT: alloca
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %s to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @two_params to i64), i32 0, i32 16, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 2)
+; The scalar is reported as its own value, widened to 64 bits.
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @two_params to i64), i32 1, i32 4, i64 %[[X]], ptr null, i32 0)
+
+; struct S sret_and_scalar(int x), returning the struct through a hidden
+; struct-return pointer: that pointer is not a source-level parameter, so `x`
+; keeps source index 0 rather than becoming the IR argument index 1.
+define void @sret_and_scalar(ptr sret(%struct.S) %0, i32 %1) !dbg !18 {
+entry:
+    #dbg_value(i32 %1, !22, !DIExpression(), !23)
+  ret void
+}
+; CHECK-LABEL: define void @sret_and_scalar(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @sret_and_scalar to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @sret_and_scalar to i64), i32 1
+
+; void dead_param(struct S *s, int x) with `x` optimized away: it has no debug
+; record left, but the subprogram still declares it, so it is reported with
+; size 0 to keep the parameter list complete.
+define void @dead_param(ptr %s) !dbg !24 {
+entry:
+    #dbg_value(ptr %s, !28, !DIExpression(), !29)
+  ret void
+}
+; CHECK-LABEL: define void @dead_param(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @dead_param to i64), i32 0, i32 16, i64 %{{[0-9]+}}, ptr @__sancov_offsets_, i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @dead_param to i64), i32 1, i32 0, i64 0, ptr null, i32 0)
+
+; A pointer whose pointee has no field layout - void *, int * - is reported as
+; the pointer value itself rather than as an address to read through.
+define void @opaque_pointer(ptr %p) !dbg !30 {
+entry:
+    #dbg_value(ptr %p, !32, !DIExpression(), !33)
+  ret void
+}
+; CHECK-LABEL: define void @opaque_pointer(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @opaque_pointer to i64), i32 0, i32 8, i64 %[[P]], ptr null, i32 0)
+
+; A floating-point parameter is reported as its bit pattern.
+define void @floating(double %d) !dbg !34 {
+entry:
+    #dbg_value(double %d, !36, !DIExpression(), !37)
+  ret void
+}
+; CHECK-LABEL: define void @floating(
+; CHECK: %[[BITS:[0-9]+]] = bitcast double %d to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @floating to i64), i32 0, i32 8, i64 %[[BITS]], ptr null, i32 0)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "args.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !3, size: 128, elements: !7)
+!7 = !{!8, !9}
+!8 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !3, baseType: !4, size: 32)
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !3, baseType: !5, size: 64, offset: 64)
+!10 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+
+!11 = distinct !DISubprogram(name: "two_params", scope: !3, file: !3, line: 1, type: !12, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !14)
+!12 = !DISubroutineType(types: !13)
+!13 = !{null, !10, !4}
+!14 = !{!15, !16}
+!15 = !DILocalVariable(name: "s", arg: 1, scope: !11, file: !3, line: 1, type: !10)
+!16 = !DILocalVariable(name: "x", arg: 2, scope: !11, file: !3, line: 1, type: !4)
+!17 = !DILocation(line: 1, column: 1, scope: !11)
+
+!18 = distinct !DISubprogram(name: "sret_and_scalar", scope: !3, file: !3, line: 5, type: !19, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !21)
+!19 = !DISubroutineType(types: !20)
+!20 = !{!6, !4}
+!21 = !{!22}
+!22 = !DILocalVariable(name: "x", arg: 1, scope: !18, file: !3, line: 5, type: !4)
+!23 = !DILocation(line: 5, column: 1, scope: !18)
+
+!24 = distinct !DISubprogram(name: "dead_param", scope: !3, file: !3, line: 9, type: !25, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !26)
+!25 = !DISubroutineType(types: !13)
+!26 = !{!28}
+!28 = !DILocalVariable(name: "s", arg: 1, scope: !24, file: !3, line: 9, type: !10)
+!29 = !DILocation(line: 9, column: 1, scope: !24)
+
+!30 = distinct !DISubprogram(name: "opaque_pointer", scope: !3, file: !3, line: 13, type: !38, scopeLine: 13, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!31 = !{!32}
+!32 = !DILocalVariable(name: "p", arg: 1, scope: !30, file: !3, line: 13, type: !39)
+!33 = !DILocation(line: 13, column: 1, scope: !30)
+
+!34 = distinct !DISubprogram(name: "floating", scope: !3, file: !3, line: 17, type: !40, scopeLine: 17, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !35)
+!35 = !{!36}
+!36 = !DILocalVariable(name: "d", arg: 1, scope: !34, file: !3, line: 17, type: !42)
+!37 = !DILocation(line: 17, column: 1, scope: !34)
+
+!38 = !DISubroutineType(types: !{null, !39})
+!39 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !4, size: 64)
+!40 = !DISubroutineType(types: !{null, !42})
+!42 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
new file mode 100644
index 0000000000000..395efab36dd77
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
@@ -0,0 +1,67 @@
+; Neither mode ever creates memory to report a value from, on any target: a
+; value that is not already in memory is reported as the value itself. Nothing
+; escapes to the stack, so no frame is realigned and no base pointer is
+; reserved on the instrumentation's account.
+;
+; This is what makes the modes portable. A function whose inline assembly claims
+; the x86-64 base pointer (rbx) is the case that would otherwise fail to
+; compile - a redzoned, realigned spill slot needs the very register the
+; assembly uses - and it now needs no special handling: it is instrumented
+; exactly like the function below it.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+; CHECK-NOT: alloca
+
+; An rdtsc-style clobber of rbx.
+define i32 @clobbers_rbx(i32 %x) #0 !dbg !6 {
+entry:
+  call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, !dbg !9
+  ret i32 %x, !dbg !9
+}
+; CHECK-LABEL: define i32 @clobbers_rbx(
+; CHECK: %[[A:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @clobbers_rbx to i64), i32 0, i32 4, i64 %[[A]], ptr null, i32 0)
+; CHECK: %[[R:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @clobbers_rbx to i64), i32 4, i64 %[[R]], ptr null, i32 0)
+; CHECK: ret i32 %x
+
+; A cpuid-style "=b" output operand names the same register.
+define i32 @uses_b_constraint(i32 %x) #0 !dbg !10 {
+entry:
+  %0 = call i32 asm "cpuid", "=b,0"(i32 %x) #0, !dbg !11
+  ret i32 %0, !dbg !11
+}
+; CHECK-LABEL: define i32 @uses_b_constraint(
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @uses_b_constraint to i64), i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @uses_b_constraint to i64), i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+
+; The same function without such inline assembly, instrumented identically.
+define i32 @plain(i32 %x) #0 !dbg !12 {
+entry:
+  ret i32 %x, !dbg !13
+}
+; CHECK-LABEL: define i32 @plain(
+; CHECK: %[[PA:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i64 ptrtoint (ptr @plain to i64), i32 0, i32 4, i64 %[[PA]], ptr null, i32 0)
+; CHECK: %[[PR:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @plain to i64), i32 4, i64 %[[PR]], ptr null, i32 0)
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "no-spill.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DISubroutineType(types: !{!4, !4})
+!6 = distinct !DISubprogram(name: "clobbers_rbx", scope: !3, file: !3, line: 1, type: !5, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!9 = !DILocation(line: 1, column: 1, scope: !6)
+!10 = distinct !DISubprogram(name: "uses_b_constraint", scope: !3, file: !3, line: 5, type: !5, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!11 = !DILocation(line: 5, column: 1, scope: !10)
+!12 = distinct !DISubprogram(name: "plain", scope: !3, file: !3, line: 9, type: !5, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!13 = !DILocation(line: 9, column: 1, scope: !12)
+!20 = !{}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 0000000000000..3e1223457d727
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,138 @@
+; Return value tracing: one __sanitizer_cov_trace_ret call before every return.
+; A scalar is reported as its value, a pointer to a struct as the address of
+; that struct together with its field offset table, and a struct returned
+; indirectly through the caller's buffer.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i32, i64 }
+%struct.Big = type { i64, i64, i64 }
+
+; CHECK-NOT: alloca
+
+; A pointer return whose pointee fields are known is reported as the address of
+; the object, so a consumer can read the fields out of it.
+define ptr @ret_struct_ptr(ptr %s) !dbg !13 {
+entry:
+  ret ptr %s
+}
+; CHECK-LABEL: define ptr @ret_struct_ptr(
+; CHECK: %[[ADDR:[0-9]+]] = ptrtoint ptr %s to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_struct_ptr to i64), i32 16, i64 %[[ADDR]], ptr @__sancov_offsets_, i32 2)
+; CHECK: ret ptr %s
+
+; A scalar return is reported as its value, once per return.
+define i32 @ret_scalar(i1 %c, i32 %x) !dbg !16 {
+entry:
+  br i1 %c, label %yes, label %no
+yes:
+  ret i32 %x
+no:
+  ret i32 0
+}
+; CHECK-LABEL: define i32 @ret_scalar(
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar to i64), i32 4, i64 %[[X]], ptr null, i32 0)
+; CHECK: ret i32 %x
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_scalar to i64), i32 4, i64 0, ptr null, i32 0)
+; CHECK: ret i32 0
+
+; A struct returned by value may be lowered to an indirect return: the IR
+; function returns void and writes the result through a hidden struct-return
+; pointer. That buffer is reported as the return value, with the size and the
+; fields of the source struct, so the return is not dropped.
+define void @ret_sret(ptr sret(%struct.Big) %0) !dbg !19 {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @ret_sret(
+; CHECK: %[[BUF:[0-9]+]] = ptrtoint ptr %0 to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_sret to i64), i32 24, i64 %[[BUF]], ptr @__sancov_offsets_.1, i32 3)
+; CHECK: ret void
+
+; A struct small enough to come back in registers has no address, so it is
+; reported as one call per register, the same way the argument side reports a
+; struct the ABI split across registers.
+define { i64, i64 } @ret_in_registers(i64 %a, i64 %b) !dbg !30 {
+entry:
+  %0 = insertvalue { i64, i64 } poison, i64 %a, 0
+  %1 = insertvalue { i64, i64 } %0, i64 %b, 1
+  ret { i64, i64 } %1
+}
+; CHECK-LABEL: define { i64, i64 } @ret_in_registers(
+; CHECK: %[[LO:[0-9]+]] = extractvalue { i64, i64 } %1, 0
+; CHECK: %[[HI:[0-9]+]] = extractvalue { i64, i64 } %1, 1
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_in_registers to i64), i32 8, i64 %[[LO]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_in_registers to i64), i32 8, i64 %[[HI]], ptr null, i32 0)
+
+; A void return has nothing to report.
+define void @ret_void() !dbg !22 {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @ret_void(
+; CHECK: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_void to i64), i32 0, i64 0, ptr null, i32 0)
+; CHECK: ret void
+
+; A musttail call has to stay adjacent to the return that forwards it, so there
+; is nowhere to put the call and the return is left alone.
+declare i32 @tail_callee(i32)
+define i32 @ret_musttail(i32 %x) !dbg !24 {
+entry:
+  %r = musttail call i32 @tail_callee(i32 %x)
+  ret i32 %r
+}
+; CHECK-LABEL: define i32 @ret_musttail(
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret(i64 ptrtoint (ptr @ret_musttail to i64)
+; CHECK: ret i32 %r
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "ret.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+; struct S { int a; long b; }
+!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !3, size: 128, elements: !7)
+!7 = !{!8, !9}
+!8 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !3, baseType: !4, size: 32)
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !3, baseType: !5, size: 64, offset: 64)
+!10 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+
+; struct Big { long a; long b; long c; }
+!11 = !DICompositeType(tag: DW_TAG_structure_type, name: "Big", file: !3, size: 192, elements: !12)
+!12 = !{!27, !28, !29}
+
+!13 = distinct !DISubprogram(name: "ret_struct_ptr", scope: !3, file: !3, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!14 = !DISubroutineType(types: !15)
+!15 = !{!10, !10}
+
+!16 = distinct !DISubprogram(name: "ret_scalar", scope: !3, file: !3, line: 5, type: !17, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!17 = !DISubroutineType(types: !18)
+!18 = !{!4, !4, !4}
+
+!19 = distinct !DISubprogram(name: "ret_sret", scope: !3, file: !3, line: 9, type: !20, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!20 = !DISubroutineType(types: !21)
+!21 = !{!11}
+
+!22 = distinct !DISubprogram(name: "ret_void", scope: !3, file: !3, line: 13, type: !23, scopeLine: 13, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!23 = !DISubroutineType(types: !{null})
+
+!24 = distinct !DISubprogram(name: "ret_musttail", scope: !3, file: !3, line: 17, type: !25, scopeLine: 17, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!25 = !DISubroutineType(types: !26)
+!26 = !{!4, !4}
+
+!27 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !11, file: !3, baseType: !5, size: 64)
+!28 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !11, file: !3, baseType: !5, size: 64, offset: 64)
+!29 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !11, file: !3, baseType: !5, size: 64, offset: 128)
+
+!30 = distinct !DISubprogram(name: "ret_in_registers", scope: !3, file: !3, line: 21, type: !32, scopeLine: 21, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !31)
+!31 = !{}
+!32 = !DISubroutineType(types: !{!6, !5, !5})



More information about the llvm-commits mailing list