Bug 32006 - ld pulls symbols from wrong library
Summary: ld pulls symbols from wrong library
Status: UNCONFIRMED
Alias: None
Product: binutils
Classification: Unclassified
Component: binutils (show other bugs)
Version: 2.41
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-07-22 14:54 UTC by Felix von Leitner
Modified: 2024-07-28 11:11 UTC (History)
3 users (show)

See Also:
Host:
Target:
Build:
Last reconfirmed:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Felix von Leitner 2024-07-22 14:54:01 UTC
I'm not quite sure what is going on here.
Basically, I'm linking a program like this:

  $ diet -v gcc -o t test.c -pthread -Wl,-Map,mapfile

diet is a wrapper from dietlibc and it will transform the command line to this:

  $ gcc -nostdlib -static -L/opt/diet/lib-x86_64 /opt/diet/lib-x86_64/start.o -o t test.c -D_REENTRANT -lpthread -Wl,-Map,mapfile -isystem /opt/diet/include -D__dietlibc__ /opt/diet/lib-x86_64/libc.a -lgcc /opt/diet/lib-x86_64/crtend.o

Now, there is a symbol called __stdio_init_file that is exported by fdglue2.o in libc.a and it is also exported from pthread_fdglue2.o in libpthread.a.

My understanding is that ld should always satisfy references from the first library on the command line that has an export under that name (leaving trickery like weak symbols aside for now).

Here's the load list from the mapfile:

LOAD /opt/diet/lib-x86_64/start.o
LOAD /tmp/ccnyXICz.o
LOAD /opt/diet/lib-x86_64/libpthread.a
LOAD /opt/diet/lib-x86_64/libc.a
LOAD /usr/lib64/gcc/x86_64-pc-linux-gnu/14.1.0/libgcc.a
LOAD /opt/diet/lib-x86_64/crtend.o

libpthread.a is named first, then libc.a.

Here's what nm says about libpthread.a:

pthread_fdglue2.o:
0000000000000000 T __stdio_init_file
                 U __stdio_init_file_nothreads

Here's what nm says about libc.a:

fdglue2.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U __errno_location
                 U __libc_close
                 U __stdio_atexit
                 U __stdio_flushall
0000000000000000 W __stdio_init_file
0000000000000000 T __stdio_init_file_nothreads
                 U __stdio_root
                 U atexit
                 U free
                 U fstat
                 U malloc

Making __stdio_init_file weak is my current attempt to fix the situation but it is not helping. So both libraries export __stdio_init_file and libpthread.a is named first, yet ld takes the one from libc instead:

/opt/diet/lib-x86_64/libc.a(fdglue2.o)                                                                                                                /opt/diet/lib-x86_64/libc.a(fopen.o) (__stdio_init_file)

What the hell is going on? Is this a bug in binutils ld?

For good measure I tried -fuse-ld=gold (same behavior) and -fuse-ld=lld (pulls in pthread_fdglue2.o) and -fuse-ld=mold (also pulls in pthread_fdglue2.o).

