Possible bug in binutils for MinGW-w64 related to linking weak symbols
Martin Storsjö
martin@martin.st
Thu Mar 19 14:27:43 GMT 2026
On Thu, 19 Mar 2026, Eric Botcazou wrote:
>> We had that discussion on the mingw list already, but in my opinion
>> both foo and bar are properly defined, so not being able to link foo
>> looks like a bug to me. It works with clang/llvm.
>
> __attribute__((weak))__ is a GNU extension so the GNU toolchain is the de
> facto reference implementation for it.
FWIW, when I implemented support for these aspects of the GNU extension in
LLVM, I tried to model it according to how it works in the GNU toolchain.
But as far as I saw when implementing it, there's no reason why it should
need to be marked as weak on the side that uses the symbol.
> Requiring that the attribute be used consistently on all the
> declarations seems fairly reasonable to me.
Maybe, but if the point of the use of the weak attribute here, is that
there _may_ be a non-weak definition of the symbol, which should be used
in that case, otherwise the weak symbol should be used - then I don't
think it should be required.
However, there definitely are other bugs involved here as well:
If I modified the testcase as provided at
https://github.com/johannesthoma/mingw-weaktest.git to declare the symbol
"foo" as weak for where it is being used, then linking does indeed
succeed, and it works correctly at runtime:
diff --git a/foo.h b/foo.h
index a482e57..b65a891 100644
--- a/foo.h
+++ b/foo.h
@@ -1,2 +1,2 @@
-int foo(void);
+int __attribute__((weak)) foo(void);
int __attribute__((weak)) bar(void);
But if I switch the order that the objects are linked, so that the use of
the weak symbol comes first, and the definition of the weak symbol comes
later, then we get a crash at runtime:
diff --git a/Makefile b/Makefile
index fd2e035..c76d0e8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
CC=i686-w64-mingw32-gcc
foobar: foo.o foobar.o
- $(CC) foo.o foobar.o -o $@
+ $(CC) foobar.o foo.o -o $@
clean:
rm -f *.o
> The difference is that bar is declared weak also in the foo.h
> header file while foo is not. The objdump of the foobar.o file
> lists a (suspicious) entry:
>
> [ 17](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 .weak._bar._main
>
> for bar but not for foo.
This is a "default symbol" for the use of the weak reference to "bar"
within this object file.
In COFF, when you have a weak symbol "bar", this is made as a "COFF weak
external", which is essentially an alias symbol, which needs to point at a
different symbol.
When you have a weak definition of a symbol, then the symbol "bar" points
at another symbol which is the actual definition. In this case, that
symbol is named ".weak._bar." (which actually is a regular, non-weak
symbol).
When you use a declaration of a weak symbol (which means that the symbol
either may be defined, or may be undefined, when it essentially is a null
pointer), then the symbol "bar" points at an absolute symbol with the
address zero. The idea here is that if nothing else overrides the symbol,
then it uses the fallback value of absolute zero (and in that case, the
user should check if it is null before referencing it).
The COFF weak external symbol with the name "bar" needs to point at
another symbol. And due to details in how this works in COFF, the symbol
it points at needs to be another external symbol (it can't be a
static/local/internal symbol).
The weird symbol naming, e.g. ".weak._bar._main", comes from the fact that
you might want to have multiple objects all referencing the same weak
symbol. If all of them define the same ".weak._bar", then there's multiple
definitions of that. Therefore, both GNU as and LLVM try to disambiguate
these; if there's a non-weak external symbol in the same object file, then
this symbol name (which needs to be unique, otherwise there'd be multiple
definition errors elsewhere anyway) allows making an unique identifier for
the weak fallback. I.e. ".weak._bar._main" means "the fallback symbol for
_bar, within the object file that otherwise defines e.g. _main".
Now for the crash when linking foobar.o before foo.o; foobar.o (which has
a weak declaration of "foo") has its own default fallback as a null
pointer, and foo.o has a weak declaration of "foo" as an actual function
implementation. It seems like GNU ld just picks the first definition of
"foo" here, which is the null pointer fallback, rather than the one
pointing at an actual implementation.
// Martin
More information about the Binutils
mailing list