Nov 12th, 2026
Many[1][2][3][4][5][6][7][8][9] Linux distributions and bundle managers audit just-built binaries for non-relocatable complete paths. If a binary contains a way /path/from/build/machine, it power not run on the end-user's machine.
Now, a essential grep on the binary, or a slightly additional sophisticated
$ strings bin/app | grep /path/from/build/machine
using GNU binutils's strings volition detect these paths, right?
The answer is no, at smallest whenever using GCC and targeting x86-64: you should not assume that cord constants end up exactly in the binary.
Basic example
In this C++ origin we create a std::string from a cord constant:
#include <iostream> #include <string> #define PREFIX "/home/linuxbrew/.linuxbrew" int main() { std::string prefix{PREFIX}; std::cout << prefix << '\n'; } Compile alongside GCC 12 or afterward and run, which prints the path:
$ g++ -O2 main.cpp -o main $ ./main /home/linuxbrew/.linuxbrew
But the way occurs exactly 0 times in the binary itself:
$ grep -acF /home/linuxbrew/.linuxbrew main 0 $ strings ./main | grep -cF /home/linuxbrew/.linuxbrew 0
Surely these bytes must be part of the binary somewhere? Let's run strings on it and grep fair for the /home prefix:
$ strings ./main | grep /home /home/linuxbrew/xbrew/.linuxbrew
Wait, did GCC fair turn
/home/linuxbrew/.linuxbrew
into an entirely different path
/home/linuxbrew/xbrew/.linuxbrew
with a made-up subdirectory xbrew? How?
Store by pieces
The answer is that GCC optimizes our std::string{PREFIX}. The fact that we see item that looks akin the prefix is merely a coincidence.
GCC splits our cord up into two (overlapping) pieces of 16 bytes, akin so
/home/linuxbrew/.linuxbrew |--------------|.......... - changeless 1 ..........|--------------| - changeless 2
which fit exactly in xmm registers, so that it can copy our cord to the std::string buffer using vectorized move instructions.
Filtering the applicable bits from g++ -O2 -S -masm=intel, we get
movdqa xmm0, XMMWORD PTR .LC0[rip] movups XMMWORD PTR [rax], xmm0 movdqa xmm0, XMMWORD PTR .LC1[rip] movups XMMWORD PTR 10[rax], xmm0
and our cord is stored at .LC0 and .LC1 as constants, which fair happen to be next to all another in the binary:
.LC0: .quad 7596498784141404207 .quad 3420313983963919726 .LC1: .quad 7795220194874516088 .quad 8603408430414524009
In general, this happens whenever a cord changeless is copied into a buffer, as alongside std::string, strcpy or memcpy; strings that are lone passed about as pointers, akin in fopen(PREFIX "/x", "r"), remain intact.
Deduplication
Some bundle managers akin Conda and Spack instal packages into a padded prefix during their builds, alongside the idea that they can be “relocated” to a shorter prefix of the user's choice: the binaries are patched in-place. Conda uses a repeated cord _placehold in a directory name to create the prefix lengthy enough.
Now, equal if we choice a 256-byte way (a convenient multiple of all x86-64 enroll widths) by padding the way Conda-style
#define PREFIX "/home/conda/feedstock_root/build_artifacts/" \ "foo_1699999999999/_h_env" \ "_placehold_placehold_placehold_placehold_placehold" \ "_placehold_placehold_placehold_placehold_placehold" \ "_placehold_placehold_placehold_placehold_placehold" \ "_placehold_placehold_placehold_placeho/"
and keep our fingers traversed that GCC puts the pieces of the way contiguously in the binary, we do not see our prefix rear in the binary. What we get alternatively is a much shorter cord of lone 176 bytes (that is 11 times 16):
$ strings chief | grep /home /home/conda/feedstock_root/build_artifacts/foo_1699999999999/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placeho/
This is gratitude to a additional optimization: since the cord _placehold repeats all 10 bytes, the 16-byte pieces reiterate all lcm(10, 16) = 80 bytes, and GCC needs to shop lone 5 distinct 16-byte constants for the padding:
/home/conda/feed stock_root/build _artifacts/foo_1 699999999999/_h_ env_placehold_pl acehold_placehol # note d_placehold_plac # that ehold_placehold_ # this placehold_placeh # is old_placehold_pl # cyclic acehold_placeho/
Is this optimization helpful?
For cord literals, I don't think so. Clang does not divided strings into constants, yet it generates basically the identical instructions. For example, obtain a longer path, and mark AVX2 alongside -march=x86-64-v3:
#define PREFIX "/home/linuxbrew/.linuxbrew/Cellar/foo/1.2.3/share/foo"
Clang merely puts the cord in a division dedicated to strings, .rodata.str1.1:
.L.str: .asciz "/home/linuxbrew/.linuxbrew/Cellar/foo/1.2.3/share/foo"
and copies its 53 bytes alongside two overlapping 32-byte ymm loads and stores, at offsets 21 and 0:
vmovups ymm0, ymmword ptr [rip + .L.str+21] vmovups ymmword ptr [rax + 21], ymm0 vmovups ymm0, ymmword ptr [rip + .L.str] vmovups ymmword ptr [rax], ymm0
That isn't particularly distinct from GCC's version, which loads the identical two pieces from constants in .rodata.cst32 instead:
vmovdqa ymm0, YMMWORD PTR .LC0[rip] vmovdqu YMMWORD PTR [rax], ymm0 vmovdqa ymm0, YMMWORD PTR .LC1[rip] vmovdqu YMMWORD PTR 21[rax], ymm0
So what does GCC gain? In conditions of performance, likely nothing measurable: the two do the identical loads and stores. With repeated pieces, GCC power have a minor edge, since a repeated part is loaded formerly and stored multiple times (11 loads for 16 stores in the before example). It additionally uses aligned reads, but that matters lone for ancient CPUs.
In conditions of size, GCC's pieces can obtain additional area than the cord itself: overlapping bytes are stored twice, and the pieces are aligned to 16, 32 or 64 bytes. The 53-byte way complete takes 64 bytes as two pieces, against 54 bytes as a string. It lone pays off if it results in deduplication, for example whenever many strings portion a prefix of at smallest 16 bytes for SSE2, 32 bytes for AVX2, and 64 bytes for AVX-512. Presumably deduplication does not preserve much in practice, particularly now that several distributions use AVX2 as their baseline.
Can I disable this optimization?
Other than compiling alongside -Os, not really. From GCC 10 onwards (maybe older versions too?) the split-into-pieces optimization triggers for strings up to 256 bytes equal without specifying -march. From GCC 12 it is related to the target's vector width, so longer strings get divided too: up to 512 bytes for AVX2 and 1024 bytes for AVX-512.
I accept the lone authority you have complete this is environment the maximum vector width to 128 bits (forcing xmm operations), by passing -mstore-max=128. But this merely restores old defaults, and method strings of up to 256 bytes are motionless stored in pieces.
Maybe post-hoc scanning is bad practice?
These post-build audits for complete paths are popular, but you could contend that they are too brittle to commencement with, so why anticipate them to activity at all?
Indeed, Debian goes much additional than a bad-path scan. Their audit asserts that a bundle can be rebuilt bit for bit in distinct circumstances: on a distinct machine, alongside a distinct user, period and date, hostname, locale, etc. That is a much additional dependable way to keep machine-specific particulars out of binaries.[10]
I would contend that, notwithstanding that, it is still helpful to keep cord constants exactly in the binary, equal on Debian, since it improves interpretability of binaries. Running strings ./binary is specified a uncomplicated and convenient way to get understanding into an opaque binary blob, that it outweighs the marginal funds that arrive alongside splitting and deduplication. It is reminiscent of Fedora and Ubuntu turning example pointers rear on by default, notwithstanding the small overhead, fair to create profilers and tracing tools activity additional reliably.
Other than that, there is the disagreement of binary patching, which — notwithstanding being considered slightly brittle — is average custom in Conda and Spack: upon instal to a user-specified location, the complete paths are patched in-place accordingly, through what can be seen as a glorified sed invocation. This lone plant if the way is stored in one piece.
If you'd akin to conversation this, a fine location is the gcc-patches thread that I started recently, alongside an first attempt at a place that adds a way to opt out.