[llvm-branch-commits] [llvm] [llvm-ar][GOFF] Implement symbol attributes for GOFF archives (PR #214528)

Amy Kwan via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Aug 12 22:06:33 PDT 2026


https://github.com/amy-kwan updated https://github.com/llvm/llvm-project/pull/214528

>From 1250c3e52cdd3134dcc2e3ab1473b7675d8184f8 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Thu, 6 Aug 2026 12:34:57 -0400
Subject: [PATCH 1/3] [llvm-ar][GOFF] Implement symbol attributes for GOFF
 archives

z/OS archive symbol table entries contain a 32-bit attribute word
alongside each member offset. The low three bits encode:
  bit 2 (0x4): 64-bit addressing (AMODE 64)
  bit 1 (0x2): XPLink calling convention
  bit 0 (0x1): Writable Static Area (WSA)

Previously in e2c8fa0, llvm-ar wrote zero for
these attributes. This patch reads them from GOFF ESD records and stores them
in a SymbolAttrs vector parallel to the existing Symbols vector in
MemberData to emit the correct word per symbol.

These attributes are tested using `llvm-nm --print-armap` implemented in
#212830 within the LIT test.
---
 llvm/include/llvm/Object/GOFFObjectFile.h |  6 ++
 llvm/lib/Object/ArchiveWriter.cpp         | 51 +++++++++++-----
 llvm/lib/Object/GOFFObjectFile.cpp        | 35 +++++++++++
 llvm/test/tools/llvm-ar/zos-symattrs.test | 73 +++++++++++++++++++++++
 4 files changed, 151 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/tools/llvm-ar/zos-symattrs.test

diff --git a/llvm/include/llvm/Object/GOFFObjectFile.h b/llvm/include/llvm/Object/GOFFObjectFile.h
index 80da64ed3730d..4ffffc828e103 100644
--- a/llvm/include/llvm/Object/GOFFObjectFile.h
+++ b/llvm/include/llvm/Object/GOFFObjectFile.h
@@ -47,6 +47,12 @@ class LLVM_ABI GOFFObjectFile : public ObjectFile {
 public:
   Expected<StringRef> getSymbolName(SymbolRef Symbol) const;
 
+  // Returns the z/OS archive symbol attribute bits for a symbol:
+  //   bit 2 (0x4): symbol is 64-bit
+  //   bit 1 (0x2): symbol uses the XPLink calling convention
+  //   bit 0 (0x1): symbol resides in a writable static area (WSA)
+  uint32_t getZOSSymbolArchiveAttributes(DataRefImpl Symb) const;
+
   GOFFObjectFile(MemoryBufferRef Object, Error &Err);
   static inline bool classof(const Binary *V) { return V->isGOFF(); }
   section_iterator section_begin() const override;
diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 9af8e0efe1872..4540ec5f1c75e 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -368,6 +368,9 @@ printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
 namespace {
 struct MemberData {
   std::vector<unsigned> Symbols;
+  // z/OS archive attribute bits per symbol. Entry i of SymbolAttrs corresponds
+  // to Symbols[i]. These attributes are empty for non-z/OS archives.
+  std::vector<uint32_t> SymbolAttrs;
   std::string Header;
   StringRef Data;
   StringRef Padding;
@@ -386,7 +389,7 @@ static MemberData computeStringTable(StringRef Names) {
   printWithSpacePadding(Out, "//", 48);
   printWithSpacePadding(Out, Size + Pad, 10);
   Out << "`\n";
-  return {{}, std::move(Header), Names, Pad ? "\n" : ""};
+  return {{}, {}, std::move(Header), Names, Pad ? "\n" : ""};
 }
 
 static sys::TimePoint<std::chrono::seconds> now(bool Deterministic) {
@@ -677,13 +680,12 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
       }
     }
 
-    for (unsigned StringOffset : M.Symbols) {
+    for (size_t I = 0, E = M.Symbols.size(); I != E; ++I) {
       if (isBSDLike(Kind))
-        printNBits(Out, Kind, StringOffset);
+        printNBits(Out, Kind, M.Symbols[I]);
       printNBits(Out, Kind, Pos); // member offset
-      // FIXME: Properly handle symbol attributes for z/OS archives.
       if (isZOSArchive(Kind))
-        printNBits(Out, Kind, 0); // symbol flags
+        printNBits(Out, Kind, M.SymbolAttrs[I]); // symbol attribute flags
     }
     Pos += M.Header.size() + M.Data.size() + M.Padding.size();
   }
@@ -1078,21 +1080,42 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
       if (!SymbolsOrErr)
         return createFileError(MemberName, SymbolsOrErr.takeError());
       D.Symbols = std::move(*SymbolsOrErr);
+      // For z/OS, populate SymbolAttrs in lockstep with Symbols so that
+      // writeSymbolTable() can emit the per-symbol attribute word.
+      if (isZOSArchive(Kind)) {
+        auto *GOFFObj = dyn_cast_or_null<GOFFObjectFile>(D.SymFile.get());
+        if (GOFFObj) {
+          for (const object::BasicSymbolRef &S : GOFFObj->symbols()) {
+            if (!isArchiveSymbol(S))
+              continue;
+            D.SymbolAttrs.push_back(
+                GOFFObj->getZOSSymbolArchiveAttributes(S.getRawDataRefImpl()));
+          }
+        } else {
+          // For non-GOFF symbolic files (e.g. bitcode/IR), there is no z/OS
+          // archive attribute data available. Pad SymbolAttrs to stay in sync
+          // with Symbols.
+          D.SymbolAttrs.resize(D.Symbols.size(), 0);
+        }
+      }
       if (D.SymFile)
         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)) {
-      D.Symbols.push_back(0);
-      SymNames << ' ' << '\0';
+      // 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 (isZOSArchive(Kind) && (LastZosObjIndex == Index) &&
+          (SymNames.tell() == 0)) {
+        D.Symbols.push_back(0);
+        D.SymbolAttrs.push_back(0);
+        SymNames << ' ' << '\0';
+      }
     }
 
     Pos += D.Header.size() + D.Data.size() + D.Padding.size();
   }
