[llvm] [SystemZ][z/OS] Add z/OS archive writing support (PR #200087)
Amy Kwan via llvm-commits
llvm-commits at lists.llvm.org
Fri Jun 12 04:11:46 PDT 2026
https://github.com/amy-kwan updated https://github.com/llvm/llvm-project/pull/200087
>From 0bc7264b094ea356ddc479d55b7c4578c17feb5c Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Wed, 27 May 2026 18:56:30 -0500
Subject: [PATCH 1/2] [SystemZ][z/OS] Add z/OS archive writing support
This patch implement z/OS archive writing in ArchiveWriter and adds the option
`llvm-ar --format=zos`.
Moreover, This patch teaches `llvm-ar` to emit z/OS-compatible archives by:
- Detecting GOFF object files as z/OS members
- Writing z/OS member headers and archive magic
- Converting archive headers and symbol-name string tables to EBCDIC
- Emitting z/OS symbol table entries
- Using EBCDIC newline padding bytes
- Add a z/OS-specific workaround for empty symbol tables by emitting a dummy
blank symbol to satisfy binder requirements.
---
llvm/lib/Object/ArchiveWriter.cpp | 71 +++++++++++++++++++++++++-
llvm/test/tools/llvm-ar/zos-write.test | 36 +++++++++++++
llvm/tools/llvm-ar/llvm-ar.cpp | 18 ++++++-
3 files changed, 123 insertions(+), 2 deletions(-)
create mode 100644 llvm/test/tools/llvm-ar/zos-write.test
diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 4610fb4303274..39ef852599b6a 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -20,6 +20,7 @@
#include "llvm/Object/COFF.h"
#include "llvm/Object/COFFImportFile.h"
#include "llvm/Object/Error.h"
+#include "llvm/Object/GOFFObjectFile.h"
#include "llvm/Object/IRObjectFile.h"
#include "llvm/Object/MachO.h"
#include "llvm/Object/ObjectFile.h"
@@ -70,6 +71,8 @@ object::Archive::Kind NewArchiveMember::detectKindFromObject() const {
if (isa<object::COFFObjectFile>(**OptionalObject) ||
isa<object::COFFImportFile>(**OptionalObject))
return object::Archive::K_COFF;
+ if (isa<object::GOFFObjectFile>(**OptionalObject))
+ return object::Archive::K_ZOS;
return object::Archive::K_GNU;
}
@@ -186,6 +189,10 @@ static bool isCOFFArchive(object::Archive::Kind Kind) {
return Kind == object::Archive::K_COFF;
}
+static bool isZOSArchive(object::Archive::Kind Kind) {
+ return Kind == object::Archive::K_ZOS;
+}
+
static bool isBSDLike(object::Archive::Kind Kind) {
switch (Kind) {
case object::Archive::K_GNU:
@@ -253,6 +260,24 @@ printBSDMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
Out.write(uint8_t(0));
}
+static void
+printZOSMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
+ const sys::TimePoint<std::chrono::seconds> &ModTime,
+ unsigned UID, unsigned GID, unsigned Perms,
+ uint64_t Size) {
+ std::string AHeader;
+ raw_string_ostream AOut(AHeader);
+ if (Name.size() <= 16) {
+ printWithSpacePadding(AOut, Twine(Name), 16);
+ printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms, Size);
+ } else {
+ printBSDMemberHeader(AOut, Pos, Name, ModTime, UID, GID, Perms, Size);
+ }
+ SmallString<256> EHeader;
+ ConverterEBCDIC::convertToEBCDIC(AHeader, EHeader);
+ Out << EHeader.str();
+}
+
static void
printBigArchiveMemberHeader(raw_ostream &Out, StringRef Name,
const sys::TimePoint<std::chrono::seconds> &ModTime,
@@ -306,6 +331,9 @@ printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
if (isBSDLike(Kind))
return printBSDMemberHeader(Out, Pos, M.MemberName, ModTime, M.UID, M.GID,
M.Perms, Size);
+ if (isZOSArchive(Kind))
+ return printZOSMemberHeader(Out, Pos, M.MemberName, ModTime, M.UID, M.GID,
+ M.Perms, Size);
if (!useStringTable(Thin, M.MemberName))
return printGNUSmallMemberHeader(Out, M.MemberName, ModTime, M.UID, M.GID,
M.Perms, Size);
@@ -390,8 +418,11 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
uint64_t Size = OffsetSize; // Number of entries
if (isBSDLike(Kind))
Size += NumSyms * OffsetSize * 2; // Table
+ else if (isZOSArchive(Kind))
+ Size += NumSyms * OffsetSize * 2; // MemberOffset + symbol flags
else
Size += NumSyms * OffsetSize; // Table
+
if (isBSDLike(Kind))
Size += OffsetSize; // byte count
Size += StringTableSize;
@@ -451,6 +482,10 @@ static void writeSymbolTableHeader(raw_ostream &Out, object::Archive::Kind Kind,
} else if (isAIXBigArchive(Kind)) {
printBigArchiveMemberHeader(Out, "", now(Deterministic), 0, 0, 0, Size,
PrevMemberOffset, NextMemberOffset);
+ } else if (isZOSArchive(Kind)) {
+ const char *Name = "__.SYMDEF";
+ printZOSMemberHeader(Out, Out.tell(), Name, now(Deterministic), 0, 0, 0,
+ Size);
} else {
const char *Name = is64BitKind(Kind) ? "/SYM64" : "";
printGNUSmallMemberHeader(Out, Name, now(Deterministic), 0, 0, 0, Size);
@@ -611,6 +646,10 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
StringTable.size(), &Pad);
writeSymbolTableHeader(Out, Kind, Deterministic, Size, PrevMemberOffset,
NextMemberOffset);
+ // Padding size is not included in the Size field of the z/OS symbol table
+ // header
+ if (isZOSArchive(Kind))
+ Size -= Pad;
if (isBSDLike(Kind))
printNBits(Out, Kind, NumSyms * 2 * OffsetSize);
@@ -631,6 +670,8 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
if (isBSDLike(Kind))
printNBits(Out, Kind, StringOffset);
printNBits(Out, Kind, Pos); // member offset
+ if (isZOSArchive(Kind))
+ printNBits(Out, Kind, Pos); // symbol flags
}
Pos += M.Header.size() + M.Data.size() + M.Padding.size();
}
@@ -638,7 +679,16 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
if (isBSDLike(Kind))
// byte count of the string table
printNBits(Out, Kind, StringTable.size());
- Out << StringTable;
+ if (isZOSArchive(Kind)) {
+ std::string AStringTable;
+ raw_string_ostream AOut(AStringTable);
+ AOut << StringTable;
+ SmallString<256> EStringTable;
+ ConverterEBCDIC::convertToEBCDIC(AStringTable, EStringTable);
+ Out << EStringTable.str();
+ } else {
+ Out << StringTable;
+ }
while (Pad--)
Out.write(uint8_t(0));
@@ -783,6 +833,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
LLVMContext &Context, ArrayRef<NewArchiveMember> NewMembers,
std::optional<bool> IsEC, function_ref<void(Error)> Warn) {
static char PaddingData[8] = {'\n', '\n', '\n', '\n', '\n', '\n', '\n', '\n'};
+ static char ZOSPaddingData[8] = {0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15}; // EBCDIC newlines.
uint64_t MemHeadPadSize = 0;
uint64_t Pos =
isAIXBigArchive(Kind) ? sizeof(object::BigArchive::FixLenHdr) : 0;
@@ -846,6 +898,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
}
std::vector<std::unique_ptr<SymbolicFile>> SymFiles;
+ uint32_t LastZosObjIndex =
+ UINT_MAX; // Only set when writing symbol table in z/OS archive.
if (NeedSymbols != SymtabWritingMode::NoSymtab || isAIXBigArchive(Kind)) {
for (const NewArchiveMember &M : NewMembers) {
@@ -856,6 +910,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
if (!SymFileOrErr)
return createFileError(M.MemberName, SymFileOrErr.takeError());
SymFiles.push_back(std::move(*SymFileOrErr));
+ if (isZOSArchive(Kind) && SymFiles.back().get())
+ LastZosObjIndex = SymFiles.size() - 1;
}
}
@@ -905,6 +961,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
unsigned TailPadding =
offsetToAlignment(Data.size() + MemberPadding, Align(2));
StringRef Padding = StringRef(PaddingData, MemberPadding + TailPadding);
+ if (isZOSArchive(Kind))
+ Padding = StringRef(ZOSPaddingData, MemberPadding + TailPadding);
sys::TimePoint<std::chrono::seconds> ModTime;
if (UniqueTimestamps)
@@ -972,6 +1030,15 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
if (CurSymFile)
HasObject = true;
}
+ // On z/OS, when there are no symbols, add a dummy blank symbol
+ // into the symbol table. This is done since the z/OS binder:
+ // - emits an error if there is no symbol table in the archive
+ // - emits an error if the symbol table has 0 symbols
+ // - should not find any references to a blank symbol
+ if ((LastZosObjIndex == Index) && (SymNames.tell() == 0)) {
+ Symbols.push_back(0);
+ SymNames << ' ' << '\0';
+ }
Pos += Header.size() + Data.size() + Padding.size();
Ret.push_back({std::move(Symbols), std::move(Header), Data, Padding,
@@ -1148,6 +1215,8 @@ Error writeArchiveToStream(raw_ostream &Out,
Out << "!<thin>\n";
else if (isAIXBigArchive(Kind))
Out << "<bigaf>\n";
+ else if (isZOSArchive(Kind))
+ Out << ZOSArchiveMagic;
else
Out << "!<arch>\n";
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
new file mode 100644
index 0000000000000..447e4562b83aa
--- /dev/null
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -0,0 +1,36 @@
+# Test writing a z/OS archive.
+
+# Create a z/OS archive from a plain text file, without a symbol table.
+# RUN: rm -rf %t.dir && mkdir -p %t.dir
+# RUN: printf 'abcd' > %t.dir/test.txt
+# RUN: llvm-ar rcSD --format=zos %t.a %t.dir/test.txt
+
+# The archive should be written and read back successfully through llvm-ar.
+# RUN: llvm-ar t %t.a | FileCheck %s --check-prefix=LIST
+# RUN: llvm-ar p %t.a test.txt | FileCheck %s --check-prefix=CONTENT
+
+# Check raw bytes:
+# - The first 8 bytes are the z/OS archive magic in EBCDIC.
+# - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
+# RUN: %python -c "d=open(r'%t.a','rb').read(); print(d[:8].hex()); print(d[66:68].hex())" | FileCheck %s --check-prefix=BYTES
+
+# LIST: test.txt
+# CONTENT: abcd
+# BYTES: 5a4c819983886e15
+# BYTES-NEXT: 7915
+
+# Odd-sized members should be padded with EBCDIC newline (0x15).
+# RUN: printf 'abc' > %t.dir/odd.txt
+# RUN: llvm-ar rcSD --format=zos %t.odd.a %t.dir/odd.txt
+# RUN: %python -c "d=open(r'%t.odd.a','rb').read(); print(f'{d[-1]:02x}')" | FileCheck %s --check-prefix=PAD
+
+# PAD: 15
+
+# Long member names should written and read back correctly.
+# RUN: printf 'xyz' > %t.dir/very_long_member_name.txt
+# RUN: llvm-ar rcSD --format=zos %t.long.a %t.dir/very_long_member_name.txt
+# RUN: llvm-ar t %t.long.a | FileCheck %s --check-prefix=LONG-LIST
+# RUN: llvm-ar p %t.long.a very_long_member_name.txt | FileCheck %s --check-prefix=LONG-CONTENT
+
+# LONG-LIST: very_long_member_name.txt
+# LONG-CONTENT: xyz
diff --git a/llvm/tools/llvm-ar/llvm-ar.cpp b/llvm/tools/llvm-ar/llvm-ar.cpp
index 320a903b59e87..4ee8fd6bb0633 100644
--- a/llvm/tools/llvm-ar/llvm-ar.cpp
+++ b/llvm/tools/llvm-ar/llvm-ar.cpp
@@ -83,6 +83,7 @@ static void printArHelp(StringRef ToolName) {
=darwin - darwin
=bsd - bsd
=bigarchive - big archive (AIX OS)
+ =zos - zos archive (z/OS OS)
=coff - coff
--plugin=<string> - ignored for compatibility
-h --help - display this help and exit
@@ -195,7 +196,16 @@ static SmallVector<const char *, 256> PositionalArgs;
static bool MRI;
namespace {
-enum Format { Default, GNU, COFF, BSD, DARWIN, BIGARCHIVE, Unknown };
+enum Format {
+ Default,
+ GNU,
+ COFF,
+ BSD,
+ DARWIN,
+ BIGARCHIVE,
+ ZOSARCHIVE,
+ Unknown
+};
}
static Format FormatType = Default;
@@ -1071,6 +1081,11 @@ static void performWriteOperation(ArchiveOperation Operation,
fail("only the gnu format has a thin mode");
Kind = object::Archive::K_AIXBIG;
break;
+ case ZOSARCHIVE:
+ if (Thin)
+ fail("only the gnu format has a thin mode");
+ Kind = object::Archive::K_ZOS;
+ break;
case Unknown:
llvm_unreachable("");
}
@@ -1389,6 +1404,7 @@ static int ar_main(int argc, char **argv) {
.Case("bsd", BSD)
.Case("bigarchive", BIGARCHIVE)
.Case("coff", COFF)
+ .Case("zos", ZOSARCHIVE)
.Default(Unknown);
if (FormatType == Unknown)
fail(std::string("Invalid format ") + Match);
>From 9796cadf5b170518a58cbafc907d90d1ac5bb7d3 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Fri, 12 Jun 2026 07:11:24 -0400
Subject: [PATCH 2/2] Address review comments.
---
llvm/lib/Object/ArchiveWriter.cpp | 14 +++++------
llvm/test/tools/llvm-ar/zos-write.test | 34 ++++++++++++++++++++------
2 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 39ef852599b6a..dfbd78f7fdd5c 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -416,10 +416,11 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
uint32_t *Padding = nullptr) {
assert((OffsetSize == 4 || OffsetSize == 8) && "Unsupported OffsetSize");
uint64_t Size = OffsetSize; // Number of entries
- if (isBSDLike(Kind))
+ // Each symbol table entry consists of a member offset.
+ // For BSD, each entry also includes a string table offset.
+ // For z/OS, each entry also includes a flag field.
+ if (isBSDLike(Kind) || isZOSArchive(Kind))
Size += NumSyms * OffsetSize * 2; // Table
- else if (isZOSArchive(Kind))
- Size += NumSyms * OffsetSize * 2; // MemberOffset + symbol flags
else
Size += NumSyms * OffsetSize; // Table
@@ -647,7 +648,7 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
writeSymbolTableHeader(Out, Kind, Deterministic, Size, PrevMemberOffset,
NextMemberOffset);
// Padding size is not included in the Size field of the z/OS symbol table
- // header
+ // header.
if (isZOSArchive(Kind))
Size -= Pad;
@@ -680,11 +681,8 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
// byte count of the string table
printNBits(Out, Kind, StringTable.size());
if (isZOSArchive(Kind)) {
- std::string AStringTable;
- raw_string_ostream AOut(AStringTable);
- AOut << StringTable;
SmallString<256> EStringTable;
- ConverterEBCDIC::convertToEBCDIC(AStringTable, EStringTable);
+ ConverterEBCDIC::convertToEBCDIC(StringTable, EStringTable);
Out << EStringTable.str();
} else {
Out << StringTable;
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index 447e4562b83aa..d26b6d092b961 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -1,17 +1,17 @@
-# Test writing a z/OS archive.
+## Test writing a z/OS archive.
-# Create a z/OS archive from a plain text file, without a symbol table.
+## Create a z/OS archive from a plain text file, without a symbol table.
# RUN: rm -rf %t.dir && mkdir -p %t.dir
# RUN: printf 'abcd' > %t.dir/test.txt
# RUN: llvm-ar rcSD --format=zos %t.a %t.dir/test.txt
-# The archive should be written and read back successfully through llvm-ar.
+## The archive should be written and read back successfully through llvm-ar.
# RUN: llvm-ar t %t.a | FileCheck %s --check-prefix=LIST
# RUN: llvm-ar p %t.a test.txt | FileCheck %s --check-prefix=CONTENT
-# Check raw bytes:
-# - The first 8 bytes are the z/OS archive magic in EBCDIC.
-# - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
+## Check raw bytes:
+## - The first 8 bytes are the z/OS archive magic in EBCDIC.
+## - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
# RUN: %python -c "d=open(r'%t.a','rb').read(); print(d[:8].hex()); print(d[66:68].hex())" | FileCheck %s --check-prefix=BYTES
# LIST: test.txt
@@ -19,14 +19,14 @@
# BYTES: 5a4c819983886e15
# BYTES-NEXT: 7915
-# Odd-sized members should be padded with EBCDIC newline (0x15).
+## Odd-sized members should be padded with EBCDIC newline (0x15).
# RUN: printf 'abc' > %t.dir/odd.txt
# RUN: llvm-ar rcSD --format=zos %t.odd.a %t.dir/odd.txt
# RUN: %python -c "d=open(r'%t.odd.a','rb').read(); print(f'{d[-1]:02x}')" | FileCheck %s --check-prefix=PAD
# PAD: 15
-# Long member names should written and read back correctly.
+## Long member names should be written and read back correctly.
# RUN: printf 'xyz' > %t.dir/very_long_member_name.txt
# RUN: llvm-ar rcSD --format=zos %t.long.a %t.dir/very_long_member_name.txt
# RUN: llvm-ar t %t.long.a | FileCheck %s --check-prefix=LONG-LIST
@@ -34,3 +34,21 @@
# LONG-LIST: very_long_member_name.txt
# LONG-CONTENT: xyz
+
+## A z/OS archive with real symbols should write a readable symbol table.
+# RUN: printf 'define i32 @mytest() {\nentry:\n ret i32 87\n}\n' > %t.dir/mytest.ll
+# RUN: llvm-as %t.dir/mytest.ll -o %t.dir/mytest.bc
+# RUN: llvm-ar rcD --format=zos %t.sym.a %t.dir/mytest.bc
+# RUN: llvm-nm --print-armap %t.sym.a | FileCheck %s --check-prefix=SYMTAB
+
+# SYMTAB: Archive map
+# SYMTAB-NEXT: mytest in mytest.bc
+
+## A z/OS archive with no real symbols should still write the dummy blank symbol.
+# RUN: printf 'source_filename = "blank.ll"\n' > %t.dir/blank.ll
+# RUN: llvm-as %t.dir/blank.ll -o %t.dir/blank.bc
+# RUN: llvm-ar rcD --format=zos %t.blank-sym.a %t.dir/blank.bc
+# RUN: llvm-nm --print-armap %t.blank-sym.a | FileCheck %s --check-prefix=EMPTY-SYMTAB
+
+# EMPTY-SYMTAB: Archive map
+# EMPTY-SYMTAB-NEXT: {{^ in blank.bc$}}
More information about the llvm-commits
mailing list