Have I been relying on undefined behaviour for over a decade?
Comment 1 H.J. Lu 2024-07-22 20:49:41 UTC
(In reply to Felix von Leitner from comment #0)
> I'm not quite sure what is going on here.
> Basically, I'm linking a program like this:
> 
>   $ diet -v gcc -o t test.c -pthread -Wl,-Map,mapfile
> 
> diet is a wrapper from dietlibc and it will transform the command line to
> this:
> 
>   $ gcc -nostdlib -static -L/opt/diet/lib-x86_64
> /opt/diet/lib-x86_64/start.o -o t test.c -D_REENTRANT -lpthread
> -Wl,-Map,mapfile -isystem /opt/diet/include -D__dietlibc__
> /opt/diet/lib-x86_64/libc.a -lgcc /opt/diet/lib-x86_64/crtend.o
>
> Now, there is a symbol called __stdio_init_file that is exported by
> fdglue2.o in libc.a and it is also exported from pthread_fdglue2.o in
> libpthread.a.

This is expected. Please add -lpthread after libc.a.
Comment 2 Alan Modra 2024-07-22 23:55:07 UTC
Standard unix linkers extract an object from an archive if the object satisifies an undefined symbol reference in objects seen before the archive is searched.  Ordering on the command line matters.
Comment 3 Felix von Leitner 2024-07-23 09:44:50 UTC
Sorry for still pestering you about this, but let's examine the problem more closely.

  $ cat a.c
  extern int a();
  extern int b(); extern int b_dep();

  int main() {
    a();
    b();
  }
  $ cat a_first.c
  int a() { return 0; }
  __asm(".section .gnu.warning.a\n.string \"a from a_first\"\n.previous");
  $ cat a_second.c
  int a() { return 1; }
  __asm(".section .gnu.warning.a\n.string \"a from a_second\"\n.previous");
  $ cat b_first.c
  extern int b_dep();
  int b() { return b_dep(); }
  $ cat b_dep_first.c
  int b_dep() { return 0; }
  __asm(".section .gnu.warning.b_dep\n.string \"b_dep from b_dep_first\"\n.previous");
  $ cat b_dep_second.c
  int b_dep() { return 1; }
  __asm(".section .gnu.warning.b_dep\n.string \"b_dep from b_dep_second\"\n.previous");
  $ gcc -c *.c
  $ ar cru first.a a_first.o b_first.o b_dep_first.o
  $ ar cru second.a a_second.o b_dep_second.o
  $

Remember, I want one library to reliably overrule the other one.
This setup exercises two scenarios. Scenario 1: I call a() that is in first.a and second.a. Second, I call b() that is in first.a but calls b_dep() that is in both libraries.
I need a way to make sure one library always wins.

I added some linker warnings so we can see which object was pulled in.

Let's see what happens:

  $ gcc -o a a.c first.a second.a
  a.c:(.text+0xa): warning: a from a_first
  b_first.c:(.text+0xa): warning: b_dep from b_dep_first
  $

Please explain that to me. You just told me to put libpthread.a AFTER libc.a so that it overrules it.

But it gets wilder:

  $ gcc -o a a.c second.a first.a
  a.c:(.text+0xa): warning: a from a_second
  b_first.c:(.text+0xa): warning: b_dep from b_dep_first
  $

As you can see, when I switch the libraries the first one does NOT reliably win. Now it wins if I reference the symbol from the main program, but not if the symbol is referenced by something from inside the library.

Please advise how I can get the effect I need. I hope you don't expect me to do preprocessor trickery to name symbols differently. The goal was to implement a libpthread.a. That should work even if you don't have the source code for parts of the program you are trying to link.
Comment 4 Thomas Jahns 2024-07-23 17:00:13 UTC
My understanding is that __stdio_init_file should be in an object file separate from the one using it in libc.a because static linkage means no late binding of symbols will occur.
Comment 5 Felix von Leitner 2024-07-23 17:04:07 UTC
(In reply to Thomas Jahns from comment #4)
> My understanding is that __stdio_init_file should be in an object file
> separate from the one using it in libc.a because static linkage means no
> late binding of symbols will occur.

I'm not sure what you are trying to say.

a) It is in a different object file.
b) Nobody is talking about late binding here.

It is precisedly in a different object file so that the linker can replace that object file with the one from libpthread. That's why it is its own function to begin with.
Comment 6 H.J. Lu 2024-07-23 20:43:44 UTC
(In reply to Felix von Leitner from comment #3)

> Please explain that to me. You just told me to put libpthread.a AFTER libc.a
> so that it overrules it.

What I meant was to use

-lpthread ... libc.a ... -lpthreadd
Comment 7 Thomas Jahns 2024-07-23 21:03:55 UTC
What I meant was that I think static resolution of symbols from the same object file is different from lookups in other files.

Also the documentation of option --start-group option suggests that symbols should be resolved strictly left to right, unless that option is used, i.e.

  $ gcc -nostdlib -static -L/opt/diet/lib-x86_64 /opt/diet/lib-x86_64/start.o -o t test.c -D_REENTRANT -lpthread -Wl,-Map,mapfile -isystem /opt/diet/include -D__dietlibc__ -Wl,--start-group /opt/diet/lib-x86_64/libc.a -lgcc -Wl,--end-group /opt/diet/lib-x86_64/crtend.o

should provide the expected behaviour.
Comment 8 Thomas Jahns 2024-07-23 21:52:58 UTC
Sorry, my fault, misplaced the group flags, should be:

  $ gcc -nostdlib -static -L/opt/diet/lib-x86_64 /opt/diet/lib-x86_64/start.o -o t test.c -D_REENTRANT -Wl,--start-group -lpthread -Wl,-Map,mapfile -isystem /opt/diet/include -D__dietlibc__ /opt/diet/lib-x86_64/libc.a -Wl,--end-group -lgcc /opt/diet/lib-x86_64/crtend.o
Comment 9 Stefan Franke 2024-07-24 07:56:57 UTC
Use brackets around that lib that must be searched recursively:

gcc -o t test.c -pthread '-(' -lpthread '-)' -Wl,-Map,mapfile
Comment 10 dreieck 2024-07-28 11:11:54 UTC
> gcc -o t test.c -pthread '-(' -lpthread '-)' -Wl,-Map,mapfile

I think it should be '-Wl,-(' and '-Wl,-)', since '-(' is an option that the linker understands, but not the C compiler. '-(' (or the equivalent '--start-group') actually also is documented in the 'ld' manpage for me.