+
   // If there are no symbols, emit an empty symbol table, to satisfy Solaris
   // tools, older versions of which expect a symbol table in a non-empty
   // archive, regardless of whether there are any symbols in it.
diff --git a/llvm/lib/Object/GOFFObjectFile.cpp b/llvm/lib/Object/GOFFObjectFile.cpp
index 057d3fc208c9b..47d690e7f092e 100644
--- a/llvm/lib/Object/GOFFObjectFile.cpp
+++ b/llvm/lib/Object/GOFFObjectFile.cpp
@@ -375,6 +375,41 @@ GOFFObjectFile::getSymbolSection(DataRefImpl Symb) const {
                                std::to_string(SymEdId));
 }
 
+uint32_t GOFFObjectFile::getZOSSymbolArchiveAttributes(DataRefImpl Symb) const {
+  const uint8_t *SymRecord = getSymbolEsdRecord(Symb);
+  uint32_t Attrs = 0;
+
+  // Bit 2 (0x4): 64-bit — AMODE is only defined for LD/ER records.
+  GOFF::ESDSymbolType SymType;
+  ESDRecord::getSymbolType(SymRecord, SymType);
+  if (SymType != GOFF::ESD_ST_PartReference) {
+    GOFF::ESDAmode Amode;
+    ESDRecord::getAmode(SymRecord, Amode);
+    if (Amode == GOFF::ESD_AMODE_64)
+      Attrs |= 0x4;
+  }
+
+  // Bit 1 (0x2): XPLink — LinkageType is ESD_LT_XPLink.
+  GOFF::ESDLinkageType LinkageType;
+  ESDRecord::getLinkageType(SymRecord, LinkageType);
+  if (LinkageType == GOFF::ESD_LT_XPLink)
+    Attrs |= 0x2;
+
+  // Bit 0 (0x1): Writable Static Area — the symbol's parent ED has NameSpace
+  // ESD_NS_Parts.
+  uint32_t ParentEsdId;
+  ESDRecord::getParentEsdId(SymRecord, ParentEsdId);
+  if (ParentEsdId) {
+    const uint8_t *EdRecord = EsdPtrs[ParentEsdId];
+    GOFF::ESDNameSpaceId NameSpace;
+    ESDRecord::getNameSpaceId(EdRecord, NameSpace);
+    if (NameSpace == GOFF::ESD_NS_Parts)
+      Attrs |= 0x1;
+  }
+
+  return Attrs;
+}
+
 uint64_t GOFFObjectFile::getSymbolSize(DataRefImpl Symb) const {
   const uint8_t *Record = getSymbolEsdRecord(Symb);
   uint32_t Length;
diff --git a/llvm/test/tools/llvm-ar/zos-symattrs.test b/llvm/test/tools/llvm-ar/zos-symattrs.test
new file mode 100644
index 0000000000000..3fab075c995e1
--- /dev/null
+++ b/llvm/test/tools/llvm-ar/zos-symattrs.test
@@ -0,0 +1,73 @@
+## Test that llvm-ar correctly writes z/OS archive symbol attribute bits,
+## and that llvm-nm --print-armap reads them back correctly.
+##
+## The three attribute bits come from GOFF ESD record fields:
+##   bit 2 (0x4): 64-bit  - AMODE == ESD_AMODE_64
+##   bit 1 (0x2): XPLink  - LinkageType == ESD_LT_XPLink
+##   bit 0 (0x1): WSA     - parent ED NameSpaceId == ESD_NS_Parts
+##
+## GOFF objects are produced by compiling embedded .ll files with llc.
+
+# REQUIRES: systemz-registered-target
+
+# RUN: rm -rf %t.dir && mkdir -p %t.dir
+# RUN: split-file %s %t.dir
+
+## Compile each .ll file into a GOFF object.
+# RUN: llc -mtriple=s390x-ibm-zos -filetype=obj %t.dir/func.ll -o %t.dir/func.o
+# RUN: llc -mtriple=s390x-ibm-zos -filetype=obj %t.dir/data.ll -o %t.dir/data.o
+
+## Build a z/OS archive containing both objects.
+# RUN: llvm-ar rcD --format=zos %t.dir/test.a %t.dir/func.o %t.dir/data.o
+
+## 1. Check the archive map shows each symbol with the correct inline flags.
+# RUN: llvm-nm --print-armap %t.dir/test.a | FileCheck %s --check-prefix=ARMAP
+
+# ARMAP:      Archive map
+# ARMAP-NEXT: s_func in func.o (flags: 0x00000006 [64-bit + XPLink])
+# ARMAP-NEXT: s_data in data.o (flags: 0x00000003 [XPLink + WSA])
+
+## 2. Test that basic archive operations still work on a GOFF archive.
+##    llvm-ar t should list all member names.
+# RUN: llvm-ar t %t.dir/test.a | FileCheck %s --check-prefix=LIST
+
+# LIST:      func.o
+# LIST-NEXT: data.o
+
+## 3. Test mixed archive: one GOFF object and one bitcode object together.
+##    The bitcode member should produce attribute of 0 (no GOFF ESD data available).
+# RUN: llc -mtriple=s390x-ibm-zos -filetype=obj %t.dir/func2.ll -o %t.dir/func2.o
+# RUN: llvm-as %t.dir/mixed.ll -o %t.dir/mixed.bc
+# RUN: llvm-ar rcD --format=zos %t.dir/mixed.a %t.dir/func2.o %t.dir/mixed.bc
+# RUN: llvm-nm --print-armap %t.dir/mixed.a | FileCheck %s --check-prefix=MIXED
+
+# MIXED:      Archive map
+# MIXED-NEXT: s_func2 in func2.o (flags: 0x00000006 [64-bit + XPLink])
+# MIXED-NEXT: mixed_sym in mixed.bc (flags: 0x00000000 [none])
+
+#--- func.ll
+target triple = "s390x-ibm-zos"
+
+define void @s_func() {
+entry:
+  ret void
+}
+
+#--- data.ll
+target triple = "s390x-ibm-zos"
+
+ at s_data = global i32 0, align 4
+
+#--- func2.ll
+target triple = "s390x-ibm-zos"
+
+define void @s_func2() {
+entry:
+  ret void
+}
+
+#--- mixed.ll
+define i32 @mixed_sym() {
+entry:
+  ret i32 0
+}

>From 671e17444520bedb1947d522d467aac0ef12eadb Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Thu, 6 Aug 2026 15:48:34 -0400
Subject: [PATCH 2/3] Remove unnecessary new line

---
 llvm/lib/Object/ArchiveWriter.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 4540ec5f1c75e..f3126357946d1 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -1115,7 +1115,6 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
 
     Pos += D.Header.size() + D.Data.size() + D.Padding.size();
   }
