c++ Mixing Potential Problems with Different Standards

Keywords: PHP Linker Linux

Recent projects have used C++ versions of C++ to C++11, but since some static libraries (.a) do not have source code, non-C++11 versions of library files are still being used for linking.It seems like there's nothing wrong with running for a few days now, but I'd like to talk about the potential risks of doing so.

First, it should be noted that after upgrading to C++11, the memory layout of some std data structures may change (to be investigated).Initially, I thought that as long as the interfaces exposed by static libraries were not using these incompatible data structures.That is, if all interfaces exposed by a static library are pure C-style and do not use any C++ std data structure, it should be safe to link such a static library.But later it was discovered that this did not seem to be the case...

See how C++ eliminates duplicate code

Suppose you have a template class MyClass<T>defined in the header file my_template.h.The source files x.cpp and y.cpp both contain this header file and instantiate the template class with type int.Since these two source files are completely separate at compile time, both source files generate object file s (x.o and y.o) that contain code for MyClass <int>.But there is actually one copy of MyClass <int>code for an executable program.So there is a duplicate code elimination step in the link phase, going back to the example above where MyClass <int>code in x.o and y.o is merged and there is only one code in the executable.

Linux GCC eliminates duplicate template code through the COMDAT section of the ELF.COMDAT is a special section (both ELF and COFF have the concept of a COMDAT section), usually associated with a string (or possibly the name of the section).When the linker processes an object file, it performs a de-duplication of the section it encounters with the same name to ensure that only one instance exists in the output file of the output.

Look at the code below

// my_template.h

template <typename T>
class MyClass
{
public:

    void func1()
    {
        i1 = 1;
        i2 = 2;
    }

public:

#ifdef TEST
    int i1;
    int i2;
#else
    int i2;
    int i1;
#endif
};

 

The code above defines the template class MyClass <T> and its memory layout depends on a TEST macro.Then we use it this way:

// x.cpp

#include <stdio.h>

#include "my_template.h"

void func_in_x()
{
    MyClass<int> c;
    c.func1();

    printf("i1=%d, i2=%d\n", c.i1, c.i2);
}

// y.cpp

#include <stdio.h>

#define TEST
#include "my_template.h"

void func_in_y()
{
    MyClass<int> c;
    c.func1();

    printf("i1=%d, i2=%d\n", c.i1, c.i2);
}

 

You can see that the macro TEST is not defined in x.cpp, but in y.cpp.Therefore, the memory layout of MyClass <int> should be different between the two compilation units.

Objdump-S x.o sees that the code for the member function func1 is as follows:

0000000000000000 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public:

    void func1()
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    {
        i1 = 1;
   8:   48 8b 45 f8             mov    -0x8(%rbp),%rax
   c:   c7 40 04 01 00 00 00    movl   $0x1,0x4(%rax)
        i2 = 2;
  13:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  17:   c7 00 02 00 00 00       movl   $0x2,(%rax)
    }
  1d:   90                      nop
  1e:   5d                      pop    %rbp
  1f:   c3                      retq

Objdump-S y.o sees that the code for the member function func1 is as follows:

0000000000000000 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public:

    void func1()
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    {
        i1 = 1;
   8:   48 8b 45 f8             mov    -0x8(%rbp),%rax
   c:   c7 00 01 00 00 00       movl   $0x1,(%rax)
        i2 = 2;
  12:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  16:   c7 40 04 02 00 00 00    movl   $0x2,0x4(%rax)
    }
  1d:   90                      nop
  1e:   5d                      pop    %rbp
  1f:   c3                      retq

Looking at the assembler code generated above, MyClass <int> does have a different memory layout.The starting address of member i1 in x.o is at the fourth byte of object memory (offset is 0x4), while the starting address of member i1 in y.o is the address of the object (offset is 0x0).

The main function code is as follows:

void func_in_x();
void func_in_y();

int main()
{
    func_in_x();
    func_in_y();
    return 0;
}

Run the program and find the following results:

$ ./a.out
i1=1, i2=2
i1=2, i2=1

Although we assign 1 to i1 and 2 to i2 for both x.cpp and y.cpp.However, a result of i1 being 2 and i2 being 1 appears.

Objdump-S a.out found the following code for func1:

0000000000000712 <_ZN7MyClassIiE5func1Ev>:
template <typename T>
class MyClass
{
public:

    void func1()
 712:   55                      push   %rbp
 713:   48 89 e5                mov    %rsp,%rbp
 716:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
    {
        i1 = 1;
 71a:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 71e:   c7 40 04 01 00 00 00    movl   $0x1,0x4(%rax)
        i2 = 2;
 725:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 729:   c7 00 02 00 00 00       movl   $0x2,(%rax)
    }
 72f:   90                      nop
 730:   5d                      pop    %rbp
 731:   c3                      retq

You can see that there is really only one MyClass <int>code in the executable and that it is the one that uses x.cpp.Therefore, the output in x.cpp is correct, and the output in y.cpp is the opposite.

Change the link order, this time let the linker process y.o before x.o.Run the program again with the following results:

$ g++ y.o x.o main.o
$ ./a.out
i1=2, i2=1
i1=1, i2=2

This time the output in x.cpp is wrong, the output in y.cpp is right.

From the above examples, we can see that the linker does not distinguish whether the section contents are consistent when it is reduplicating the COMDAT section.Therefore, there is a potential risk that C++ templates with inconsistent memory layouts will be linked together in different object files.A static library (.a) file is actually a simple collection of object files plus a symbol table, so linking a static library (.a) file is the same as linking an object file.Dynamic libraries (.so) may work slightly differently.

So the remaining question is, will compiling std's data structures using different C++ standards generate a COMDAT section with the same name?

Take the vector<int>::push_back function as an example:

$ g++ -std=c++98 x.cpp -c
$ readelf -g x.o | grep push_back
COMDAT group section [    4] `.group' [_ZNSt6vectorIiSaIiEE9push_backERKi] contains 2 sections:
   [   64]   .text._ZNSt6vectorIiSaIiEE9push_backERKi
   [   65]   .rela.text._ZNSt6vectorIiSaIiEE9push_backERKi

$ g++ -std=c++11 x.cpp -c
$ readelf -g x.o | grep push_back
COMDAT group section [    5] `.group' [_ZNSt6vectorIiSaIiEE9push_backEOi] contains 2 sections:
   [   72]   .text._ZNSt6vectorIiSaIiEE9push_backEOi
   [   73]   .rela.text._ZNSt6vectorIiSaIiEE9push_backEOi

The COMDAT section generated for vector <int>:: push_back does have a different name.But before we have a thorough understanding of this naming rule (mangling), we can only say that vector <int>:: push_back is a good function and does not represent other situations.

 

There are still some unresolved issues, which are documented here:

(1) The impact of different C++ versions on mangling?Impact on COMDAT section generation?

(2) Does dynamic library (.so) also have this problem?How will it affect the different ways dynamic libraries are used?For example, will process runtime links differ from dlopen?

 

Reference material:

(1)https://forum.osdev.org/viewtopic.php?f=13&t=28618

(2)https://www.airs.com/blog/archives/52

Posted by phpmoron on Tue, 09 Jul 2019 10:45:37 -0700