StoneThrow
StoneThrow

Reputation: 6285

g++ Cygwin/Linux or version discrepancy

Can someone explain the discrepancy in how two instances of g++ handle compilation of the following code to shared libraries?

Foo.h

#ifndef Foo_h
#define Foo_h

void Foo();

#endif // Foo_h

Foo.cpp

#include "Foo.h"
#include <iostream>

void Foo()
{
    std::cout << "Greetings from Foo()!" << std::endl;
}

Bar.h

#ifndef Bar_h
#define Bar_h

void Bar();

#endif // Bar_h

Bar.cpp

#include "Bar.h"
#include "Foo.h"
#include <iostream>

void Bar()
{
    Foo();
    std::cout << "Greetings from Bar()!" << std::endl;
}

On a true Linux box:

>g++ --version
g++ (GCC) 4.4.7 20120313 (Red Hat 4.4.7-11)
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

>g++ -fpic -c Foo.cpp
>g++ -fpic -c Bar.cpp
>g++ -shared -o libFoo.so Foo.o
>g++ -shared -o libBar.so Bar.o
>

On Cygwin:

>g++ --version
g++ (GCC) 5.4.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
>g++ -fpic -c Foo.cpp
>g++ -fpic -c Bar.cpp
>g++ -shared -o libFoo.so Foo.o
>g++ -shared -o libBar.so Bar.o
Bar.o:Bar.cpp:(.text+0x9): undefined reference to `Foo()'
Bar.o:Bar.cpp:(.text+0x9): relocation truncated to fit: R_X86_64_PC32 against undefined symbol `Foo()'
collect2: error: ld returned 1 exit status

I don't really have enough *nix-savvy to know how to install different/matching versions of g++ on either box to see if that is the cause of the problem (on one of the boxes I wouldn't have the privilege to do so, anyway).

I'd always thought that object files, and by extension, libraries - either static or shared - were allowed to have unresolved symbols, and that it's only when linking an executable that all symbols needed to be resolved. That concept has pretty much held true over several years of development experience, too, so I'm baffled by the error generated on Cygwin. I'm very curious to know what's happening here. Thank you.

UPDATE

An answerer below provided the following suggestion which works: g++ -shared -o libBar.so Bar.o libFoo.so

Look at the resulting contents of libBar.so:

>nm --demangle libBar.so | grep Foo
00000004e4b791c4 I __imp__Z3Foov
00000004e4b7903c I _head_libFoo_so
00000004e4b71750 T Foo()
00000004e4b793ec I libFoo_so_iname

Per my understanding, this means that Foo() is binary-included in libBar.so, i.e. the compiled binary content of Foo() is present in libBar.so.

This is a bit different than the picture I'd had in my head based on the behavior on the true Linux box. I thought each .so would be "standalone" binary code, just like a .o file, or a .a file that was comprised of only one object file.

I guess what I'm having trouble wrapping my head around is that the Cygwin (or g++ 5.4) behavior is saying that a library cannot have unresolved symbols - this feels contrary to what a lot of prior experience has ingrained in me. I get that an executable cannot have unresolveds, but a library ought to be able to have unresolveds, right? After all, you can't execute a library - it has no main(). I know for sure that static libraries can have unresolveds, and I thought difference between shared and static libraries is whether their code is link-time added to the executable binary, or whether their code is runtime looked-up by the executable.

Appreciate further clarity the community can shed here. Thank you.

Upvotes: 0

Views: 112

Answers (2)

n. m. could be an AI
n. m. could be an AI

Reputation: 120059

There are no shared objects on Windows. There are DLLs. DLLs behave differently from Un*x shared objects. In particular, they are not allowed to have undefined symbols. The PE (Portable Executable) format used in Windows for both DLLs and executables just doesn't have a way to express them. Google "pe dll" "undefined symbols", there's a lot of info around.

Cygwin tries very hard to hide peculiarities of Windows from the programmer but there's only so much it can do.

When you link libBar.so against libFoo.so, code from libFoo.so is not physically included in libBar.do. That would defeat the purpose of DLLs to be loaded at run time. Instead, the linking process creates stubs for all functions imported from other DLLs. These are not real functions. You can make sure this is indeed the case by looking for strings:

% strings libFoo.so | grep "Greetings from"
Greetings from Foo
% strings libBar.so | grep "Greetings from"
Greetings from Bar

Upvotes: 1

I think this is more g++ 5.4 vs 4.4 (that's a big difference by the way) than cygwin vs Linux.

A shared object is a very different beast to an object file. In some ways it is more like an executable. The model that 5.4 is using, is that when it links the shared object, it doesn't need to have copies of all the symbols in its hand, but it needs to tell the run-time loader which shared objects the loader can find the remaining symbols in. I suggest:

g++ -shared -o libBar.so Bar.o libFoo.so

(Alternatively, you can use -lFoo if you like, but then you'll need to set up the library paths correctly).

The more interesting question is why g++ 4.4 works as is: That I don't know.

Why does nm show Foo in libBar.so?

Foo() is not binary included in libBar.so. The following is very handy-wavy and approximate. If you want more accuracy, you are going to have to read about the loader and the format of executable files.

The assembler of libBar.so is going to look something like:

Bar():
    CALL 000000    # The zeros are a blank that will be filled in by
                   # the loader with the address of Foo
    PUSH "Greetings from Bar()!"
    PUSH 000000    # Blank to be filled with address of std::cout
    CALL 000000    # Blank to be filled with address of
                   # std::ostream::operator<<(const char*)
... etc

Then elsewhere in libBar.so there will be a section that says something like:

Bar+1    Foo
Bar+9    std::cout
Bar+11   std::ostream::operator<<(const char *)

which tells the loader to fill in Bar+1 with the address for Foo. Finally there will be a section that says:

Foo                                   libFoo.so
std::cout                             libc.so
std::ostream::operator<<(const char*) libc.so

which tells the loader that it can find Foo in libFoo.so etc. It is that last section which nm is reporting.

Upvotes: 2

Related Questions