Files
TheRock/compiler/amd-llvm/amd/comgr/test-unit/hotswap/rewriter/HotswapElfTest.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1242 lines
51 KiB
C++

//===- HotswapElfTest.cpp - Unit tests for HotSwap ELF layer --------------===//
//
// Part of Comgr, under the Apache License v2.0 with LLVM Exceptions. See
// amd/comgr/LICENSE.TXT in this repository for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "comgr-test-elf-utils.h"
#include "hotswap/rewriter/internal.h"
#include "gtest/gtest.h"
#include <array>
#include <cstring>
#include <limits>
using namespace COMGR::hotswap;
static std::vector<uint8_t> makeText(size_t Size = 16) {
return std::vector<uint8_t>(Size, 0);
}
static unsigned readReservedSgprs(const std::vector<uint8_t> &Bytes,
uint64_t KernelDescriptorOffset) {
namespace hsa = llvm::amdhsa;
uint32_t Rsrc1 = 0;
std::memcpy(&Rsrc1,
Bytes.data() + KernelDescriptorOffset +
offsetof(hsa::kernel_descriptor_t, compute_pgm_rsrc1),
sizeof(Rsrc1));
return (AMDHSA_BITS_GET(
Rsrc1, hsa::COMPUTE_PGM_RSRC1_GRANULATED_WAVEFRONT_SGPR_COUNT) +
1) *
8;
}
static unsigned readReservedVgprs(const std::vector<uint8_t> &Bytes,
uint64_t KernelDescriptorOffset,
unsigned Granule) {
namespace hsa = llvm::amdhsa;
uint32_t Rsrc1 = 0;
std::memcpy(&Rsrc1,
Bytes.data() + KernelDescriptorOffset +
offsetof(hsa::kernel_descriptor_t, compute_pgm_rsrc1),
sizeof(Rsrc1));
return (AMDHSA_BITS_GET(
Rsrc1, hsa::COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT) +
1) *
Granule;
}
// -- ElfView::create ----------------------------------------------------------
TEST(ElfView, RejectsTruncatedInput) {
uint8_t Garbage[] = {0x7f, 'E', 'L', 'F', 0, 0, 0, 0};
llvm::Expected<ElfView> ViewOrErr = ElfView::create(Garbage, sizeof(Garbage));
EXPECT_FALSE((bool)ViewOrErr);
llvm::consumeError(ViewOrErr.takeError());
}
TEST(ElfView, RejectsNonElfInput) {
uint8_t NotElf[64] = {};
llvm::Expected<ElfView> ViewOrErr = ElfView::create(NotElf, sizeof(NotElf));
EXPECT_FALSE((bool)ViewOrErr);
llvm::consumeError(ViewOrErr.takeError());
}
// -- ElfView::findKernelAtAddress ---------------------------------------------
TEST(ElfView, FindKernelAtAddressResolvesNearestPrecedingForZeroSizeSymbol) {
// AMDGPU kernel entry symbols frequently have st_size == 0 (the size lives on
// the .kd object symbol), so an exact [st_value, st_value + st_size)
// containment test never matches. The lookup must resolve via the
// nearest-preceding STT_FUNC symbol instead.
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "zero_size_kernel";
Opts.TextAddr = 0x1000;
Opts.ZeroSizeKernelSym = true;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
// findKernelAtAddress takes a virtual address; at the entry and at an
// interior offset the zero-size symbol still resolves.
EXPECT_EQ(ViewOrErr->findKernelAtAddress(0x1000), "zero_size_kernel");
EXPECT_EQ(ViewOrErr->findKernelAtAddress(0x1000 + 4), "zero_size_kernel");
std::optional<ElfView::FunctionTextRange> Range =
ViewOrErr->findFunctionTextRangeAtOffset(4);
ASSERT_TRUE(Range.has_value());
EXPECT_EQ(Range->Begin, 0u);
EXPECT_EQ(Range->End, makeText().size());
EXPECT_NE(Range->Symbol, nullptr);
EXPECT_NE(Range->Symtab, nullptr);
// An address before the symbol has no preceding function symbol to resolve.
EXPECT_EQ(ViewOrErr->findKernelAtAddress(0x0FF0), "");
}
// -- findNearestSled ----------------------------------------------------------
TEST(FindNearestSled, SkipsSledsOutsideInstructionFunctionRange) {
std::vector<NopSled> Sleds;
// {Start, End, WritePos, FunctionStart, FunctionEnd}
Sleds.push_back({/*Start=*/0, /*End=*/32, /*WritePos=*/0,
/*FunctionStart=*/0, /*FunctionEnd=*/32});
Sleds.push_back({/*Start=*/96, /*End=*/128, /*WritePos=*/96,
/*FunctionStart=*/96, /*FunctionEnd=*/160});
NopSled *Sled = findNearestSled(Sleds, 108, 8);
ASSERT_NE(Sled, nullptr);
EXPECT_EQ(Sled->Start, 96u);
EXPECT_EQ(findNearestSled(Sleds, 64, 8), nullptr);
}
// -- ElfView::getKernelStaticLdsSize ------------------------------------------
TEST(ElfView, GetKernelStaticLdsSizeReturnsNulloptWhenKdMissing) {
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText());
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelStaticLdsSize("nonexistent_kernel"),
std::nullopt);
}
TEST(ElfView, GetKernelStaticLdsSizeReadsLdsSizeFromKernelDescriptor) {
static constexpr uint32_t TestLdsSize = 16384;
comgr_test::KernelDescriptorElfOptions Opts;
Opts.ElfType = llvm::ELF::ET_REL;
Opts.KernelName = "test_kernel";
Opts.TextAddr = 0;
Opts.RodataAddr = 0;
Opts.GroupSegmentFixedSize = TestLdsSize;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
std::optional<uint32_t> Lds =
ViewOrErr->getKernelStaticLdsSize("test_kernel");
ASSERT_TRUE(Lds.has_value());
EXPECT_EQ(*Lds, TestLdsSize);
}
TEST(ElfView, KernelDescriptorsEnumeratesAndUpdatesEntryOffset) {
namespace hsa = llvm::amdhsa;
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
llvm::ArrayRef<KernelDescriptorInfo> KDs = ViewOrErr->kernelDescriptors();
ASSERT_EQ(KDs.size(), 1u);
EXPECT_EQ(KDs[0].KernelName, "entry_kernel");
EXPECT_EQ(KDs[0].VAddr, Obj.RodataAddr);
EXPECT_EQ(KDs[0].EntryOffset, Obj.EntryOffset);
EXPECT_EQ(ViewOrErr->getKernelDescriptorVAddr("entry_kernel"),
Obj.RodataAddr);
const int64_t NewOff = -128;
ASSERT_TRUE(
ViewOrErr->updateKernelDescriptorEntryOffset("entry_kernel", NewOff));
int64_t ReadBack = 0;
std::memcpy(
&ReadBack,
Obj.Bytes.data() + Obj.KernelDescriptorOffset +
offsetof(hsa::kernel_descriptor_t, kernel_code_entry_byte_offset),
sizeof(ReadBack));
EXPECT_EQ(ReadBack, NewOff);
ASSERT_EQ(ViewOrErr->kernelDescriptors().size(), 1u);
EXPECT_EQ(ViewOrErr->kernelDescriptors()[0].EntryOffset, NewOff);
// Prime the descriptor fallback cache before changing the encoded count.
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 8u);
ASSERT_TRUE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/true));
EXPECT_GE(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 10u);
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 16u);
}
TEST(ElfView, KernelDescriptorsSkipsKdWhenFileOffsetOverflows) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "overflow_kernel";
Opts.RodataAddr = 0x1000;
Opts.KernelDescriptorSymbolValue =
std::numeric_limits<uint64_t>::max() - 0x20;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_TRUE(ViewOrErr->kernelDescriptors().empty());
EXPECT_EQ(ViewOrErr->findKernelDescriptor("overflow_kernel"), nullptr);
}
// growWithTrampolines appends the pool at a fresh high virtual address instead
// of growing .text and shifting everything after it, so existing allocatable
// symbols keep their addresses. (This replaces the earlier test that pinned the
// buggy shifting behavior; the shift is exactly what corrupted the baked ISA
// references -- see GrowWithTrampolinesKeepsIsaReferenceConsistentWithSymbol.)
TEST(ElfView, GrowWithTrampolinesKeepsAllocSectionSymbols) {
static constexpr uint64_t GrowthBytes = 8;
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
Trampoline T;
T.Bytes.assign(GrowthBytes, 0);
std::vector<Trampoline> Trampolines;
Trampolines.push_back(T);
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Out =
ViewOrErr->growWithTrampolines(Trampolines, SNop);
ASSERT_NE(Out, nullptr);
uint8_t *OutData = reinterpret_cast<uint8_t *>(Out->getBufferStart());
llvm::Expected<ElfView> OutView =
ElfView::create(OutData, Out->getBufferSize());
ASSERT_TRUE((bool)OutView) << llvm::toString(OutView.takeError());
llvm::ArrayRef<KernelDescriptorInfo> KDs = OutView->kernelDescriptors();
ASSERT_EQ(KDs.size(), 1u);
EXPECT_EQ(KDs[0].VAddr, Obj.RodataAddr);
}
// gfx1250 "address of a global" idiom, as emitted for e.g. `x++` on a
// __managed__/__device__ global and observed in GPU_func of the
// Unit_hipModuleGetGlobal_Functional reproducer:
//
// s_get_pc_i64 s[0:1] ; s[0:1] = addr of next insn
// s_add_nc_u64 s[0:1], s[0:1], lit64(delta) ; s[0:1] = that addr + delta
//
// The 64-bit literal is baked at link time (no relocation) and encodes the
// distance from the s_add instruction to the referenced symbol. Reading it back
// out of .text is exactly how the hardware resolves the global's address, so it
// is the ground truth any rewrite must keep consistent with the symbol table.
namespace {
constexpr uint8_t SGetPcI64SS01[4] = {0xBE, 0x80, 0x47, 0x00};
constexpr uint8_t SAddNcU64Lit[4] = {0xA9, 0x80, 0xFE, 0x00};
constexpr size_t GetPcOffset = 0;
constexpr size_t AddOpOffset = 4; // s_get_pc_i64 is one dword
constexpr size_t Lit64Offset = 8; // + s_add_nc_u64 opcode dword
constexpr size_t RefSeqSize = 16; // + 8-byte lit64
// Build a .text image containing the reference idiom. The literal is computed
// so that, loaded at TextAddr, the ISA resolves the reference to TargetVAddr.
std::vector<uint8_t> makeTextReferencing(uint64_t TextAddr,
uint64_t TargetVAddr) {
std::vector<uint8_t> Text(RefSeqSize, 0);
std::memcpy(Text.data() + GetPcOffset, SGetPcI64SS01, sizeof(SGetPcI64SS01));
std::memcpy(Text.data() + AddOpOffset, SAddNcU64Lit, sizeof(SAddNcU64Lit));
// s_get_pc_i64 returns the address of the *following* instruction (the
// s_add), so the PC base the add works from is TextAddr + AddOpOffset.
const uint64_t PcBase = TextAddr + AddOpOffset;
const uint64_t Lit = TargetVAddr - PcBase; // two's-complement; forward here
std::memcpy(Text.data() + Lit64Offset, &Lit, sizeof(Lit));
return Text;
}
// Decode the reference idiom out of a .text image loaded at TextAddr and return
// the virtual address the ISA resolves it to.
uint64_t decodeReferencedVAddr(const uint8_t *Text, uint64_t TextAddr) {
uint64_t Lit = 0;
std::memcpy(&Lit, Text + Lit64Offset, sizeof(Lit));
return TextAddr + AddOpOffset + Lit;
}
} // namespace
// The real invariant: after appending trampolines, the address the *ISA*
// resolves a global reference to (decoded from the PC-relative literal in
// .text) must equal the address the *symbol table* reports for that global.
//
// This is the ELF-layer reproduction of Unit_hipModuleGetGlobal_Functional: the
// entry-trampoline rewrite grew .text, which shifted the referenced symbol by
// the trampoline size while leaving the baked literal pointing at the old
// location, so the ISA and the symbol table disagreed and the kernel
// dereferenced the wrong address. Here the descriptor symbol lives in .rodata
// (after .text), standing in for a global in a post-.text data section.
TEST(ElfView, GrowWithTrampolinesKeepsIsaReferenceConsistentWithSymbol) {
// One 256-byte entry stub, matching the real
// Unit_hipModuleGetGlobal_Functional reproducer's 0x100 shift.
static constexpr uint64_t GrowthBytes = 0x100;
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
// .text (at TextAddr) references the descriptor symbol in .rodata (at
// RodataAddr), which sits after .text -- like a kernel referencing a global
// in a post-.text data section.
std::vector<uint8_t> Text =
makeTextReferencing(Opts.TextAddr, Opts.RodataAddr);
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(Text, Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
// Sanity: before the rewrite, the ISA reference and the symbol agree.
ASSERT_EQ(decodeReferencedVAddr(ViewOrErr->textData(), ViewOrErr->textAddr()),
Obj.RodataAddr);
Trampoline T;
T.Bytes.assign(GrowthBytes, 0);
std::vector<Trampoline> Trampolines{T};
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Out =
ViewOrErr->growWithTrampolines(Trampolines, SNop);
ASSERT_NE(Out, nullptr);
uint8_t *OutData = reinterpret_cast<uint8_t *>(Out->getBufferStart());
llvm::Expected<ElfView> OutView =
ElfView::create(OutData, Out->getBufferSize());
ASSERT_TRUE((bool)OutView) << llvm::toString(OutView.takeError());
// Decode whatever is now in .text into the address the ISA resolves the
// reference to...
const uint64_t IsaResolved =
decodeReferencedVAddr(OutView->textData(), OutView->textAddr());
// ...vs. the address the symbol table now reports for the same global.
llvm::ArrayRef<KernelDescriptorInfo> KDs = OutView->kernelDescriptors();
ASSERT_EQ(KDs.size(), 1u);
const uint64_t SymbolVAddr = KDs[0].VAddr;
EXPECT_EQ(IsaResolved, SymbolVAddr);
}
// A fully-linked code object's DWARF encodes *absolute* virtual addresses of
// code and data (DW_AT_low_pc, DW_OP_addr, the .debug_addr pool, .debug_line
// set_address) with no relocations. The old growWithTrampolines shifted
// post-.text virtual addresses and symbols but left .debug_* contents
// untouched, so any such address went stale by the trampoline size -- the
// debugger would resolve a global to the pre-shift location. This is the
// debug-info analogue of the ISA-literal corruption above; the no-shift model
// keeps both in agreement.
namespace {
// Minimal ET_DYN AMDGPU object:
// [1] .text (alloc, exec) at TextAddr
// [2] .data (alloc, write) at DataAddr -- holds global "g"
// [3] .debug_info (non-alloc) -- 8-byte absolute address DWARF would
// encode for "g" (stands in for a
// DW_AT_low_pc / DW_OP_addr / .debug_addr
// entry)
// [4] .symtab [5] .strtab [6] .shstrtab
// Both .data and .debug_info follow .text.
struct DwarfRefElf {
std::vector<uint8_t> Bytes;
uint64_t DataAddr = 0;
unsigned DebugSectionIndex = 3;
unsigned SymtabSectionIndex = 4;
unsigned GlobalSymIndex = 1;
};
DwarfRefElf makeDwarfRefElf(uint64_t TextAddr = 0x1000,
uint64_t DataAddr = 0x2000) {
using namespace llvm::ELF;
constexpr uint64_t TextSize = 16;
constexpr unsigned SymCount = 2; // null + "g"
static const char ShStr[] =
"\0.text\0.data\0.debug_info\0.symtab\0.strtab\0.shstrtab\0";
constexpr uint32_t NameText = 1;
constexpr uint32_t NameData = 7;
constexpr uint32_t NameDebug = 13;
constexpr uint32_t NameSymtab = 25;
constexpr uint32_t NameStrtab = 33;
constexpr uint32_t NameShstrtab = 41;
static const char Str[] = "\0g\0";
constexpr uint32_t GNameOff = 1;
constexpr unsigned ShNum = 7;
const uint64_t ShOff = sizeof(Elf64_Ehdr);
const uint64_t TextOff =
comgr_test::alignTo8(ShOff + ShNum * sizeof(Elf64_Shdr));
const uint64_t DataOff = comgr_test::alignTo8(TextOff + TextSize);
const uint64_t DebugOff = comgr_test::alignTo8(DataOff + 8);
const uint64_t StrOff = comgr_test::alignTo8(DebugOff + 8);
const uint64_t SymOff = comgr_test::alignTo8(StrOff + sizeof(Str));
const uint64_t ShStrOff =
comgr_test::alignTo8(SymOff + SymCount * sizeof(Elf64_Sym));
const uint64_t BufSize = comgr_test::alignTo8(ShStrOff + sizeof(ShStr) + 64);
DwarfRefElf R;
R.Bytes.assign(BufSize, 0);
R.DataAddr = DataAddr;
uint8_t *B = R.Bytes.data();
Elf64_Ehdr Ehdr = comgr_test::makeElf64Ehdr(EM_AMDGPU);
Ehdr.e_ident[EI_OSABI] = ELFOSABI_AMDGPU_HSA;
Ehdr.e_type = ET_DYN;
Ehdr.e_version = EV_CURRENT;
Ehdr.e_shoff = ShOff;
Ehdr.e_ehsize = sizeof(Elf64_Ehdr);
Ehdr.e_shentsize = sizeof(Elf64_Shdr);
Ehdr.e_shnum = ShNum;
Ehdr.e_shstrndx = 6;
std::memcpy(B, &Ehdr, sizeof(Ehdr));
std::memcpy(B + StrOff, Str, sizeof(Str));
std::memcpy(B + ShStrOff, ShStr, sizeof(ShStr));
// The absolute address DWARF encodes for "g".
const uint64_t DebugAddr = DataAddr;
std::memcpy(B + DebugOff, &DebugAddr, sizeof(DebugAddr));
auto writeShdr = [&](unsigned Idx, const Elf64_Shdr &Sh) {
std::memcpy(B + ShOff + Idx * sizeof(Elf64_Shdr), &Sh, sizeof(Sh));
};
Elf64_Shdr Text{};
Text.sh_name = NameText;
Text.sh_type = SHT_PROGBITS;
Text.sh_flags = SHF_ALLOC | SHF_EXECINSTR;
Text.sh_offset = TextOff;
Text.sh_addr = TextAddr;
Text.sh_size = TextSize;
Text.sh_addralign = 4;
writeShdr(1, Text);
Elf64_Shdr Data{};
Data.sh_name = NameData;
Data.sh_type = SHT_PROGBITS;
Data.sh_flags = SHF_ALLOC | SHF_WRITE;
Data.sh_offset = DataOff;
Data.sh_addr = DataAddr;
Data.sh_size = 8;
Data.sh_addralign = 8;
writeShdr(2, Data);
Elf64_Shdr Debug{};
Debug.sh_name = NameDebug;
Debug.sh_type = SHT_PROGBITS;
Debug.sh_flags = 0; // non-alloc, like real .debug_* sections
Debug.sh_offset = DebugOff;
Debug.sh_size = 8;
Debug.sh_addralign = 1;
writeShdr(3, Debug);
Elf64_Shdr Symtab{};
Symtab.sh_name = NameSymtab;
Symtab.sh_type = SHT_SYMTAB;
Symtab.sh_offset = SymOff;
Symtab.sh_size = SymCount * sizeof(Elf64_Sym);
Symtab.sh_link = 5; // .strtab
Symtab.sh_info = 1;
Symtab.sh_entsize = sizeof(Elf64_Sym);
writeShdr(4, Symtab);
Elf64_Shdr Strtab{};
Strtab.sh_name = NameStrtab;
Strtab.sh_type = SHT_STRTAB;
Strtab.sh_offset = StrOff;
Strtab.sh_size = sizeof(Str);
writeShdr(5, Strtab);
Elf64_Shdr Shstr{};
Shstr.sh_name = NameShstrtab;
Shstr.sh_type = SHT_STRTAB;
Shstr.sh_offset = ShStrOff;
Shstr.sh_size = sizeof(ShStr);
writeShdr(6, Shstr);
Elf64_Sym G{};
G.st_name = GNameOff;
G.setBindingAndType(STB_GLOBAL, STT_OBJECT);
G.st_shndx = 2; // .data
G.st_value = DataAddr;
G.st_size = 8;
std::memcpy(B + SymOff + 1 * sizeof(Elf64_Sym), &G, sizeof(G));
return R;
}
// Read an 8-byte value from section [Idx] at intra-section byte offset Off.
uint64_t readSectionU64(const ElfView &V, unsigned Idx, uint64_t Off) {
uint64_t Val = 0;
std::memcpy(
&Val, V.data() + static_cast<uint64_t>(V.sections()[Idx].sh_offset) + Off,
sizeof(Val));
return Val;
}
// Read st_value of symbol SymIdx in the symbol table at section [SymtabIdx].
uint64_t readSymbolValue(const ElfView &V, unsigned SymtabIdx,
unsigned SymIdx) {
llvm::ELF::Elf64_Sym Sym{};
std::memcpy(&Sym,
V.data() +
static_cast<uint64_t>(V.sections()[SymtabIdx].sh_offset) +
SymIdx * sizeof(llvm::ELF::Elf64_Sym),
sizeof(Sym));
return Sym.st_value;
}
} // namespace
// The invariant: after appending trampolines, the address the object's DWARF
// encodes for a global must still equal the address the symbol table reports
// for it. The no-shift model leaves both untouched, so they agree (the old
// shift moved the symbol but not the DWARF address). Debug-info analogue of
// GrowWithTrampolinesKeepsIsaReferenceConsistentWithSymbol.
TEST(ElfView, GrowWithTrampolinesKeepsDwarfConsistentWithSymbol) {
static constexpr uint64_t GrowthBytes = 0x100;
DwarfRefElf Obj = makeDwarfRefElf();
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
// Sanity: before the rewrite, DWARF and the symbol agree.
ASSERT_EQ(readSectionU64(*ViewOrErr, Obj.DebugSectionIndex, 0), Obj.DataAddr);
ASSERT_EQ(
readSymbolValue(*ViewOrErr, Obj.SymtabSectionIndex, Obj.GlobalSymIndex),
Obj.DataAddr);
Trampoline T;
T.Bytes.assign(GrowthBytes, 0);
std::vector<Trampoline> Trampolines{T};
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Out =
ViewOrErr->growWithTrampolines(Trampolines, SNop);
ASSERT_NE(Out, nullptr);
uint8_t *OutData = reinterpret_cast<uint8_t *>(Out->getBufferStart());
llvm::Expected<ElfView> OutView =
ElfView::create(OutData, Out->getBufferSize());
ASSERT_TRUE((bool)OutView) << llvm::toString(OutView.takeError());
// Address DWARF still encodes for "g" ...
const uint64_t DwarfAddr = readSectionU64(*OutView, Obj.DebugSectionIndex, 0);
// ... vs. the address the symbol table now reports for "g".
const uint64_t SymbolVAddr =
readSymbolValue(*OutView, Obj.SymtabSectionIndex, Obj.GlobalSymIndex);
EXPECT_EQ(DwarfAddr, SymbolVAddr);
}
// Covers: addKernelEntryTrampolineSymbols attaches a distinct, correctly
// placed `<kernel>.stub` symbol for every appended entry-trampoline stub, so a
// dispatch whose entry now points at a stub still resolves to a name.
//
// How: build a synthetic AMDGPU code object that has a .symtab, then grow .text
// by two entry-stub-sized (KernelEntryStubStride) blocks with
// growWithTrampolines -- mirroring the pass appending one stub per kernel.
// Call addKernelEntryTrampolineSymbols with two fixups that use distinct kernel
// names and the two stub offsets (0 and KernelEntryStubStride). Re-parse the
// returned buffer with llvm::object::ELFFile and, for each fixup, assert a
// "<name>.stub" symbol exists in .symtab that is (a) STT_FUNC, (b) defined in
// the appended trampoline-pool section (st_shndx), (c) located at PoolVAddr +
// StubTextOffset (where the dispatch entry now points), and (d) sized to
// KernelEntryStubStride. Two fixups (rather than one) prove each stub gets its
// own name at its own address, not a single shared or mis-placed entry.
TEST(ElfView, AddKernelEntryTrampolineSymbolsNamesEachStub) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
std::optional<uint64_t> PoolVAddrOr = ViewOrErr->trampolinePoolVAddr();
ASSERT_TRUE(PoolVAddrOr.has_value());
const uint64_t PoolVAddr = *PoolVAddrOr;
// Grow .text by two entry-stub-sized blocks, mirroring the entry-trampoline
// pass appending one stub per kernel.
Trampoline Stub;
Stub.Bytes.assign(2 * KernelEntryStubStride, 0xAA);
std::vector<Trampoline> Growth{Stub};
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Grown =
ViewOrErr->growWithTrampolines(Growth, SNop);
ASSERT_NE(Grown, nullptr);
// One fixup per appended stub; the names need not match real kernels, since
// addKernelEntryTrampolineSymbols only attaches a symbol at each stub
// address.
std::vector<KernelEntryTrampolineFixup> Fixups = {
{"kernel_a", /*StubTextOffset=*/0, /*RequiredSgprs=*/10},
{"kernel_b", /*StubTextOffset=*/KernelEntryStubStride,
/*RequiredSgprs=*/12},
};
std::unique_ptr<llvm::WritableMemoryBuffer> WithSyms =
addKernelEntryTrampolineSymbols(*Grown, PoolVAddr, Fixups);
ASSERT_NE(WithSyms, nullptr);
using ELFT = llvm::object::ELF64LE;
const uint8_t *Data =
reinterpret_cast<const uint8_t *>(WithSyms->getBufferStart());
llvm::Expected<llvm::object::ELFFile<ELFT>> FileOrErr =
llvm::object::ELFFile<ELFT>::create(llvm::StringRef(
reinterpret_cast<const char *>(Data), WithSyms->getBufferSize()));
ASSERT_TRUE((bool)FileOrErr) << llvm::toString(FileOrErr.takeError());
llvm::object::ELFFile<ELFT> &File = *FileOrErr;
llvm::Expected<ELFT::ShdrRange> SecsOrErr = File.sections();
ASSERT_TRUE((bool)SecsOrErr) << llvm::toString(SecsOrErr.takeError());
const ELFT::Shdr *SymtabShdr = nullptr;
unsigned PoolIdx = 0;
{
unsigned I = 0;
for (const ELFT::Shdr &S : *SecsOrErr) {
if (S.sh_type == llvm::ELF::SHT_SYMTAB && !SymtabShdr)
SymtabShdr = &S;
if ((S.sh_flags & llvm::ELF::SHF_ALLOC) && S.sh_addr == PoolVAddr &&
PoolIdx == 0)
PoolIdx = I;
++I;
}
}
ASSERT_NE(SymtabShdr, nullptr);
ASSERT_NE(PoolIdx, 0u) << "trampoline-pool section not found at PoolVAddr";
llvm::Expected<ELFT::SymRange> SymsOrErr = File.symbols(SymtabShdr);
ASSERT_TRUE((bool)SymsOrErr) << llvm::toString(SymsOrErr.takeError());
llvm::Expected<llvm::StringRef> StrTabOrErr =
File.getStringTableForSymtab(*SymtabShdr);
ASSERT_TRUE((bool)StrTabOrErr) << llvm::toString(StrTabOrErr.takeError());
auto FindSym = [&](llvm::StringRef Name) -> const ELFT::Sym * {
for (const ELFT::Sym &Sym : *SymsOrErr) {
llvm::Expected<llvm::StringRef> N = Sym.getName(*StrTabOrErr);
if (N && *N == Name)
return &Sym;
}
return nullptr;
};
// Every appended stub must have a <kernel>.stub STT_FUNC symbol covering the
// stub, in the trampoline-pool section, at the stub's virtual address (where
// the dispatch entry now points).
for (const KernelEntryTrampolineFixup &F : Fixups) {
const ELFT::Sym *Sym = FindSym(F.KernelName + ".stub");
ASSERT_NE(Sym, nullptr) << "missing stub symbol for " << F.KernelName;
EXPECT_EQ(static_cast<unsigned>(Sym->getType()),
static_cast<unsigned>(llvm::ELF::STT_FUNC));
EXPECT_EQ(Sym->st_shndx, PoolIdx);
EXPECT_EQ(Sym->st_value, PoolVAddr + F.StubTextOffset);
EXPECT_EQ(Sym->st_size, KernelEntryStubStride);
// The symbol's [value, value + size) range must lie fully inside the
// section it claims (st_shndx), catching a symbol placed in the wrong
// section or past its bounds.
const ELFT::Shdr &StubSec = (*SecsOrErr)[Sym->st_shndx];
EXPECT_GE(Sym->st_value, StubSec.sh_addr);
EXPECT_LE(Sym->st_value + Sym->st_size, StubSec.sh_addr + StubSec.sh_size);
}
}
// A zero-sized allocatable section can share the trampoline pool's base
// address: trampolinePoolVAddr() still selects that address and
// growWithTrampolines() appends the real pool after it. The stub symbol must
// land in the real, non-empty pool section that spans PoolVAddr -- not the
// empty section at the same address -- so its [value, value + size) range stays
// inside its declared section. Guards the pool-section lookup against an exact
// sh_addr == PoolVAddr scan that could select the empty section first.
TEST(ElfView, AddKernelEntryTrampolineSymbolsSkipsZeroSizedSectionAtPoolVAddr) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.ExtraAllocSectionAddr = 0x3000; // page-aligned, past .rodata
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
std::optional<uint64_t> PoolVAddrOr = ViewOrErr->trampolinePoolVAddr();
ASSERT_TRUE(PoolVAddrOr.has_value());
const uint64_t PoolVAddr = *PoolVAddrOr;
// The zero-sized section shares the pool base -- the scenario under test.
ASSERT_EQ(PoolVAddr, 0x3000u);
Trampoline Stub;
Stub.Bytes.assign(KernelEntryStubStride, 0xAA);
std::vector<Trampoline> Growth{Stub};
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Grown =
ViewOrErr->growWithTrampolines(Growth, SNop);
ASSERT_NE(Grown, nullptr);
std::vector<KernelEntryTrampolineFixup> Fixups = {
{"kernel_a", /*StubTextOffset=*/0, /*RequiredSgprs=*/10}};
std::unique_ptr<llvm::WritableMemoryBuffer> WithSyms =
addKernelEntryTrampolineSymbols(*Grown, PoolVAddr, Fixups);
ASSERT_NE(WithSyms, nullptr);
using ELFT = llvm::object::ELF64LE;
llvm::Expected<llvm::object::ELFFile<ELFT>> FileOrErr =
llvm::object::ELFFile<ELFT>::create(llvm::StringRef(
reinterpret_cast<const char *>(WithSyms->getBufferStart()),
WithSyms->getBufferSize()));
ASSERT_TRUE((bool)FileOrErr) << llvm::toString(FileOrErr.takeError());
llvm::object::ELFFile<ELFT> &File = *FileOrErr;
llvm::Expected<ELFT::ShdrRange> SecsOrErr = File.sections();
ASSERT_TRUE((bool)SecsOrErr) << llvm::toString(SecsOrErr.takeError());
const ELFT::Shdr *Symtab = nullptr;
for (const ELFT::Shdr &S : *SecsOrErr)
if (S.sh_type == llvm::ELF::SHT_SYMTAB) {
Symtab = &S;
break;
}
ASSERT_NE(Symtab, nullptr);
llvm::Expected<ELFT::SymRange> Syms = File.symbols(Symtab);
ASSERT_TRUE((bool)Syms) << llvm::toString(Syms.takeError());
llvm::Expected<llvm::StringRef> Str = File.getStringTableForSymtab(*Symtab);
ASSERT_TRUE((bool)Str) << llvm::toString(Str.takeError());
const ELFT::Sym *StubSym = nullptr;
for (const ELFT::Sym &Sym : *Syms) {
llvm::Expected<llvm::StringRef> N = Sym.getName(*Str);
ASSERT_TRUE((bool)N) << llvm::toString(N.takeError());
if (*N == "kernel_a.stub") {
StubSym = &Sym;
break;
}
}
ASSERT_NE(StubSym, nullptr);
// The symbol must reference the real pool section (non-empty, spanning
// PoolVAddr), not the zero-sized section at the same address.
ASSERT_LT(StubSym->st_shndx, SecsOrErr->size());
const ELFT::Shdr &Sec = (*SecsOrErr)[StubSym->st_shndx];
EXPECT_NE(Sec.sh_size, 0u);
EXPECT_GE(StubSym->st_value, Sec.sh_addr);
EXPECT_LT(StubSym->st_value, Sec.sh_addr + Sec.sh_size);
EXPECT_EQ(StubSym->st_value, PoolVAddr);
}
TEST(ElfView, AddKernelEntryTrampolineSymbolsPreservesPhdr) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 8;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
std::optional<uint64_t> PoolVAddrOr = ViewOrErr->trampolinePoolVAddr();
ASSERT_TRUE(PoolVAddrOr.has_value());
const uint64_t PoolVAddr = *PoolVAddrOr;
Trampoline Stub;
Stub.Bytes.assign(2 * KernelEntryStubStride, 0xAA);
std::vector<Trampoline> Growth{Stub};
const uint8_t SNop[4] = {};
std::unique_ptr<llvm::WritableMemoryBuffer> Grown =
ViewOrErr->growWithTrampolines(Growth, SNop);
ASSERT_NE(Grown, nullptr);
std::vector<KernelEntryTrampolineFixup> Fixups = {
{"kernel_a", /*StubTextOffset=*/0, /*RequiredSgprs=*/10},
{"kernel_b", /*StubTextOffset=*/KernelEntryStubStride,
/*RequiredSgprs=*/12},
};
std::unique_ptr<llvm::WritableMemoryBuffer> WithSyms =
addKernelEntryTrampolineSymbols(*Grown, PoolVAddr, Fixups);
ASSERT_NE(WithSyms, nullptr);
using ELFT = llvm::object::ELF64LE;
llvm::Expected<llvm::object::ELFFile<ELFT>> FileOrErr =
llvm::object::ELFFile<ELFT>::create(llvm::StringRef(
WithSyms->getBufferStart(), WithSyms->getBufferSize()));
ASSERT_TRUE((bool)FileOrErr) << llvm::toString(FileOrErr.takeError());
llvm::Expected<ELFT::PhdrRange> PhdrsOrErr = FileOrErr->program_headers();
ASSERT_TRUE((bool)PhdrsOrErr) << llvm::toString(PhdrsOrErr.takeError());
EXPECT_GE(PhdrsOrErr->size(), 2u);
bool FoundPoolLoad = false;
for (const auto &Phdr : *PhdrsOrErr) {
EXPECT_LE(Phdr.p_offset, WithSyms->getBufferSize());
if (Phdr.p_type == llvm::ELF::PT_LOAD && (Phdr.p_flags & llvm::ELF::PF_X)) {
FoundPoolLoad = true;
EXPECT_GT(Phdr.p_filesz, 0u);
ASSERT_LE(Phdr.p_offset + Phdr.p_filesz, WithSyms->getBufferSize());
const uint8_t *PoolBytes =
reinterpret_cast<const uint8_t *>(WithSyms->getBufferStart()) +
Phdr.p_offset;
for (uint64_t I = 0; I < Phdr.p_filesz; ++I)
EXPECT_EQ(PoolBytes[I], 0xAA) << "pool content mismatch at byte " << I;
}
}
EXPECT_TRUE(FoundPoolLoad) << "no executable PT_LOAD segment found";
}
TEST(ElfView, UpdateKernelDescriptorSgprCountUpdatesMetadataAndDescriptor) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 8;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
// Prime the metadata cache before the in-place update.
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 8u);
ASSERT_TRUE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/true));
std::optional<unsigned> MetadataSgprs =
ViewOrErr->getKernelSgprCount("entry_kernel");
ASSERT_TRUE(MetadataSgprs.has_value());
EXPECT_EQ(*MetadataSgprs, 10u);
EXPECT_GE(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 10u);
}
TEST(ElfView, ReadsWorkgroupCapacityMetadata) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataVgprCount = 120;
Opts.MetadataMaxFlatWorkgroupSize = 1024;
Opts.MetadataWavefrontSize = 32;
Opts.MetadataClusterDims = std::array<unsigned, 3>{2, 3, 4};
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("entry_kernel"), 120u);
EXPECT_EQ(ViewOrErr->getKernelMaxFlatWorkgroupSize("entry_kernel"), 1024u);
EXPECT_EQ(ViewOrErr->getKernelWavefrontSize("entry_kernel"), 32u);
std::optional<KernelClusterDims> ClusterDims =
ViewOrErr->getKernelClusterDims("entry_kernel");
ASSERT_TRUE(ClusterDims);
EXPECT_EQ(ClusterDims->X, 2u);
EXPECT_EQ(ClusterDims->Y, 3u);
EXPECT_EQ(ClusterDims->Z, 4u);
}
TEST(ElfView, RejectsMalformedClusterDimsMetadata) {
using MalformedClusterDims =
comgr_test::KernelDescriptorElfOptions::MalformedClusterDims;
constexpr MalformedClusterDims Cases[] = {
MalformedClusterDims::NotArray,
MalformedClusterDims::WrongArity,
MalformedClusterDims::NonInteger,
};
for (MalformedClusterDims Case : Cases) {
SCOPED_TRACE(static_cast<unsigned>(Case));
comgr_test::KernelDescriptorElfOptions Opts;
Opts.MetadataVgprCount = 120;
Opts.MalformedMetadataClusterDims = Case;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelClusterDims("kernel"), std::nullopt);
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("kernel"), 120u);
}
}
TEST(ElfView, CachesAllMetadataFieldsPerKernelAndDeduplicatesNames) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.MetadataKernels = {
{"first", 8, 16, 128, 32, std::array<unsigned, 3>{1, 2, 3}},
{"second", 12, 24, 256, 64, std::array<unsigned, 3>{4, 5, 6}},
{"first", 99, 99, 999, 99, std::array<unsigned, 3>{9, 9, 9}},
};
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelSgprCount("first"), 8u);
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("first"), 16u);
EXPECT_EQ(ViewOrErr->getKernelMaxFlatWorkgroupSize("first"), 128u);
EXPECT_EQ(ViewOrErr->getKernelWavefrontSize("first"), 32u);
std::optional<KernelClusterDims> FirstDims =
ViewOrErr->getKernelClusterDims("first");
ASSERT_TRUE(FirstDims);
EXPECT_EQ(FirstDims->X, 1u);
EXPECT_EQ(FirstDims->Y, 2u);
EXPECT_EQ(FirstDims->Z, 3u);
EXPECT_EQ(ViewOrErr->getKernelSgprCount("second"), 12u);
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("second"), 24u);
EXPECT_EQ(ViewOrErr->getKernelMaxFlatWorkgroupSize("second"), 256u);
EXPECT_EQ(ViewOrErr->getKernelWavefrontSize("second"), 64u);
std::optional<KernelClusterDims> SecondDims =
ViewOrErr->getKernelClusterDims("second");
ASSERT_TRUE(SecondDims);
EXPECT_EQ(SecondDims->X, 4u);
EXPECT_EQ(SecondDims->Y, 5u);
EXPECT_EQ(SecondDims->Z, 6u);
}
TEST(ElfView, MalformedMetadataLeavesTerminalErrorCacheState) {
comgr_test::KernelDescriptorElfOptions ValidOpts;
ValidOpts.MetadataVgprCount = 120;
std::string ValidBlob = comgr_test::makeAmdgpuMetadataBlob(ValidOpts);
comgr_test::KernelDescriptorElfOptions Opts;
Opts.RawMetadataBlob = std::string(ValidBlob.size(), '\xc1');
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::StringRef Bytes(reinterpret_cast<const char *>(Obj.Bytes.data()),
Obj.Bytes.size());
size_t BlobOffset = Bytes.find(*Opts.RawMetadataBlob);
ASSERT_NE(BlobOffset, llvm::StringRef::npos);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("kernel"), std::nullopt);
std::memcpy(Obj.Bytes.data() + BlobOffset, ValidBlob.data(),
ValidBlob.size());
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("kernel"), std::nullopt);
llvm::Expected<ElfView> FreshViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)FreshViewOrErr)
<< llvm::toString(FreshViewOrErr.takeError());
EXPECT_EQ(FreshViewOrErr->getKernelMetadataVgprCount("kernel"), 120u);
}
TEST(ElfView, UpdateKernelDescriptorVgprCountIsChecked) {
static constexpr unsigned Granule = 16;
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(readReservedVgprs(Obj.Bytes, Obj.KernelDescriptorOffset, Granule),
16u);
ASSERT_TRUE(
ViewOrErr->updateKernelDescriptorVgprCount("entry_kernel", 17, Granule));
EXPECT_EQ(readReservedVgprs(Obj.Bytes, Obj.KernelDescriptorOffset, Granule),
32u);
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorVgprCount("entry_kernel", 4096,
Granule));
EXPECT_EQ(readReservedVgprs(Obj.Bytes, Obj.KernelDescriptorOffset, Granule),
32u);
}
TEST(ElfView, UpdateKernelMetadataVgprCountsUpdatesInPlace) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataVgprCount = 9;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("entry_kernel"), 9u);
llvm::StringMap<unsigned> RequiredVgprs;
RequiredVgprs.try_emplace("entry_kernel", 10u);
ASSERT_TRUE(ViewOrErr->updateKernelMetadataVgprCounts(RequiredVgprs));
EXPECT_EQ(ViewOrErr->getKernelMetadataVgprCount("entry_kernel"), 10u);
}
TEST(ElfView, UpdateKernelMetadataVgprCountsRejectsMissingCount) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataOmitVgprCount = true;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
llvm::StringMap<unsigned> RequiredVgprs;
RequiredVgprs.try_emplace("entry_kernel", 10u);
EXPECT_FALSE(ViewOrErr->updateKernelMetadataVgprCounts(RequiredVgprs));
}
TEST(ElfView, UpdateGfx1250RevisionMetadataRetagsKernelInPlace) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.MetadataSgprCount = 8;
Opts.MetadataGfx1250Revision = "B0";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::StringRef Before(reinterpret_cast<const char *>(Obj.Bytes.data()),
Obj.Bytes.size());
EXPECT_NE(Before.find("B0"), llvm::StringRef::npos);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
ASSERT_TRUE(ViewOrErr->updateGfx1250RevisionMetadata("A0"));
llvm::StringRef After(reinterpret_cast<const char *>(Obj.Bytes.data()),
Obj.Bytes.size());
EXPECT_EQ(After.find("B0"), llvm::StringRef::npos);
EXPECT_NE(After.find("A0"), llvm::StringRef::npos);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountCanUpdateMetadataOnly) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 8;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
const unsigned DescriptorSgprsBefore =
readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset);
ASSERT_TRUE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/false));
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 10u);
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset),
DescriptorSgprsBefore);
}
TEST(ElfView, UpdateKernelMetadataSgprCountsKeepsPrimedCacheCoherent) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 8;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 8u);
llvm::StringMap<unsigned> RequiredSgprs;
RequiredSgprs.try_emplace("entry_kernel", 10u);
ASSERT_TRUE(ViewOrErr->updateKernelMetadataSgprCounts(RequiredSgprs));
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), 10u);
}
TEST(ElfView, UpdateKernelMetadataSgprCountsBatchesMixedRequirements) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.MetadataKernels = {{"needs_update", 8}, {"already_enough", 16}};
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
llvm::StringMap<unsigned> RequiredSgprs;
RequiredSgprs.try_emplace("needs_update", 10u);
RequiredSgprs.try_emplace("already_enough", 12u);
ASSERT_TRUE(ViewOrErr->updateKernelMetadataSgprCounts(RequiredSgprs));
EXPECT_EQ(ViewOrErr->getKernelSgprCount("needs_update"), 10u);
EXPECT_EQ(ViewOrErr->getKernelSgprCount("already_enough"), 16u);
}
TEST(ElfView, UpdateKernelMetadataSgprCountsRejectsAbsentKernelAtomically) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.MetadataKernels = {{"needs_update", 8}, {"already_enough", 16}};
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
llvm::StringMap<unsigned> RequiredSgprs;
RequiredSgprs.try_emplace("needs_update", 10u);
RequiredSgprs.try_emplace("already_enough", 12u);
RequiredSgprs.try_emplace("absent_kernel", 4u);
EXPECT_FALSE(ViewOrErr->updateKernelMetadataSgprCounts(RequiredSgprs));
EXPECT_EQ(ViewOrErr->getKernelSgprCount("needs_update"), 8u);
EXPECT_EQ(ViewOrErr->getKernelSgprCount("already_enough"), 16u);
EXPECT_EQ(ViewOrErr->getKernelSgprCount("absent_kernel"), std::nullopt);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountMetadataOnlyRequiresMetadata) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
const unsigned DescriptorSgprsBefore =
readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset);
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/false));
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset),
DescriptorSgprsBefore);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountRejectsMissingMetadataCount) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataOmitSgprCount = true;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/true));
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 8u);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountRejectsMissingMetadataKernel) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataKernelName = "other_kernel";
Opts.MetadataSgprCount = 8;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), std::nullopt);
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/true));
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 8u);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountRejectsNonIntegerMetadataCount) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCountAsString = true;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_EQ(ViewOrErr->getKernelSgprCount("entry_kernel"), std::nullopt);
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 10, /*UpdateDescriptor=*/true));
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 8u);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountRejectsMetadataSizeChange) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 9;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 128, /*UpdateDescriptor=*/true));
std::optional<unsigned> MetadataSgprs =
ViewOrErr->getKernelSgprCount("entry_kernel");
ASSERT_TRUE(MetadataSgprs.has_value());
EXPECT_EQ(*MetadataSgprs, 9u);
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 8u);
}
TEST(ElfView, UpdateKernelDescriptorSgprCountRejectsDescriptorLimitFirst) {
comgr_test::KernelDescriptorElfOptions Opts;
Opts.KernelName = "entry_kernel";
Opts.MetadataSgprCount = 200;
comgr_test::KernelDescriptorElf Obj =
comgr_test::makeKernelDescriptorElf(makeText(), Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Obj.Bytes.data(), Obj.Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
EXPECT_FALSE(ViewOrErr->updateKernelDescriptorSgprCount(
"entry_kernel", 100000, /*UpdateDescriptor=*/true));
std::optional<unsigned> MetadataSgprs =
ViewOrErr->getKernelSgprCount("entry_kernel");
ASSERT_TRUE(MetadataSgprs.has_value());
EXPECT_EQ(*MetadataSgprs, 200u);
EXPECT_EQ(readReservedSgprs(Obj.Bytes, Obj.KernelDescriptorOffset), 8u);
}
// -- kernel-descriptor cache dedup --------------------------------------------
// The descriptor cache dedups by (name, vaddr): the same kernel name can appear
// at more than one descriptor vaddr, and a repeat of an already-seen
// (name, vaddr) pair must be dropped regardless of ordering. This pins the
// A@x, A@y, A@x case, which a "remember only the last vaddr per name" dedup
// would mishandle by re-adding the trailing A@x.
TEST(ElfView, KernelDescriptorCacheDedupsByNameAndVAddrAcrossOrdering) {
comgr_test::MultiKernelDescriptorElfOptions Opts;
Opts.TextAddr = 0x1000;
Opts.TextSize = 0x400;
Opts.RodataAddr = 0x2000;
// Distinct descriptor vaddrs for the two "A" instances, and a "B" in between,
// then a duplicate of the first A. Expect three unique descriptors:
// A@0x2000, B@0x2100, A@0x2200 -- the trailing A@0x2000 duplicate is dropped.
Opts.Kernels = {
{"kern_a", 0x1000, 0x2000, /*EntryOffset=*/-0x1000},
{"kern_b", 0x1100, 0x2100, /*EntryOffset=*/-0x1000},
{"kern_a", 0x1200, 0x2200, /*EntryOffset=*/-0x1000},
{"kern_a", 0x1000, 0x2000, /*EntryOffset=*/-0x1000}, // dup of first
};
std::vector<uint8_t> Bytes = comgr_test::makeMultiKernelDescriptorElf(Opts);
llvm::Expected<ElfView> ViewOrErr =
ElfView::create(Bytes.data(), Bytes.size());
ASSERT_TRUE((bool)ViewOrErr) << llvm::toString(ViewOrErr.takeError());
llvm::ArrayRef<KernelDescriptorInfo> KDs = ViewOrErr->kernelDescriptors();
ASSERT_EQ(KDs.size(), 3u);
EXPECT_EQ(KDs[0].KernelName, "kern_a");
EXPECT_EQ(KDs[0].VAddr, 0x2000u);
EXPECT_EQ(KDs[1].KernelName, "kern_b");
EXPECT_EQ(KDs[1].VAddr, 0x2100u);
EXPECT_EQ(KDs[2].KernelName, "kern_a");
EXPECT_EQ(KDs[2].VAddr, 0x2200u);
}