| //===- 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; |
| } |