| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| //===- JMCInstrumenter.cpp - JMC Instrumentation --------------------------===// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // JMCInstrumenter pass: | ||
| // - add "/alternatename:__CheckForDebuggerJustMyCode=__JustMyCode_Default" to | ||
| // "llvm.linker.options" | ||
| // - create the dummy COMDAT function __JustMyCode_Default | ||
| // - instrument each function with a call to __CheckForDebuggerJustMyCode. The | ||
| // sole argument should be defined in .msvcjmc. Each flag is 1 byte initilized | ||
| // to 1. | ||
| // - (TODO) currently targeting MSVC, adds ELF debuggers support | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #include "llvm/ADT/SmallString.h" | ||
| #include "llvm/ADT/StringExtras.h" | ||
| #include "llvm/CodeGen/Passes.h" | ||
| #include "llvm/IR/DIBuilder.h" | ||
| #include "llvm/IR/DebugInfoMetadata.h" | ||
| #include "llvm/IR/DerivedTypes.h" | ||
| #include "llvm/IR/Function.h" | ||
| #include "llvm/IR/Instructions.h" | ||
| #include "llvm/IR/LLVMContext.h" | ||
| #include "llvm/IR/Module.h" | ||
| #include "llvm/IR/Type.h" | ||
| #include "llvm/InitializePasses.h" | ||
| #include "llvm/Pass.h" | ||
| #include "llvm/Support/DJB.h" | ||
| #include "llvm/Support/Path.h" | ||
| #include "llvm/Transforms/Utils/ModuleUtils.h" | ||
|
|
||
| using namespace llvm; | ||
|
|
||
| #define DEBUG_TYPE "jmc-instrument" | ||
|
|
||
| namespace { | ||
| struct JMCInstrumenter : public ModulePass { | ||
| static char ID; | ||
| JMCInstrumenter() : ModulePass(ID) { | ||
| initializeJMCInstrumenterPass(*PassRegistry::getPassRegistry()); | ||
| } | ||
| bool runOnModule(Module &M) override; | ||
| }; | ||
| char JMCInstrumenter::ID = 0; | ||
| } // namespace | ||
|
|
||
| INITIALIZE_PASS( | ||
| JMCInstrumenter, DEBUG_TYPE, | ||
| "Instrument function entry with call to __CheckForDebuggerJustMyCode", | ||
| false, false) | ||
|
|
||
| ModulePass *llvm::createJMCInstrumenterPass() { return new JMCInstrumenter(); } | ||
|
|
||
| namespace { | ||
| const char CheckFunctionName[] = "__CheckForDebuggerJustMyCode"; | ||
|
|
||
| std::string getFlagName(DISubprogram &SP, bool UseX86FastCall) { | ||
| // Best effort path normalization. This is to guarantee an unique flag symbol | ||
| // is produced for the same directory. Some builds may want to use relative | ||
| // paths, or paths with a specific prefix (see the -fdebug-compilation-dir | ||
| // flag), so only hash paths in debuginfo. Don't expand them to absolute | ||
| // paths. | ||
| SmallString<256> FilePath(SP.getDirectory()); | ||
| sys::path::append(FilePath, SP.getFilename()); | ||
| sys::path::native(FilePath); | ||
| sys::path::remove_dots(FilePath, /*remove_dot_dot=*/true); | ||
|
|
||
| // The naming convention for the flag name is __<hash>_<file name> with '.' in | ||
| // <file name> replaced with '@'. For example C:\file.any.c would have a flag | ||
| // __D032E919_file@any@c. The naming convention match MSVC's format however | ||
| // the match is not required to make JMC work. The hashing function used here | ||
| // is different from MSVC's. | ||
|
|
||
| std::string Suffix; | ||
| for (auto C : sys::path::filename(FilePath)) | ||
| Suffix.push_back(C == '.' ? '@' : C); | ||
|
|
||
| sys::path::remove_filename(FilePath); | ||
| return (UseX86FastCall ? "_" : "__") + | ||
| utohexstr(djbHash(FilePath), /*LowerCase=*/false, | ||
| /*Width=*/8) + | ||
| "_" + Suffix; | ||
| } | ||
|
|
||
| void attachDebugInfo(GlobalVariable &GV, DISubprogram &SP) { | ||
| Module &M = *GV.getParent(); | ||
| DICompileUnit *CU = SP.getUnit(); | ||
| assert(CU); | ||
| DIBuilder DB(M, false, CU); | ||
|
|
||
| auto *DType = | ||
| DB.createBasicType("unsigned char", 8, dwarf::DW_ATE_unsigned_char, | ||
| llvm::DINode::FlagArtificial); | ||
|
|
||
| auto *DGVE = DB.createGlobalVariableExpression( | ||
| CU, GV.getName(), /*LinkageName=*/StringRef(), SP.getFile(), | ||
| /*LineNo=*/0, DType, /*IsLocalToUnit=*/true, /*IsDefined=*/true); | ||
| GV.addMetadata(LLVMContext::MD_dbg, *DGVE); | ||
| DB.finalize(); | ||
| } | ||
|
|
||
| FunctionType *getCheckFunctionType(LLVMContext &Ctx) { | ||
| Type *VoidTy = Type::getVoidTy(Ctx); | ||
| PointerType *VoidPtrTy = Type::getInt8PtrTy(Ctx); | ||
| return FunctionType::get(VoidTy, VoidPtrTy, false); | ||
| } | ||
|
|
||
| void createDefaultCheckFunction(Module &M, bool UseX86FastCall) { | ||
| LLVMContext &Ctx = M.getContext(); | ||
| const char *DefaultCheckFunctionName = | ||
| UseX86FastCall ? "_JustMyCode_Default" : "__JustMyCode_Default"; | ||
| // Create the function. | ||
| Function *DefaultCheckFunc = | ||
| Function::Create(getCheckFunctionType(Ctx), GlobalValue::ExternalLinkage, | ||
| DefaultCheckFunctionName, &M); | ||
| DefaultCheckFunc->setUnnamedAddr(GlobalValue::UnnamedAddr::Global); | ||
| DefaultCheckFunc->addParamAttr(0, Attribute::NoUndef); | ||
| if (UseX86FastCall) | ||
| DefaultCheckFunc->addParamAttr(0, Attribute::InReg); | ||
| appendToUsed(M, {DefaultCheckFunc}); | ||
| Comdat *C = M.getOrInsertComdat(DefaultCheckFunctionName); | ||
| C->setSelectionKind(Comdat::Any); | ||
| DefaultCheckFunc->setComdat(C); | ||
| BasicBlock *EntryBB = BasicBlock::Create(Ctx, "", DefaultCheckFunc); | ||
| ReturnInst::Create(Ctx, EntryBB); | ||
|
|
||
| // Add a linker option /alternatename to set the default implementation for | ||
| // the check function. | ||
| // https://devblogs.microsoft.com/oldnewthing/20200731-00/?p=104024 | ||
| std::string AltOption = std::string("/alternatename:") + CheckFunctionName + | ||
| "=" + DefaultCheckFunctionName; | ||
| llvm::Metadata *Ops[] = {llvm::MDString::get(Ctx, AltOption)}; | ||
| MDTuple *N = MDNode::get(Ctx, Ops); | ||
| M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(N); | ||
| } | ||
| } // namespace | ||
|
|
||
| bool JMCInstrumenter::runOnModule(Module &M) { | ||
| bool Changed = false; | ||
| LLVMContext &Ctx = M.getContext(); | ||
| Triple ModuleTriple(M.getTargetTriple()); | ||
| bool UseX86FastCall = | ||
| ModuleTriple.isOSWindows() && ModuleTriple.getArch() == Triple::x86; | ||
|
|
||
| Function *CheckFunction = nullptr; | ||
| DenseMap<DISubprogram *, Constant *> SavedFlags(8); | ||
| for (auto &F : M) { | ||
| if (F.isDeclaration()) | ||
| continue; | ||
| auto *SP = F.getSubprogram(); | ||
| if (!SP) | ||
| continue; | ||
|
|
||
| Constant *&Flag = SavedFlags[SP]; | ||
| if (!Flag) { | ||
| std::string FlagName = getFlagName(*SP, UseX86FastCall); | ||
| IntegerType *FlagTy = Type::getInt8Ty(Ctx); | ||
| Flag = M.getOrInsertGlobal(FlagName, FlagTy, [&] { | ||
| // FIXME: Put the GV in comdat and have linkonce_odr linkage to save | ||
| // .msvcjmc section space? maybe not worth it. | ||
| GlobalVariable *GV = new GlobalVariable( | ||
| M, FlagTy, /*isConstant=*/false, GlobalValue::InternalLinkage, | ||
| ConstantInt::get(FlagTy, 1), FlagName); | ||
| GV->setSection(".msvcjmc"); | ||
| GV->setAlignment(Align(1)); | ||
| GV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global); | ||
| attachDebugInfo(*GV, *SP); | ||
| return GV; | ||
| }); | ||
| } | ||
|
|
||
| if (!CheckFunction) { | ||
| assert(!M.getFunction(CheckFunctionName) && | ||
| "JMC instrument more than once?"); | ||
| CheckFunction = cast<Function>( | ||
| M.getOrInsertFunction(CheckFunctionName, getCheckFunctionType(Ctx)) | ||
| .getCallee()); | ||
| CheckFunction->setUnnamedAddr(GlobalValue::UnnamedAddr::Global); | ||
| CheckFunction->addParamAttr(0, Attribute::NoUndef); | ||
| if (UseX86FastCall) { | ||
| CheckFunction->setCallingConv(CallingConv::X86_FastCall); | ||
| CheckFunction->addParamAttr(0, Attribute::InReg); | ||
| } | ||
| } | ||
| // FIXME: it would be nice to make CI scheduling boundary, although in | ||
| // practice it does not matter much. | ||
| auto *CI = CallInst::Create(CheckFunction, {Flag}, "", | ||
| &*F.begin()->getFirstInsertionPt()); | ||
| CI->addParamAttr(0, Attribute::NoUndef); | ||
| if (UseX86FastCall) { | ||
| CI->setCallingConv(CallingConv::X86_FastCall); | ||
| CI->addParamAttr(0, Attribute::InReg); | ||
| } | ||
|
|
||
| Changed = true; | ||
| } | ||
| if (!Changed) | ||
| return false; | ||
|
|
||
| createDefaultCheckFunction(M, UseX86FastCall); | ||
| return true; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| ; RUN: opt -jmc-instrument -S < %s | FileCheck %s | ||
|
|
||
| ; CHECK: $_JustMyCode_Default = comdat any | ||
|
|
||
| ; CHECK: @"_A85D9D03_x@c" = internal unnamed_addr global i8 1, section ".msvcjmc", align 1, !dbg !0 | ||
| ; CHECK: @llvm.used = appending global [1 x i8*] [i8* bitcast (void (i8*)* @_JustMyCode_Default to i8*)], section "llvm.metadata" | ||
|
|
||
| ; CHECK: define void @w1() #0 !dbg !10 { | ||
| ; CHECK: call x86_fastcallcc void @__CheckForDebuggerJustMyCode(i8* inreg noundef @"_A85D9D03_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: declare x86_fastcallcc void @__CheckForDebuggerJustMyCode(i8* inreg noundef) unnamed_addr | ||
|
|
||
| ; CHECK: define void @_JustMyCode_Default(i8* inreg noundef %0) unnamed_addr comdat { | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: !0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression()) | ||
| ; CHECK: !1 = distinct !DIGlobalVariable(name: "_A85D9D03_x@c", scope: !2, file: !3, type: !5, isLocal: true, isDefinition: true) | ||
| ; CHECK: !2 = distinct !DICompileUnit(language: DW_LANG_C99, file: !3, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, globals: !4, splitDebugInlining: false, nameTableKind: None) | ||
| ; CHECK: !3 = !DIFile(filename: "./b/./../b/x.c", directory: "C:\\\\a\\\\") | ||
| ; CHECK: !4 = !{!0} | ||
| ; CHECK: !5 = !DIBasicType(name: "unsigned char", size: 8, encoding: DW_ATE_unsigned_char, flags: DIFlagArtificial) | ||
| ; CHECK: !6 = !{i32 2, !"CodeView", i32 1} | ||
| ; CHECK: !7 = !{i32 2, !"Debug Info Version", i32 3} | ||
| ; CHECK: !8 = !{!"clang"} | ||
| ; CHECK: !9 = !{!"/alternatename:__CheckForDebuggerJustMyCode=_JustMyCode_Default"} | ||
| ; CHECK: !10 = distinct !DISubprogram(name: "w1", scope: !3, file: !3, line: 1, type: !11, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !13) | ||
| ; CHECK: !11 = !DISubroutineType(types: !12) | ||
|
|
||
| target datalayout = "e-m:x-p:32:32-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32-a:0:32-S32" | ||
| target triple = "i386-pc-windows-msvc" | ||
|
|
||
| define void @w1() #0 !dbg !10 { | ||
| ret void | ||
| } | ||
|
|
||
| attributes #0 = { "target-cpu"="pentium4" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" } | ||
|
|
||
| !llvm.dbg.cu = !{!0} | ||
| !llvm.module.flags = !{!7, !8} | ||
| !llvm.ident = !{!9} | ||
|
|
||
| !0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None) | ||
| !1 = !DIFile(filename: "./b/./../b/x.c", directory: "C:\\\\a\\\\") | ||
| !7 = !{i32 2, !"CodeView", i32 1} | ||
| !8 = !{i32 2, !"Debug Info Version", i32 3} | ||
| !9 = !{!"clang"} | ||
| !10 = distinct !DISubprogram(name: "w1", scope: !1, file: !1, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !31 = !DISubroutineType(types: !32) | ||
| !32 = !{null} | ||
| !33 = !{} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| ; RUN: opt -jmc-instrument -mtriple=x86_64-pc-windows-msvc -S < %s | FileCheck %s | ||
| ; RUN: opt -jmc-instrument -mtriple=aarch64-pc-windows-msvc -S < %s | FileCheck %s | ||
| ; RUN: opt -jmc-instrument -mtriple=arm-pc-windows-msvc -S < %s | FileCheck %s | ||
|
|
||
| ; CHECK: $__JustMyCode_Default = comdat any | ||
|
|
||
| ; CHECK: @"__7DF23CF5_x@c" = internal unnamed_addr global i8 1, section ".msvcjmc", align 1, !dbg !0 | ||
| ; CHECK: @"__A85D9D03_x@c" = internal unnamed_addr global i8 1, section ".msvcjmc", align 1, !dbg !5 | ||
| ; CHECK: @llvm.used = appending global [1 x i8*] [i8* bitcast (void (i8*)* @__JustMyCode_Default to i8*)], section "llvm.metadata" | ||
|
|
||
| ; CHECK: define void @l1() !dbg !13 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__7DF23CF5_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: define void @l2() !dbg !17 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__7DF23CF5_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: define void @w1() !dbg !19 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__A85D9D03_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: define void @w2() !dbg !20 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__A85D9D03_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: define void @w3() !dbg !22 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__A85D9D03_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: define void @w4() !dbg !24 { | ||
| ; CHECK: call void @__CheckForDebuggerJustMyCode(i8* noundef @"__A85D9D03_x@c") | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: declare void @__CheckForDebuggerJustMyCode(i8* noundef) unnamed_addr | ||
|
|
||
| ; CHECK: define void @__JustMyCode_Default(i8* noundef %0) unnamed_addr comdat { | ||
| ; CHECK: ret void | ||
| ; CHECK: } | ||
|
|
||
| ; CHECK: !llvm.linker.options = !{!12} | ||
|
|
||
| ; CHECK: !0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression()) | ||
| ; CHECK: !1 = distinct !DIGlobalVariable(name: "__7DF23CF5_x@c", scope: !2, file: !3, type: !8, isLocal: true, isDefinition: true) | ||
| ; CHECK: !2 = distinct !DICompileUnit(language: DW_LANG_C99, file: !3, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, globals: !4, splitDebugInlining: false, nameTableKind: None) | ||
| ; CHECK: !3 = !DIFile(filename: "a/x.c", directory: "/tmp") | ||
| ; CHECK: !4 = !{!0, !5} | ||
| ; CHECK: !5 = !DIGlobalVariableExpression(var: !6, expr: !DIExpression()) | ||
| ; CHECK: !6 = distinct !DIGlobalVariable(name: "__A85D9D03_x@c", scope: !2, file: !7, type: !8, isLocal: true, isDefinition: true) | ||
| ; CHECK: !7 = !DIFile(filename: "./x.c", directory: "C:\\\\a\\\\b\\\\") | ||
| ; CHECK: !8 = !DIBasicType(name: "unsigned char", size: 8, encoding: DW_ATE_unsigned_char, flags: DIFlagArtificial) | ||
| ; CHECK: !9 = !{i32 2, !"CodeView", i32 1} | ||
| ; CHECK: !10 = !{i32 2, !"Debug Info Version", i32 3} | ||
| ; CHECK: !11 = !{!"clang"} | ||
| ; CHECK: !12 = !{!"/alternatename:__CheckForDebuggerJustMyCode=__JustMyCode_Default"} | ||
| ; CHECK: !13 = distinct !DISubprogram(name: "f", scope: !3, file: !3, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !14 = !DISubroutineType(types: !15) | ||
| ; CHECK: !15 = !{null} | ||
| ; CHECK: !16 = !{} | ||
| ; CHECK: !17 = distinct !DISubprogram(name: "f", scope: !18, file: !18, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !18 = !DIFile(filename: "x.c", directory: "/tmp/a") | ||
| ; CHECK: !19 = distinct !DISubprogram(name: "f", scope: !7, file: !7, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !20 = distinct !DISubprogram(name: "f", scope: !21, file: !21, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !21 = !DIFile(filename: "./b\\x.c", directory: "C:\\\\a\\\\") | ||
| ; CHECK: !22 = distinct !DISubprogram(name: "f", scope: !23, file: !23, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !23 = !DIFile(filename: "./b/x.c", directory: "C:\\\\a\\\\") | ||
| ; CHECK: !24 = distinct !DISubprogram(name: "f", scope: !25, file: !25, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !16) | ||
| ; CHECK: !25 = !DIFile(filename: "./b/./../b/x.c", directory: "C:\\\\a") | ||
|
|
||
| ; All use the same flag | ||
| define void @l1() !dbg !10 { | ||
| ret void | ||
| } | ||
| define void @l2() !dbg !11 { | ||
| ret void | ||
| } | ||
|
|
||
| ; All use the same flag | ||
| define void @w1() !dbg !12 { | ||
| ret void | ||
| } | ||
| define void @w2() !dbg !13 { | ||
| ret void | ||
| } | ||
| define void @w3() !dbg !14 { | ||
| ret void | ||
| } | ||
| define void @w4() !dbg !15 { | ||
| ret void | ||
| } | ||
|
|
||
| !llvm.dbg.cu = !{!0} | ||
| !llvm.module.flags = !{!7, !8} | ||
| !llvm.ident = !{!9} | ||
|
|
||
| !0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None) | ||
| !1 = !DIFile(filename: "a/x.c", directory: "/tmp") | ||
| !2 = !DIFile(filename: "x.c", directory: "/tmp/a") | ||
| !3 = !DIFile(filename: "./x.c", directory: "C:\\\\a\\\\b\\\\") | ||
| !4 = !DIFile(filename: "./b\\x.c", directory: "C:\\\\a\\\\") | ||
| !5 = !DIFile(filename: "./b/x.c", directory: "C:\\\\a\\\\") | ||
| !6 = !DIFile(filename: "./b/./../b/x.c", directory: "C:\\\\a") | ||
| !7 = !{i32 2, !"CodeView", i32 1} | ||
| !8 = !{i32 2, !"Debug Info Version", i32 3} | ||
| !9 = !{!"clang"} | ||
| !10 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !11 = distinct !DISubprogram(name: "f", scope: !2, file: !2, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !12 = distinct !DISubprogram(name: "f", scope: !3, file: !3, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !13 = distinct !DISubprogram(name: "f", scope: !4, file: !4, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !14 = distinct !DISubprogram(name: "f", scope: !5, file: !5, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !15 = distinct !DISubprogram(name: "f", scope: !6, file: !6, line: 1, type: !31, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !33) | ||
| !31 = !DISubroutineType(types: !32) | ||
| !32 = !{null} | ||
| !33 = !{} |