The simple answer is anything >= X and/or Y should work.
You can get more specific using
readelf, at least for the library dependencies.
readelf -d yourfile will have
NEEDED lines for the library
SONAMEs that you require. For glibc this has been
libc.so.6 for a long time now. If you go find that file, you’ll see it’s actually a symlink to a more specific version like
NEEDED lines will matter more for other libraries that don’t keep such a long-term stability, like
readelf -V yourfile will tell you the actual symbol versions required. So if the highest you see
GLIBC_2.14, for example, then you’ll need at least 2.14 to run it. Not all symbols will have version tags, especially from libraries that don’t maintain a long term
SONAME, then they probably don’t version symbols either.
It may be that the highest GLIBC_X.Y symbol used is actually less than the version of glibc you compiled against, which means you could indeed run against that earlier version. But if you need to run on that earlier version, it’s best to just compile against that baseline so you’re sure not to pick up newer symbols.
readelf -s yourfile will show the symbol table, including versions, so you might see something like
memcpy@GLIBC_2.14. This can help you figure out which new symbols you’re actually using.
aside on memcpy
memcpy in particular is kind of interesting. My
libc.so has two,
memcpy@GLIBC_2.2.5. The difference is that in 2.14 they started enforcing that the source and destination must not overlap. But they treated this as an ABI change, and the old symbol still has the old behavior that was basically no different than
memmove. So your binaries that were linked to the old version won’t be affected by the new behavior.
All this is to say how libraries are versioned. I’m actually not sure if there’s a good way to tell what kernel version is really required. It usually has to do with what syscalls are made, but even if you trace that, it’s still possible for a program to deal with
ENOSYS on an older system and try a different path. Even Rust itself does this in a few places, like using
pipe2 so it can apply the
O_CLOEXEC flag, but falling back to plain
pipe as needed.