Hi Richard, Thanks for your reply. I've been thinking about this more and I believe there is good justification for GCC to have a way to opt out of the split-in-pieces optimization for string literals.
The main argument is that it makes binaries more transparent and easier to
audit. A good analogue is that distributions like Fedora and Ubuntu opt in to
-fno-omit-frame-pointer to improve the effectiveness of profiling and debugging
tools, at a minor performance penalty. I consider `strings ./binary` to be in
the same toolbox, and allowing distributions to make the trade-off between
transparency and performance is reasonable.
Another argument is that there is sufficient evidence that package managers and
distributions *want* string scanning to work despite them being aware it's never
entirely reliable. You can find scripts and tooling in RPM (Fedora, RHEL, ...),
Arch Linux, FreeBSD ports, Homebrew, and elsewhere.
For the Guix package manager string scanning is considered crucial for garbage
collection; as I understand it they recursively scan text files and binaries
for certain hashes to determine whether a certain dependency is no longer
referenced.
Similarly, Conda and Spack allow users to do unprivileged installs of binaries
to their home directories, which requires binary patching. Given the popularity
of Conda, I think it's safe to say that it works in practice, despite being
somewhat brittle.
Regarding your comment
> "Unfixed" the object file still contains the strings:
>
> > strings t.o
> /opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdef89abcdefghijklmnopqrstuvwxyz/foo/opt/array/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefg9abcdefghijklmnopqrstuvwxyz/bar
notice that this is more of an argument to the opposite, because it shows two
separate issues. The first is that two distinct strings from the source file
show up as a *single* string in the binary. The second is that it shows overlap
in the last two pieces.
The input was
/opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz/foo
but `strings` shows a different value
/opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdef89abcdefghijklmnopqrstuvwxyz/foo
the explanation being that it's split into these 32-byte pieces where `89abcdef`
overlaps:
klmnopqrstuvwxyz0123456789abcdef
89abcdefghijklmnopqrstuvwxyz/foo
because the string length is not a multiple of 32 and all move operations
use ymm registers.
I'm looking at a narrower patch that adds just `-fno-split-string-constants`.
The idea is that just for strings the pieces are loaded from the embedded string
literal itself instead of from constant pool entries or immediates, so the
literal stays intact in the binary. Would you consider a contribution along
these lines?
Best,
Harmen Stoppels
On Thursday, 12 November 2026 at 11:13 AM, Richard Biener
<[email protected]> wrote:
> On Wed, Nov 11, 2026 at 2:01 PM Harmen Stoppels <[email protected]>
> wrote:
> >
> > Copies from constant data, such as a memcpy from a string literal, are
> > expanded into stores of vector constants when store_by_pieces allows it.
> > This splits the string into pieces of vector width, the last one often
> > overlapping with the previous one, so the string literal may no longer
> > appear in the resulting binary.
> >
> > This complicates scanning binaries for embedded absolute paths, which is
> > common practice in package managers.
> >
> > The new option allows package managers to opt out of this optimization,
> > so they can reliably scan binaries for embedded absolute paths.
>
> At least your testcase is fragile as it relies on assembler scanning.
> "Unfixed" the object file still contains the strings:
>
> > strings t.o
> /opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdef89abcdefghijklmnopqrstuvwxyz/foo/opt/array/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefg9abcdefghijklmnopqrstuvwxyz/bar
>
> albeit in .data section.
>
> I think the approach of scanning binaries is just fragile and if you
> can resort to a compile-time workaround (add your new flag) you
> could as well invent other compile-time means to diagnose
> absolute paths (matching some pattern).
>
> So no, this doesn't look like something we'd want.
>
> Richard.
>
> > gcc/ChangeLog:
> >
> > * common.opt (fstore-constants-by-pieces): New option.
> > * doc/invoke.texi (-fno-store-constants-by-pieces): Document.
> > * expr.cc (can_store_by_pieces): Return false for copies from
> > constant data with -fno-store-constants-by-pieces.
> >
> > gcc/testsuite/ChangeLog:
> >
> > * gcc.target/i386/store-constants-by-pieces-1.c: New test.
> >
> > Signed-off-by: Harmen Stoppels <[email protected]>
> > ---
> >
> > Hello,
> >
> > In the Spack package manager we scan binaries for embedded absolute
> > paths, which only works if literal C-strings are preserved in the
> > binary. That is not the case when targeting x86_64, where a default
> > optimization breaks the string literals up into constants that fit in
> > vector registers. As I understand it, there is no guarantee that those
> > constants are stored contiguously (which could have the happy
> > side-effect of getting the literal back in the binary), but even if they
> > did, the very last chunk is special in that it overlaps with the
> > previous chunk when the literal's size is not a multiple of the vector
> > width.
> >
> > As far as I can see, -mmove-max=128 is related, but that only lowers the
> > limit to 255 bytes, and also negatively affects ordinary memory copies,
> > so it does not really help.
> >
> > Therefore, I'm proposing a compiler option that allows us to disable
> > this optimization, with the hope that we can reliably scan for
> > non-relocatable absolute paths that accidentally get embedded in
> > binaries.
> >
> > I have not checked, but presumably we are not the only package manager
> > that has such audits after a build.
> >
> > Best,
> >
> > Harmen Stoppels
> >
> > gcc/common.opt | 4 ++++
> > gcc/doc/invoke.texi | 13 ++++++++++++-
> > gcc/expr.cc | 3 +++
> > .../i386/store-constants-by-pieces-1.c | 18 ++++++++++++++++++
> > 4 files changed, 37 insertions(+), 1 deletion(-)
> > create mode 100644
> > gcc/testsuite/gcc.target/i386/store-constants-by-pieces-1.c
> >
> > diff --git a/gcc/common.opt b/gcc/common.opt
> > index c40606570e9..14fd9453589 100644
> > --- a/gcc/common.opt
> > +++ b/gcc/common.opt
> > @@ -1956,6 +1956,10 @@ fstrict-volatile-bitfields
> > Common Var(flag_strict_volatile_bitfields) Init(-1) Optimization
> > Force bitfield accesses to match their type width.
> >
> > +fstore-constants-by-pieces
> > +Common Var(flag_store_constants_by_pieces) Init(1) Optimization
> > +Expand copies from constant data into stores of immediates.
> > +
> > fstore-merging
> > Common Var(flag_store_merging) Optimization
> > Merge adjacent stores.
> > diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
> > index ef9cb025c22..11927e2dd16 100644
> > --- a/gcc/doc/invoke.texi
> > +++ b/gcc/doc/invoke.texi
> > @@ -681,7 +681,8 @@ Objective-C and Objective-C++ Dialects}.
> > -fsingle-precision-constant -fsplit-ivs-in-unroller -fsplit-loops
> > -fspeculatively-call-stored-functions
> > -fsplit-wide-types -fsplit-wide-types-early -fssa-backprop -fssa-phiopt
> > --fstdarg-opt -fstore-merging -fstrict-aliasing -fipa-strict-aliasing
> > +-fstdarg-opt -fstore-constants-by-pieces -fstore-merging
> > +-fstrict-aliasing -fipa-strict-aliasing
> > -fthread-jumps -ftracer -ftree-bit-ccp
> > -ftree-builtin-call-dce -ftree-ccp -ftree-ch -ftree-coalesce-vars
> > -ftree-copy-prop -ftree-cselim -ftree-dce -ftree-dominator-opts
> > @@ -15420,6 +15421,16 @@ merges contiguous stores of immediate values
> > narrower than a word into fewer
> > wider stores to reduce the number of instructions. This is enabled by
> > default
> > at @option{-O2} and higher as well as @option{-Os}.
> >
> > +@opindex fstore-constants-by-pieces
> > +@opindex fno-store-constants-by-pieces
> > +@item -fno-store-constants-by-pieces
> > +Do not expand copies from constant data, such as a @code{memcpy} from a
> > +string literal or the initialization of a character array, into stores of
> > +immediate values. The data is copied from the constant in memory instead,
> > +so that it stays intact in the object file. This is useful when binaries
> > +are scanned for embedded strings, such as absolute paths. The default is
> > +@option{-fstore-constants-by-pieces}.
> > +
> > @opindex ftree-ter
> > @opindex fno-tree-ter
> > @item -ftree-ter
> > diff --git a/gcc/expr.cc b/gcc/expr.cc
> > index c654387ab01..bd8119af61d 100644
> > --- a/gcc/expr.cc
> > +++ b/gcc/expr.cc
> > @@ -1772,6 +1772,9 @@ can_store_by_pieces (unsigned HOST_WIDE_INT len,
> > if (len == 0)
> > return true;
> >
> > + if (!memsetp && !flag_store_constants_by_pieces)
> > + return false;
> > +
> > if (!targetm.use_by_pieces_infrastructure_p (len, align,
> > memsetp
> > ? SET_BY_PIECES
> > diff --git a/gcc/testsuite/gcc.target/i386/store-constants-by-pieces-1.c
> > b/gcc/testsuite/gcc.target/i386/store-constants-by-pieces-1.c
> > new file mode 100644
> > index 00000000000..5feff67cb29
> > --- /dev/null
> > +++ b/gcc/testsuite/gcc.target/i386/store-constants-by-pieces-1.c
> > @@ -0,0 +1,18 @@
> > +/* { dg-do compile } */
> > +/* { dg-options "-O2 -mavx2 -fno-store-constants-by-pieces" } */
> > +
> > +void
> > +foo (char *d)
> > +{
> > + __builtin_memcpy (d,
> > "/opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz/foo",
> > 88);
> > +}
> > +
> > +void
> > +bar (void (*f) (char *))
> > +{
> > + char b[] =
> > "/opt/array/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz/bar";
> > + f (b);
> > +}
> > +
> > +/* { dg-final { scan-assembler
> > "/opt/memcpy/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz/foo"
> > } } */
> > +/* { dg-final { scan-assembler
> > "/opt/array/0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz/bar"
> > } } */
> > --
> > 2.53.0
> >
> >
>
0001-middle-end-Add-fno-split-string-constants.patch
Description: Binary data
