diff --git a/bfd/elf64-s390.c b/bfd/elf64-s390.c
index 3925cf6494d3..4730cc2cdb54 100644
--- a/bfd/elf64-s390.c
+++ b/bfd/elf64-s390.c
@@ -852,6 +852,7 @@ elf_s390_copy_indirect_symbol (struct bfd_link_info *info,
       dir->ref_regular |= ind->ref_regular;
       dir->ref_regular_nonweak |= ind->ref_regular_nonweak;
       dir->needs_plt |= ind->needs_plt;
+      dir->pointer_equality_needed |= ind->pointer_equality_needed;
     }
   else
     _bfd_elf_link_hash_copy_indirect (info, dir, ind);
@@ -1037,6 +1038,7 @@ elf_s390_check_relocs (bfd *abfd,
 		 referenced.  */
 	      h->ref_regular = 1;
 	      h->needs_plt = 1;
+	      h->pointer_equality_needed = 1;
 	    }
 	}
 
@@ -1078,6 +1080,38 @@ elf_s390_check_relocs (bfd *abfd,
 	    {
 	      h->needs_plt = 1;
 	      h->plt.refcount += 1;
+
+	      /* GCC 12-14 unconditionally suffix non-local symbols
+		 with @PLT, regardless of whether they are used in
+		 function call instructions (i.e. brasl) or address
+		 taking instructions (i.e. larl).  Treat PLT32DBL
+		 relocation for "larl rX,<sym>@PLT" instruction as
+		 address taking and require pointer equality.  */
+	      if (bfd_link_executable (info)
+		  && r_type == R_390_PLT32DBL
+		  && rel->r_offset >= 2)
+		{
+		  bfd_byte *contents;
+		  void *insn_start;
+		  uint16_t op;
+
+		  if (elf_section_data (sec)->this_hdr.contents != NULL)
+		    contents = elf_section_data (sec)->this_hdr.contents;
+		  else if (!_bfd_elf_mmap_section_contents (abfd, sec, &contents))
+		    return false;
+
+		  insn_start = contents + rel->r_offset - 2;
+		  op = bfd_get_16 (abfd, insn_start) & 0xff0f;
+
+		  if (op == 0xc000)
+		    {
+		      /* larl rX,<sym>@PLT  */
+		      h->pointer_equality_needed = 1;
+		    }
+
+		  if (elf_section_data (sec)->this_hdr.contents != contents)
+		    _bfd_elf_munmap_section_contents (sec, contents);
+		}
 	    }
 	  break;
 
@@ -1227,6 +1261,12 @@ elf_s390_check_relocs (bfd *abfd,
 		     refers to is in a shared lib.  */
 		  h->plt.refcount += 1;
 		}
+
+	      /* Require pointer equality in PDE for above PC-relative
+		 relocations, that are likely in address taken context,
+		 and direct relocations, that are likely in function
+		 reference context.  */
+	      h->pointer_equality_needed = 1;
 	    }
 
 	  /* If we are creating a shared library, and this is a reloc
@@ -3692,11 +3732,16 @@ elf_s390_finish_dynamic_symbol (bfd *output_bfd,
 	  if (!h->def_regular)
 	    {
 	      /* Mark the symbol as undefined, rather than as defined in
-		 the .plt section.  Leave the value alone.  This is a clue
+		 the .plt section.  Leave the value if there were any
+		 relocations where pointer equality matters (this is a clue
 		 for the dynamic linker, to make function pointer
 		 comparisons work between an application and shared
-		 library.  */
+		 library), otherwise set it to zero.  If a function is only
+		 called from a binary, there is no need to slow down
+		 shared libraries because of that.  */
 	      sym->st_shndx = SHN_UNDEF;
+	      if (!h->pointer_equality_needed)
+		sym->st_value = 0;
 	    }
 	}
     }