-
   // If there are no symbols, emit an empty symbol table, to satisfy Solaris
   // tools, older versions of which expect a symbol table in a non-empty
   // archive, regardless of whether there are any symbols in it.

>From 47cc2f4fcc4931e2cb0ca7652d08fe8aa874de7f Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Thu, 13 Aug 2026 01:06:07 -0400
Subject: [PATCH 3/3] Address comments in GOFFObjectFile.cpp

---
 llvm/lib/Object/GOFFObjectFile.cpp | 37 +++++++++++++++---------------
 1 file changed, 18 insertions(+), 19 deletions(-)

diff --git a/llvm/lib/Object/GOFFObjectFile.cpp b/llvm/lib/Object/GOFFObjectFile.cpp
index 47d690e7f092e..03181415e5473 100644
--- a/llvm/lib/Object/GOFFObjectFile.cpp
+++ b/llvm/lib/Object/GOFFObjectFile.cpp
@@ -379,15 +379,20 @@ uint32_t GOFFObjectFile::getZOSSymbolArchiveAttributes(DataRefImpl Symb) const {
   const uint8_t *SymRecord = getSymbolEsdRecord(Symb);
   uint32_t Attrs = 0;
 
-  // Bit 2 (0x4): 64-bit — AMODE is only defined for LD/ER records.
-  GOFF::ESDSymbolType SymType;
-  ESDRecord::getSymbolType(SymRecord, SymType);
-  if (SymType != GOFF::ESD_ST_PartReference) {
-    GOFF::ESDAmode Amode;
-    ESDRecord::getAmode(SymRecord, Amode);
-    if (Amode == GOFF::ESD_AMODE_64)
-      Attrs |= 0x4;
+  // Bit 2 (0x4): 64-bit AMODE. If the child AMODE is unspecified,
+  // query the parent ED.
+  GOFF::ESDAmode Amode;
+  ESDRecord::getAmode(SymRecord, Amode);
+  if (Amode == GOFF::ESD_AMODE_None) {
+    uint32_t ParentEsdId;
+    ESDRecord::getParentEsdId(SymRecord, ParentEsdId);
+    if (ParentEsdId) {
+      const uint8_t *EdRecord = EsdPtrs[ParentEsdId];
+      ESDRecord::getAmode(EdRecord, Amode);
+    }
   }
+  if (Amode == GOFF::ESD_AMODE_64)
+    Attrs |= 0x4;
 
   // Bit 1 (0x2): XPLink — LinkageType is ESD_LT_XPLink.
   GOFF::ESDLinkageType LinkageType;
@@ -395,17 +400,11 @@ uint32_t GOFFObjectFile::getZOSSymbolArchiveAttributes(DataRefImpl Symb) const {
   if (LinkageType == GOFF::ESD_LT_XPLink)
     Attrs |= 0x2;
 
-  // Bit 0 (0x1): Writable Static Area — the symbol's parent ED has NameSpace
-  // ESD_NS_Parts.
-  uint32_t ParentEsdId;
-  ESDRecord::getParentEsdId(SymRecord, ParentEsdId);
-  if (ParentEsdId) {
-    const uint8_t *EdRecord = EsdPtrs[ParentEsdId];
-    GOFF::ESDNameSpaceId NameSpace;
-    ESDRecord::getNameSpaceId(EdRecord, NameSpace);
-    if (NameSpace == GOFF::ESD_NS_Parts)
-      Attrs |= 0x1;
-  }
+  // Bit 0 (0x1): Writable Static Area.
+  GOFF::ESDNameSpaceId NameSpace;
+  ESDRecord::getNameSpaceId(SymRecord, NameSpace);
+  if (NameSpace == GOFF::ESD_NS_Parts)
+    Attrs |= 0x1;
 
   return Attrs;
 }



More information about the llvm-branch-commits mailing list