blob: 7b9b9a416c35da622969b4f5b97344deaa1a6a01 [file]
//===- OffloadBinary.cpp - Utilities for handling offloading code ---------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "llvm/Object/OffloadBinary.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/BinaryFormat/Magic.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/Module.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/MC/StringTableBuilder.h"
#include "llvm/Object/Archive.h"
#include "llvm/Object/Binary.h"
#include "llvm/Object/ELFObjectFile.h"
#include "llvm/Object/Error.h"
#include "llvm/Object/IRObjectFile.h"
#include "llvm/Object/ObjectFile.h"
#include "llvm/Support/Alignment.h"
#include "llvm/Support/Compression.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/TargetParser/AMDGPUTargetParser.h"
#include <cassert>
#include <cstddef>
#include <memory>
using namespace llvm;
using namespace llvm::object;
namespace {
/// Attempts to extract all the embedded device images contained inside the
/// buffer \p Contents. The buffer is expected to contain a valid offloading
/// binary format.
Error extractOffloadFiles(MemoryBufferRef Contents,
SmallVectorImpl<OffloadFile> &Binaries) {
uint64_t Offset = 0;
// There could be multiple offloading binaries stored at this section.
while (Offset < Contents.getBufferSize()) {
std::unique_ptr<MemoryBuffer> Buffer =
MemoryBuffer::getMemBuffer(Contents.getBuffer().drop_front(Offset), "",
/*RequiresNullTerminator*/ false);
if (!isAddrAligned(Align(OffloadBinary::getAlignment()),
Buffer->getBufferStart()))
Buffer = MemoryBuffer::getMemBufferCopy(Buffer->getBuffer(),
Buffer->getBufferIdentifier());
auto HeaderOrErr = OffloadBinary::extractHeader(*Buffer);
if (!HeaderOrErr)
return HeaderOrErr.takeError();
const OffloadBinary::Header *Header = *HeaderOrErr;
MemoryBufferRef Slice(Buffer->getBuffer().take_front(Header->Size),
Contents.getBufferIdentifier());
auto BinariesOrErr = OffloadBinary::create(Slice);
if (!BinariesOrErr)
return BinariesOrErr.takeError();
for (auto &Binary : *BinariesOrErr)
Binaries.emplace_back(std::move(Binary));
Offset = alignTo(Offset + Header->Size, OffloadBinary::getAlignment());
}
return Error::success();
}
// Extract offloading binaries from an Object file \p Obj.
Error extractFromObject(const ObjectFile &Obj,
SmallVectorImpl<OffloadFile> &Binaries) {
assert((Obj.isELF() || Obj.isCOFF()) && "Invalid file type");
for (SectionRef Sec : Obj.sections()) {
// ELF files contain a section with the LLVM_OFFLOADING type.
if (Obj.isELF() &&
static_cast<ELFSectionRef>(Sec).getType() != ELF::SHT_LLVM_OFFLOADING)
continue;
// COFF has no section types so we rely on the name of the section.
if (Obj.isCOFF()) {
Expected<StringRef> NameOrErr = Sec.getName();
if (!NameOrErr)
return NameOrErr.takeError();
if (!NameOrErr->starts_with(".llvm.offloading"))
continue;
}
Expected<StringRef> Buffer = Sec.getContents();
if (!Buffer)
return Buffer.takeError();
MemoryBufferRef Contents(*Buffer, Obj.getFileName());
if (Error Err = extractOffloadFiles(Contents, Binaries))
return Err;
}
return Error::success();
}
Error extractFromBitcode(MemoryBufferRef Buffer,
SmallVectorImpl<OffloadFile> &Binaries) {
LLVMContext Context;
SMDiagnostic Err;
std::unique_ptr<Module> M = getLazyIRModule(
MemoryBuffer::getMemBuffer(Buffer, /*RequiresNullTerminator=*/false), Err,
Context);
if (!M)
return createStringError(inconvertibleErrorCode(),
"Failed to create module");
// Extract offloading data from globals referenced by the
// `llvm.embedded.object` metadata with the `.llvm.offloading` section.
auto *MD = M->getNamedMetadata("llvm.embedded.objects");
if (!MD)
return Error::success();
for (const MDNode *Op : MD->operands()) {
if (Op->getNumOperands() < 2)
continue;
MDString *SectionID = dyn_cast<MDString>(Op->getOperand(1));
if (!SectionID || SectionID->getString() != ".llvm.offloading")
continue;
GlobalVariable *GV =
mdconst::dyn_extract_or_null<GlobalVariable>(Op->getOperand(0));
if (!GV)
continue;
auto *CDS = dyn_cast<ConstantDataSequential>(GV->getInitializer());
if (!CDS)
continue;
MemoryBufferRef Contents(CDS->getAsString(), M->getName());
if (Error Err = extractOffloadFiles(Contents, Binaries))
return Err;
}
return Error::success();
}
Error extractFromArchive(const Archive &Library,
SmallVectorImpl<OffloadFile> &Binaries) {
// Try to extract device code from each file stored in the static archive.
Error Err = Error::success();
for (auto Child : Library.children(Err)) {
auto ChildBufferOrErr = Child.getMemoryBufferRef();
if (!ChildBufferOrErr)
return ChildBufferOrErr.takeError();
std::unique_ptr<MemoryBuffer> ChildBuffer =
MemoryBuffer::getMemBuffer(*ChildBufferOrErr, false);
// Check if the buffer has the required alignment.
if (!isAddrAligned(Align(OffloadBinary::getAlignment()),
ChildBuffer->getBufferStart()))
ChildBuffer = MemoryBuffer::getMemBufferCopy(
ChildBufferOrErr->getBuffer(),
ChildBufferOrErr->getBufferIdentifier());
if (Error Err = extractOffloadBinaries(*ChildBuffer, Binaries))
return Err;
}
if (Err)
return Err;
return Error::success();
}
bool isCompressed(const OffloadBinary::Header &Header) {
return Header.Version >= 3 && Header.InflatedSize != 0;
}
Expected<std::unique_ptr<MemoryBuffer>>
decompressOffloadBinary(MemoryBufferRef Buf) {
const auto *Header =
reinterpret_cast<const OffloadBinary::Header *>(Buf.getBufferStart());
if (Header->EntriesOffset != sizeof(OffloadBinary::Header) ||
Header->EntriesOffset > Header->Size ||
Header->InflatedSize < Header->EntriesOffset)
return errorCodeToError(object_error::unexpected_eof);
// Get the compressed binary blob after the header.
StringRef Compressed = Buf.getBuffer()
.take_front(Header->Size)
.drop_front(Header->EntriesOffset);
uint64_t BodySize = Header->InflatedSize - Header->EntriesOffset;
SmallVector<uint8_t, 0> Body;
if (Error Err = compression::decompress(arrayRefFromStringRef(Compressed),
Body, BodySize))
return std::move(Err);
// Restore the old header data for the newly uncompressed blob.
OffloadBinary::Header Restored = *Header;
Restored.Size = Restored.InflatedSize;
Restored.InflatedSize = 0;
if (Restored.EntriesOffset + Body.size() != Restored.Size)
return errorCodeToError(object_error::parse_failed);
SmallString<0> Out;
Out.reserve(Restored.Size);
Out.append(StringRef(reinterpret_cast<const char *>(&Restored),
sizeof(OffloadBinary::Header)));
Out.append(toStringRef(Body));
return MemoryBuffer::getMemBufferCopy(Out, Buf.getBufferIdentifier());
}
} // namespace
Expected<const OffloadBinary::Header *>
OffloadBinary::extractHeader(MemoryBufferRef Buf) {
if (Buf.getBufferSize() < sizeof(Header))
return errorCodeToError(object_error::parse_failed);
// Check for 0x10FF1OAD magic bytes.
if (identify_magic(Buf.getBuffer()) != file_magic::offload_binary)
return errorCodeToError(object_error::parse_failed);
// Make sure that the data has sufficient alignment.
if (!isAddrAligned(Align(getAlignment()), Buf.getBufferStart()))
return errorCodeToError(object_error::parse_failed);
const char *Start = Buf.getBufferStart();
const Header *TheHeader = reinterpret_cast<const Header *>(Start);
if (TheHeader->Version == 0 || TheHeader->Version > OffloadBinary::Version)
return errorCodeToError(object_error::parse_failed);
if (TheHeader->Size > Buf.getBufferSize() || TheHeader->Size < sizeof(Header))
return errorCodeToError(object_error::unexpected_eof);
if (isCompressed(*TheHeader))
return TheHeader;
if (TheHeader->Size < sizeof(Entry))
return errorCodeToError(object_error::unexpected_eof);
uint64_t EntriesCount =
(TheHeader->Version == 1) ? 1 : TheHeader->EntriesCount;
uint64_t EntriesSize = sizeof(Entry) * EntriesCount;
if (TheHeader->EntriesOffset > TheHeader->Size - EntriesSize ||
// v1/v2 headers are 32 bytes; sizeof(Header) grew in v3.
EntriesSize > TheHeader->Size - offsetof(Header, InflatedSize))
return errorCodeToError(object_error::unexpected_eof);
return TheHeader;
}
Expected<SmallVector<std::unique_ptr<OffloadBinary>>>
OffloadBinary::create(MemoryBufferRef Buf, std::optional<uint64_t> Index) {
auto HeaderOrErr = extractHeader(Buf);
if (!HeaderOrErr)
return HeaderOrErr.takeError();
const Header *OnDisk = *HeaderOrErr;
// The binary data may be a compressed image.
std::shared_ptr<MemoryBuffer> Binary;
if (isCompressed(*OnDisk)) {
auto DecompressedOrErr = decompressOffloadBinary(Buf);
if (!DecompressedOrErr)
return DecompressedOrErr.takeError();
Binary = std::shared_ptr<MemoryBuffer>(std::move(*DecompressedOrErr));
} else {
Binary = std::shared_ptr<MemoryBuffer>(MemoryBuffer::getMemBufferCopy(
Buf.getBuffer().take_front(OnDisk->Size), Buf.getBufferIdentifier()));
}
// Owned is now an uncompressed OffloadBinary, parse it as before.
MemoryBufferRef Owned = *Binary;
HeaderOrErr = extractHeader(Owned);
if (!HeaderOrErr)
return HeaderOrErr.takeError();
const Header *TheHeader = *HeaderOrErr;
const char *Start = Owned.getBufferStart();
const Entry *Entries =
reinterpret_cast<const Entry *>(&Start[TheHeader->EntriesOffset]);
auto validateEntry = [&](const Entry *TheEntry) -> Error {
const uint64_t BufSize = Owned.getBufferSize();
if (TheEntry->ImageOffset > BufSize ||
TheEntry->ImageSize > BufSize - TheEntry->ImageOffset)
return errorCodeToError(object_error::unexpected_eof);
const size_t StringEntrySize =
TheHeader->Version == 1 ? sizeof(StringEntryV1) : sizeof(StringEntry);
if (TheEntry->StringOffset > BufSize ||
TheEntry->NumStrings >
(BufSize - TheEntry->StringOffset) / StringEntrySize)
return errorCodeToError(object_error::unexpected_eof);
return Error::success();
};
SmallVector<std::unique_ptr<OffloadBinary>> Binaries;
if (TheHeader->Version > 1 && Index.has_value()) {
if (*Index >= TheHeader->EntriesCount)
return errorCodeToError(object_error::parse_failed);
const Entry *TheEntry = &Entries[*Index];
if (auto Err = validateEntry(TheEntry))
return std::move(Err);
Binaries.emplace_back(
new OffloadBinary(Binary, TheHeader, TheEntry, *Index));
return std::move(Binaries);
}
uint64_t EntriesCount = TheHeader->Version == 1 ? 1 : TheHeader->EntriesCount;
for (uint64_t I = 0; I < EntriesCount; ++I) {
const Entry *TheEntry = &Entries[I];
if (auto Err = validateEntry(TheEntry))
return std::move(Err);
Binaries.emplace_back(new OffloadBinary(Binary, TheHeader, TheEntry, I));
}
return std::move(Binaries);
}
SmallString<0> OffloadBinary::write(ArrayRef<OffloadingImage> OffloadingData) {
uint64_t EntriesCount = OffloadingData.size();
assert(EntriesCount > 0 && "At least one offloading image is required");
// Create a null-terminated string table with all the used strings.
// Also calculate total size of images.
StringTableBuilder StrTab(StringTableBuilder::ELF);
uint64_t TotalStringEntries = 0;
uint64_t TotalImagesSize = 0;
for (const OffloadingImage &Img : OffloadingData) {
for (auto &KeyAndValue : Img.StringData) {
StrTab.add(KeyAndValue.first);
StrTab.add(KeyAndValue.second);
}
TotalStringEntries += Img.StringData.size();
TotalImagesSize += Img.Image->getBufferSize();
}
StrTab.finalize();
uint64_t StringEntrySize = sizeof(StringEntry) * TotalStringEntries;
uint64_t EntriesSize = sizeof(Entry) * EntriesCount;
uint64_t StrTabOffset = sizeof(Header) + EntriesSize + StringEntrySize;
// Make sure the image we're wrapping around is aligned as well.
uint64_t BinaryDataSize =
alignTo(StrTabOffset + StrTab.getSize(), getAlignment());
// Create the header and fill in the offsets. The entries will be directly
// placed after the header in memory. Align the size to the alignment of the
// header so this can be placed contiguously in a single section.
Header TheHeader{};
TheHeader.Size = alignTo(BinaryDataSize + TotalImagesSize, getAlignment());
TheHeader.EntriesOffset = sizeof(Header);
TheHeader.EntriesCount = EntriesCount;
SmallString<0> Data;
Data.reserve(TheHeader.Size);
raw_svector_ostream OS(Data);
OS << StringRef(reinterpret_cast<char *>(&TheHeader), sizeof(Header));
// Create the entries using the string table offsets. The string table will be
// placed directly after the set of entries in memory, and all the images are
// after that.
uint64_t StringEntryOffset = sizeof(Header) + EntriesSize;
uint64_t ImageOffset = BinaryDataSize;
for (const OffloadingImage &Img : OffloadingData) {
Entry TheEntry;
TheEntry.TheImageKind = Img.TheImageKind;
TheEntry.TheOffloadKind = Img.TheOffloadKind;
TheEntry.Flags = Img.Flags;
TheEntry.StringOffset = StringEntryOffset;
StringEntryOffset += sizeof(StringEntry) * Img.StringData.size();
TheEntry.NumStrings = Img.StringData.size();
TheEntry.ImageOffset = ImageOffset;
ImageOffset += Img.Image->getBufferSize();
TheEntry.ImageSize = Img.Image->getBufferSize();
OS << StringRef(reinterpret_cast<char *>(&TheEntry), sizeof(Entry));
}
// Create the string map entries.
for (const OffloadingImage &Img : OffloadingData) {
for (auto &KeyAndValue : Img.StringData) {
StringEntry Map{StrTabOffset + StrTab.getOffset(KeyAndValue.first),
StrTabOffset + StrTab.getOffset(KeyAndValue.second),
KeyAndValue.second.size()};
OS << StringRef(reinterpret_cast<char *>(&Map), sizeof(StringEntry));
}
}
StrTab.write(OS);
// Add padding to required image alignment.
OS.write_zeros(BinaryDataSize - OS.tell());
for (const OffloadingImage &Img : OffloadingData)
OS << Img.Image->getBuffer();
// Add final padding to required alignment.
assert(TheHeader.Size >= OS.tell() && "Too much data written?");
OS.write_zeros(TheHeader.Size - OS.tell());
assert(TheHeader.Size == OS.tell() && "Size mismatch");
return Data;
}
Expected<SmallString<0>>
OffloadBinary::write(ArrayRef<OffloadingImage> OffloadingData,
compression::Params Compress) {
if (const char *Reason = compression::getReasonIfUnsupported(Compress.format))
return createStringError(Reason);
// Write the complete offloading binary as normal.
SmallString<0> Uncompressed = write(OffloadingData);
OffloadBinary::Header Header =
*reinterpret_cast<const OffloadBinary::Header *>(Uncompressed.data());
// Compress the entries after the header with the requested configuration.
StringRef Body = StringRef(Uncompressed).drop_front(Header.EntriesOffset);
SmallVector<uint8_t, 0> CompressedBuffer;
compression::compress(Compress, arrayRefFromStringRef(Body),
CompressedBuffer);
// Reset the header sizes and create the newly compressed binary.
Header.InflatedSize = Uncompressed.size();
Header.Size = Header.EntriesOffset + CompressedBuffer.size();
SmallString<0> Data;
Data.reserve(alignTo(Header.Size, getAlignment()));
raw_svector_ostream OS(Data);
OS << StringRef(reinterpret_cast<const char *>(&Header),
Header.EntriesOffset);
OS << toStringRef(CompressedBuffer);
assert(Header.Size == OS.tell() && "Size mismatch");
OS.write_zeros(alignTo(Header.Size, getAlignment()) - Header.Size);
return Data;
}
Error object::extractOffloadBinaries(MemoryBufferRef Buffer,
SmallVectorImpl<OffloadFile> &Binaries) {
file_magic Type = identify_magic(Buffer.getBuffer());
switch (Type) {
case file_magic::bitcode:
return extractFromBitcode(Buffer, Binaries);
case file_magic::elf_relocatable:
case file_magic::elf_executable:
case file_magic::elf_shared_object:
case file_magic::coff_object: {
Expected<std::unique_ptr<ObjectFile>> ObjFile =
ObjectFile::createObjectFile(Buffer, Type);
if (!ObjFile)
return ObjFile.takeError();
return extractFromObject(*ObjFile->get(), Binaries);
}
case file_magic::archive: {
Expected<std::unique_ptr<llvm::object::Archive>> LibFile =
object::Archive::create(Buffer);
if (!LibFile)
return LibFile.takeError();
return extractFromArchive(*LibFile->get(), Binaries);
}
case file_magic::offload_binary:
return extractOffloadFiles(Buffer, Binaries);
default:
return Error::success();
}
}
OffloadKind object::getOffloadKind(StringRef Name) {
return llvm::StringSwitch<OffloadKind>(Name)
.Case("openmp", OFK_OpenMP)
.Case("cuda", OFK_Cuda)
.Case("hip", OFK_HIP)
.Case("sycl", OFK_SYCL)
.Default(OFK_None);
}
StringRef object::getOffloadKindName(OffloadKind Kind) {
switch (Kind) {
case OFK_OpenMP:
return "openmp";
case OFK_Cuda:
return "cuda";
case OFK_HIP:
return "hip";
case OFK_SYCL:
return "sycl";
default:
return "none";
}
}
ImageKind object::getImageKind(StringRef Name) {
return llvm::StringSwitch<ImageKind>(Name)
.Case("o", IMG_Object)
.Case("bc", IMG_Bitcode)
.Case("cubin", IMG_Cubin)
.Case("fatbin", IMG_Fatbinary)
.Case("s", IMG_PTX)
.Case("spv", IMG_SPIRV)
.Default(IMG_None);
}
StringRef object::getImageKindName(ImageKind Kind) {
switch (Kind) {
case IMG_Object:
return "o";
case IMG_Bitcode:
return "bc";
case IMG_Cubin:
return "cubin";
case IMG_Fatbinary:
return "fatbin";
case IMG_PTX:
return "s";
case IMG_SPIRV:
return "spv";
default:
return "";
}
}
bool object::areTargetsEquivalent(const OffloadFile::TargetID &LHS,
const OffloadFile::TargetID &RHS) {
llvm::Triple LHSTT(LHS.first);
llvm::Triple RHSTT(RHS.first);
// Check for logical AMDGPU target-id equivalence.
if (LHSTT.isAMDGPU()) {
AMDGPU::TargetID LHSID(LHSTT, LHS.second);
AMDGPU::TargetID RHSID(RHSTT, RHS.second);
return LHSID.isEquivalent(RHSID);
}
// For other targets the triples must be compatible and the arch must match.
return LHSTT.isCompatibleWith(RHSTT) && LHS.second == RHS.second;
}
bool object::areTargetsCompatible(const OffloadFile::TargetID &Provided,
const OffloadFile::TargetID &Requested) {
llvm::Triple ProvidedTT(Provided.first);
llvm::Triple RequestedTT(Requested.first);
// The AMDGPU target requires target-id aware checks (base processor plus
// xnack/sramecc features).
if (ProvidedTT.isAMDGPU()) {
AMDGPU::TargetID ProvidedID(ProvidedTT, Provided.second);
AMDGPU::TargetID RequestedID(RequestedTT, Requested.second);
return ProvidedID.providesFor(RequestedID);
}
// For other targets the triples must be compatible.
if (!ProvidedTT.isCompatibleWith(RequestedTT))
return false;
// If the architecture is "generic" we assume it is always compatible.
if (Provided.second == "generic" || Requested.second == "generic")
return true;
return Provided.second == Requested.second;
}