[llvm-branch-commits] [llvm] release/23.x: [llvm-ar][GOFF] Implement symbol attributes for GOFF archives (#214528) (PR #226705)

Amy Kwan via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Sat Sep 26 07:53:04 PDT 2026


https://github.com/amy-kwan created https://github.com/llvm/llvm-project/pull/226705



Backport 847fb5cfde68fdea490171bc24087ebb0cd90738

This backport has been modified slightly by updating the LIT tests to remove
the dependency on llvm-nm. llvm-nm added GOFF support after the LLVM 23.x
release cut, and its new functionality is not available in LLVM 23.

>From c23e0f76e02d6a3f24ec26827e31c58e42b4c411 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Thu, 10 Sep 2026 10:38:30 -0400
Subject: [PATCH] release/23.x: [llvm-ar][GOFF] Implement symbol attributes for
 GOFF archives (#214528)

Backport 847fb5cfde68fdea490171bc24087ebb0cd90738

This backport has been modified slightly by updating the LIT tests to remove
the dependency on llvm-nm. llvm-nm added GOFF support after the LLVM 23.x release
cut, and its new functionality is not available in LLVM 23.
---
 llvm/include/llvm/Object/GOFFObjectFile.h |  6 ++
 llvm/lib/Object/ArchiveWriter.cpp         | 52 +++++++++++----
 llvm/lib/Object/GOFFObjectFile.cpp        | 38 +++++++++++
 llvm/test/tools/llvm-ar/zos-symattrs.test | 81 +++++++++++++++++++++++
 llvm/test/tools/llvm-ar/zos-write.test    | 10 ++-
 5 files changed, 172 insertions(+), 15 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 80da64ed3730d8..4ffffc828e1030 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 9af8e0efe1872d..6ef79bb5ba44a6 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,14 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
       }
     }
 
-    for (unsigned StringOffset : M.Symbols) {
+    assert((!isZOSArchive(Kind) || M.Symbols.size() == M.SymbolAttrs.size()) &&
+           "Incorrect number of symbol attributes!");
+    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 attributes
     }
     Pos += M.Header.size() + M.Data.size() + M.Padding.size();
   }
@@ -1078,17 +1082,37 @@ 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 (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());
+        }
+      }
       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();
diff --git a/llvm/lib/Object/GOFFObjectFile.cpp b/llvm/lib/Object/GOFFObjectFile.cpp
index d00ad71105fd50..2a12a80e3b108d 100644
--- a/llvm/lib/Object/GOFFObjectFile.cpp
+++ b/llvm/lib/Object/GOFFObjectFile.cpp
@@ -381,6 +381,44 @@ 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. If the child AMODE is unspecified,
+  // query the parent ED.
+  // TODO: The parent-walk path (child ESD_AMODE_None with a parent that has
+  // ESD_AMODE_64) cannot currently be tested as GOFFObjectWriter always emits
+  // ESD_AMODE_64 directly on LD/ER records and does not set AMODE on ED
+  // records. Full coverage requires yaml2obj GOFF ESD record support.
+  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;
+  ESDRecord::getLinkageType(SymRecord, LinkageType);
+  if (LinkageType == GOFF::ESD_LT_XPLink)
+    Attrs |= 0x2;
+
+  // Bit 0 (0x1): Writable Static Area.
+  GOFF::ESDNameSpaceId NameSpace;
+  ESDRecord::getNameSpaceId(SymRecord, 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 00000000000000..d776922586d167
--- /dev/null
+++ b/llvm/test/tools/llvm-ar/zos-symattrs.test
@@ -0,0 +1,81 @@
+## Test that llvm-ar correctly writes z/OS archive symbol attribute bits.
+##
+## 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     - 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 attribute words written into the archive symbol table.
+##    s_func: 0x6 (64-bit=0x4 + XPLink=0x2)
+##    s_data: 0x3 (XPLink=0x2 + WSA=0x1)
+# RUN: %python -c " \
+# RUN:   d = open(r'%t.dir/test.a', 'rb').read(); \
+# RUN:   print(hex(int.from_bytes(d[76:80], 'big'))); \
+# RUN:   print(hex(int.from_bytes(d[84:88], 'big'))) \
+# RUN: " | FileCheck %s --check-prefix=ATTRS
+
+# ATTRS:      0x6
+# ATTRS-NEXT: 0x3
+
+## 2. Test that basic archive operations still work on a GOFF archive.
+##    llvm-ar t should list exactly the expected member names and nothing else.
+# RUN: llvm-ar t %t.dir/test.a | FileCheck %s --check-prefix=LIST \
+# RUN:   --implicit-check-not={{.}}
+
+# LIST: func.o
+# LIST-NEXT: data.o
+
+## 3. Test mixed archive: one GOFF object and one bitcode object together.
+##    The bitcode member should have an 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: %python -c " \
+# RUN:   d = open(r'%t.dir/mixed.a', 'rb').read(); \
+# RUN:   print(hex(int.from_bytes(d[76:80], 'big'))); \
+# RUN:   print(hex(int.from_bytes(d[84:88], 'big'))) \
+# RUN: " | FileCheck %s --check-prefix=MIXED-ATTRS
+
+# MIXED-ATTRS:      0x6
+# MIXED-ATTRS-NEXT: 0x0
+
+#--- 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
+}
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index 0e390183f8d008..410d1ed020d604 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -1,6 +1,6 @@
 ## Test writing a z/OS archive.
 
-# RUN: rm -rf %t.dir %t.a %t.odd.a %t.long.a %t.sym.a %t.blank-sym.a && mkdir -p %t.dir
+# RUN: rm -rf %t.dir %t.a %t.odd.a %t.long.a %t.sym.a %t.blank-sym.a %t.gnu-blank.a && mkdir -p %t.dir
 # RUN: split-file %s %t.dir
 
 ## Create a z/OS archive from a plain text file with symbol table writing disabled.
@@ -62,6 +62,14 @@
 # EMPTY-SYMTAB-NEXT: in blank.bc
 # EMPTY-SYMTAB-NOT: in blank.bc
 
+## The blank-symbol injection is z/OS specific. A GNU archive with no real
+## symbols should not have a blank symbol injected into its symbol table.
+# RUN: llvm-ar rcD %t.gnu-blank.a %t.dir/blank.bc
+# RUN: llvm-ar t %t.gnu-blank.a | FileCheck %s --check-prefix=GNU-NO-BLANK \
+# RUN:   --implicit-check-not={{.}}
+
+# GNU-NO-BLANK: blank.bc
+
 #--- mytest.ll
 define i32 @mytest() {
 entry:



More information about the llvm-branch-commits mailing list