@@ -3730,6 +3775,9 @@ elf_s390_finish_dynamic_symbol (bfd *output_bfd,
 	    }
 	  else
 	    {
+	      if (!h->pointer_equality_needed)
+		abort ();
+
 	      /* For non-shared objects explicit GOT slots must be
 		 filled with the PLT slot address for pointer
 		 equality reasons.  */
@@ -4344,6 +4392,19 @@ elf_s390_create_dynamic_sections (bfd *dynobj,
   return true;
 }
 
+/* Return TRUE if symbol should be hashed in the `.gnu.hash' section.  */
+
+static bool
+elf_s390_hash_symbol (struct elf_link_hash_entry *h)
+{
+  if (h->plt.offset != (bfd_vma) -1
+      && !h->def_regular
+      && !h->pointer_equality_needed)
+    return false;
+
+  return _bfd_elf_hash_symbol (h);
+}
+
 /* Why was the hash table entry size definition changed from
    ARCH_SIZE/8 to 4? This breaks the 64 bit dynamic linker and
    this is the only reason for the s390_elf64_size_info structure.  */
@@ -4424,6 +4485,7 @@ static const struct elf_size_info s390_elf64_size_info =
 #define elf_backend_sort_relocs_p	      elf_s390_elf_sort_relocs_p
 #define elf_backend_additional_program_headers elf_s390_additional_program_headers
 #define elf_backend_modify_segment_map	      elf_s390_modify_segment_map
+#define elf_backend_hash_symbol		      elf_s390_hash_symbol
 
 #define bfd_elf64_mkobject		elf_s390_mkobject
 #define elf_backend_object_p		elf_s390_object_p
diff --git a/ld/testsuite/ld-elf/pr29655.rd b/ld/testsuite/ld-elf/pr29655.rd
new file mode 100644
index 000000000000..b294ca95a641
--- /dev/null
+++ b/ld/testsuite/ld-elf/pr29655.rd
@@ -0,0 +1,5 @@
+Symbol table '\.dynsym' contains [0-9]+ entries:
+ +Num: +Value +Size Type +Bind +Vis +Ndx Name
+#...
+ +[0-9]+: +0+ +0 +FUNC +GLOBAL +DEFAULT +UND +fun_public
+#...
diff --git a/ld/testsuite/ld-elf/pr29655a.c b/ld/testsuite/ld-elf/pr29655a.c
new file mode 100644
index 000000000000..e523f82d4564
--- /dev/null
+++ b/ld/testsuite/ld-elf/pr29655a.c
@@ -0,0 +1,20 @@
+#include <stdio.h>
+
+typedef void Fn();
+
+void __attribute__((visibility("hidden")))
+fun (void)
+{}
+
+extern void fun_public() __attribute__((alias("fun")));
+
+void
+call_callback (Fn *callback)
+{
+  if (callback == fun)
+    printf("PASS\n");
+  else
+    printf("FAIL\n");
+
+  callback ();
+}
diff --git a/ld/testsuite/ld-elf/pr29655b.c b/ld/testsuite/ld-elf/pr29655b.c
new file mode 100644
index 000000000000..527fd462b944
--- /dev/null
+++ b/ld/testsuite/ld-elf/pr29655b.c
@@ -0,0 +1,15 @@
+#ifndef __PIC__
+#error "this file must be compiled with -fPIC"
+#endif
+
+typedef void Fn();
+void fun_public(void);
+void call_callback(Fn *callback);
+
+int
+main ()
+{
+  fun_public ();
+  call_callback (fun_public);
+  return 0;
+}
diff --git a/ld/testsuite/ld-elf/shared.exp b/ld/testsuite/ld-elf/shared.exp
index 808ad6ffdb6c..a44a65b3428b 100644
--- a/ld/testsuite/ld-elf/shared.exp
+++ b/ld/testsuite/ld-elf/shared.exp
@@ -1863,3 +1863,33 @@ run_ld_link_tests [list \
 	"pr23658-2" \
     ] \
 ]
+
+# PR 29655
+run_cc_link_tests [list \
+    [list \
+	"Build pr29655.so" \
+	"-shared" \
+	"-fPIC" \
+	{ pr29655a.c } \
+	{} \
+	"pr29655.so" \
+    ] \
+]
+# PR 29655 (cont.): Check that in PIC code linked as PDE taking the address
+# of a function defined in a DSO results in the function address (from GOT)
+# and not the "canonical PLT" address from the PDE.
+# This is just an optimization and both is valid, although libraries may
+# depend on this specific behavior, so do not complain loudly.
+setup_xfail *-*-*
+clear_xfail aarch64-*-* alpha-*-* arm*-*-* hppa-*-* i?86-*-* ia64-*-* \
+    microblaze-*-* mips-*-* mips64-*-* powerpc*-*-* s390x-*-* x86_64-*-*
+run_cc_link_tests [list \
+    [list \
+	"Build pr29655" \
+	"$NOPIE_LDFLAGS -Wl,--no-as-needed,-rpath,tmpdir tmpdir/pr29655.so" \
+	"-fPIC" \
+	{ pr29655b.c } \
+	{{readelf {--dyn-syms --wide} pr29655.rd}} \
+	"pr29655" \
+    ] \
+]
diff --git a/ld/testsuite/ld-s390/plt_64-1.wf b/ld/testsuite/ld-s390/plt_64-1.wf
index 61dc4c7a9894..eac5502e111b 100644
--- a/ld/testsuite/ld-s390/plt_64-1.wf
+++ b/ld/testsuite/ld-s390/plt_64-1.wf
@@ -19,14 +19,14 @@ Contents of the .eh_frame section:
   DW_CFA_nop
   DW_CFA_nop
 
-00000018 000000000000001c 0000001c FDE cie=00000000 pc=00000000010002b8..00000000010002e4
+00000018 000000000000001c 0000001c FDE cie=00000000 pc=00000000010002b0..00000000010002dc
   DW_CFA_remember_state
-  DW_CFA_advance_loc: 6 to 00000000010002be
+  DW_CFA_advance_loc: 6 to 00000000010002b6
   DW_CFA_offset: r14 at cfa-48
   DW_CFA_offset: r15 at cfa-40
-  DW_CFA_advance_loc: 8 to 00000000010002c6
+  DW_CFA_advance_loc: 8 to 00000000010002be
   DW_CFA_def_cfa_offset: 320
-  DW_CFA_advance_loc: 24 to 00000000010002de
+  DW_CFA_advance_loc: 24 to 00000000010002d6
   DW_CFA_restore_state
   DW_CFA_nop
   DW_CFA_nop
diff --git a/ld/testsuite/ld-s390/plt_64-1_eh.wf b/ld/testsuite/ld-s390/plt_64-1_eh.wf
index 717e7a7e2a61..794aeee23864 100644
--- a/ld/testsuite/ld-s390/plt_64-1_eh.wf
+++ b/ld/testsuite/ld-s390/plt_64-1_eh.wf
@@ -19,7 +19,7 @@ Contents of the .eh_frame section:
   DW_CFA_nop
   DW_CFA_nop
 
-00000018 0000000000000014 0000001c FDE cie=00000000 pc=0000000001000258..00000000010002b8
+00000018 0000000000000014 0000001c FDE cie=00000000 pc=0000000001000250..00000000010002b0
   DW_CFA_nop
   DW_CFA_nop
   DW_CFA_nop
