The linker starts with the object files that are provided directly (and not indirectly by an archive file). Then it identifies unsatisfied references, i.e. function calls and external variables whose target is not contained in the object files included so far. It then searches the libraries for these targets and adds the matching object files to the executable being built. This will satisfy some reference but possibly add additional unsatisfied references. The process is repeated until all references are satisfied (success) or the target of some unsatisfied references cannot be found (error).
Basically, the loader includes all object files that are directly or indirectly referenced from a few root files.
Another aspect you could investigate is the section granularity. The default is that the entire compilation unit becomes a single ELF section. So if a
.c file contains many functions, they will all be included if just a single one is used.
The standard C library works around this by putting each function into a separate
.c file. But there are also compiler options that could help and I don’t know if PlatformIO uses them (it’s likely platform specific). Have a look at the following options:
-ffunction-sections: Creates a separate ELF section for each function
-fdata-sections: Dito for data (global and static variables)
-Wl,--gc-sections: Instructs the linker to garbage collect unused ELF sections
These options are likely to increase the code size of the individual functions but might result in an overall smaller executable because fewer functions are included.