1

Consider the following source files:

a.c:

extern int baz();

int foo() { return 123; }
int bar() { return baz() + 1; }

b.c:

extern int foo();

int main() { return foo(); }

Now, when I try to build a program using these sources, here's what happens:

$ gcc -c -o a.o a.c
$ gcc -c -o b.o b.c
$ gcc -o prog a.o b.o
/usr/bin/ld: a.o: in function `bar':
a.c:(.text+0x15): undefined reference to `baz'
collect2: error: ld returned 1 exit status

This is on Devuan GNU/Linux Chimaera, with GNU ld 2.35.2, GCC 10.2.1.

Why does this happen? I mean, one does not need any complex optimization to know that baz() is not really needed in foo() - ld naturally notices this at some point - e.g. when finishing its traversal of foo() without noticing a location where baz() is used.

Now, you could say "einpoklum, you didn't ask the compiler to go to any trouble for you" - and that's fair, I guess, but even if I use -O3 with these instructions, I get the same error.

Note: with LTO and optimization enabled, we can circumvent this issue:

$ gcc -c -flto -O1 -o b.o b.c
$ gcc -c -flto -O1 -o a.o a.c
$ gcc -o prog -O1 -flto a.o b.o
$ /prog ; echo $?;
123
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • enable dead code removal and use -fdata-sections -ffunction-sections – 0___________ Jan 21 '22 at 20:53
  • @0___________: 1. But dead code will be removed anyway when linking, won't it? It's not like the linker puts _all_ of the library in the executable... or does it? 2. Why the f*** do I need to use arcane `-f` switches for something trivial to happen? :-( – einpoklum Jan 21 '22 at 20:55
  • @ Does it work? – 0___________ Jan 21 '22 at 20:57
  • @0___________: I'm not sure what you mean by "enable dead code removal". Will `-O3` take care of it? Also, are these switches for compilation or for linking? When I use `-O3 -flto -fdata-sections -ffunction-sections` with all gcc invocations, it doesn't work. – einpoklum Jan 21 '22 at 20:57
  • 1
    The *compiler* does not know `bar` is not going to be used and is placing it into the common `.text` section. The linker knows, but it cannot eliminate code with function granularity, it can remove sections. This is why each function needs to be placed in a separate section for that. – Eugene Sh. Jan 21 '22 at 21:00
  • @einpoklum `--gc-sections` – 0___________ Jan 21 '22 at 21:00
  • @EugeneSh.: "it cannot eliminate code with function granularity" - 1. Why not? 2. So what? It can keep `bar()` with its unresolved symbol in the final executable and none will be the wiser. Or it can satisfy the unresolved symbol with a stub, or with the `exit()` function etc. – einpoklum Jan 21 '22 at 21:02
  • @0___________: 1. `--gc-sections` where? 2. There is no such option. – einpoklum Jan 21 '22 at 21:02
  • it is `ld` option – 0___________ Jan 21 '22 at 21:03
  • @einpoklum Can't tell the exact reason, but it is a limitation pretty common to different compilers I am familiar with. – Eugene Sh. Jan 21 '22 at 21:03
  • @EugeneSh.: See edited comment. – einpoklum Jan 21 '22 at 21:05
  • @EugeneSh. It speeds up the process. Sections are marked as hot or cold. Simply elimination is possible without special offset tables magic. Single-pass is possible – 0___________ Jan 21 '22 at 21:07
  • @0___________: 1. If the sped-up process fails, one should go the slow way. 2. A linker switch could say `--dont-speed-up-the-process`. – einpoklum Jan 21 '22 at 21:08
  • @einpoklum it is not needed complication for no reason. You simply need to know your tools. Everything is explained in the documentation – 0___________ Jan 21 '22 at 21:10
  • Someone has to decide on what the default behaviour is. They've hopefully made that decision based on usage and implementation considerations. You personally may prefer different defaults but if the defaults were done your way someone else would want the other way. – kaylum Jan 21 '22 at 21:15
  • @kaylum: I don't mind this not being default behavior, but it should be single switch with an easy-to-understand name. – einpoklum Jan 21 '22 at 22:16

2 Answers2

2

In a “plain” traditional compilation of this code:

extern int baz();

int foo() { return 123; }
int bar() { return baz() + 1; }

the compiler creates one object module that contains the code of both routines along with definitions for symbols foo and bar and a reference to baz. There is nothing to tell the linker where the code belonging to foo begins and ends, where the code belonging to bar begins and ends or even that any given piece of code—or any given byte in the object module—belongs only to one of foo or bar. Had I written in assembly and assembled to make an object module, I could have included code in foo that jumped into bar (using only hard-coded offsets calculated by the assembler and not revealed in any symbols visible to the linker) or vice-versa.

So the linker has no way of knowing that foo and bar can be separated.

Later, a protocol was created for the compiler to keep functions separated and to provide sufficient information in the object modules that the linker could determine where they were separated and to tell the linker it was okay to separate functions. When the options for that are enabled, the linker may be able to include foo in the program without including bar.

That this feature is not yet the default in the tools is a matter of legacy in various build systems and projects, inertia, and current practice.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • OP can build gcc himself with these options enabled by default if he wishes to. – 0___________ Jan 21 '22 at 21:17
  • Interesting! 1. But doesn't that make linking a lot slower, when using large libraries? I mean, if my library has 100 functions but my program has 1, I would need to do 100x external symbol resolutions than I would do if I could work on just the function I use. And I may be able to entirely avoid reading some objects and libraries altogether. 2. What is that protocol called? 3. Can't you enable this option with a compiler switch? – einpoklum Jan 21 '22 at 22:06
2

If you use gcc and binutils ld to build your programs you need to place functions in separate sections. It is archived by -fdata-sections & -ffunction-sections command line options.

Same with data. Then if you do not want dead code to be included in your executable you need to enable it by using --gc-sections ld option.

Putting this all together:

$ gcc -fdata-sections -ffunction-sections -c -o a.o a.c
$ gcc -c -o b.o b.c
$ gcc -Wl,--gc-sections -o prog a.o b.o
$ /prog ; echo $?
123

If you want to enable it by default simple build GCC with those options enabled.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
0___________
  • 60,014
  • 4
  • 34
  • 74