Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linking LLVM causes gcov to fail

Passing --coverage to gcc while also linking LLVM causes an undefined reference to `__gcov_exit' error from the linker. I've set up a fresh project to try to isolate this problem. You can view the source on github and inspect the compiler output on Travis-CI.

This is the difference between a coverage and a non-coverage build

-DCMAKE_CXX_FLAGS="--coverage"

This is the difference between an LLVM and a non-LLVM build

target_link_libraries(Test
        PUBLIC
        LLVMCore
)

The LLVM job succeeds. The Coverage job succeeds. The LLVM + Coverage job fails with this error

undefined reference to `__gcov_exit'
like image 497
Indiana Kernick Avatar asked Dec 05 '18 06:12

Indiana Kernick


1 Answers

Notes:

  • Check [cmake 3.13]: CMAKE_<LANG>_FLAGS for details regarding which env var affects which cmake var - as you figured out in your (deleted) answer
  • Only noticed cxx_std_17 from cmake 3.8, so you might want to update your cmake_minimum_required
  • Forked your repo to [GitHub]: CristiFati/gcov_error - An attempt to reproduce the gcov/llvm linker error and did some debugging on it - beware, I might delete it someday
  • Was able to get my head around Travis CI, it's a real gold-mine

At the beginning I thought that it would be a trivial fix (something related to -fprofile-arcs, -ftest-coverage, -lgcov flags) as noted in [man7]: GCC(1) (--coverage option), but it wasn't.
I wasn't able to reproduce the problem on my Ubtu 16 x64 VM (although travis is very nice, it's kind of slow for debugging purposes (especially when due to rushing one might forget or add an extra character when editing :) ) and also it doesn't offer the same access level that a local machine does,) because the environment:

  • cmake 3.5.1
  • gcc 5.4.0
  • llvm 3.8.0

is pretty far away from what's on the travis docker image. I must mention that neither I tried building the packages from sources (that would probably have caused lots of headaches), nor did I try downloading them from the repositories where they are downloaded during CI builds (didn't even check if those repos are public). Anyway the VM was pretty much unusable because:

  • I couldn't reproduce the issue (after some minor changes to adapt to older tools,) everything worked
  • More: I ran into [SO]: Error when using CMake with LLVM - (@CristiFati's answer)

It was eating me alive, so I ended up building 48 freaking times (on travis) in order to get it working, and that is only because I failed to notice the obvious.

The problem

For each of the 3 configurations I'm going to paste the compile and link (g++) commands generated (from your build: [Travis CI]: Kerndog73 / gcov_error - Build #24)

  1. LLVM:

    /usr/bin/g++-7  -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/usr/lib/llvm-7/include  -std=gnu++1z -o CMakeFiles/Test.dir/main.cpp.o -c /home/travis/build/Kerndog73/gcov_error/main.cpp
    /usr/bin/g++-7    -rdynamic CMakeFiles/Test.dir/main.cpp.o  -o Test  -L/usr/lib/gcc/x86_64-linux-gnu/4.8 /usr/lib/llvm-7/lib/libLLVMCore.a /usr/lib/llvm-7/lib/libLLVMBinaryFormat.a /usr/lib/llvm-7/lib/libLLVMSupport.a -lz -lrt -ldl -ltinfo -lpthread -lm /usr/lib/llvm-7/lib/libLLVMDemangle.a
    
  2. Coverage:

    /usr/bin/g++-7  -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/usr/lib/llvm-7/include  --coverage   -std=gnu++1z -o CMakeFiles/Test.dir/main.cpp.o -c /home/travis/build/Kerndog73/gcov_error/main.cpp
    /usr/bin/g++-7  --coverage  -rdynamic CMakeFiles/Test.dir/main.cpp.o  -o Test
    
  3. LLVM + Coverage:

    /usr/bin/g++-7  -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/usr/lib/llvm-7/include  --coverage   -std=gnu++1z -o CMakeFiles/Test.dir/main.cpp.o -c /home/travis/build/Kerndog73/gcov_error/main.cpp
    /usr/bin/g++-7  --coverage  -rdynamic CMakeFiles/Test.dir/main.cpp.o  -o Test  -L/usr/lib/gcc/x86_64-linux-gnu/4.8 /usr/lib/llvm-7/lib/libLLVMCore.a /usr/lib/llvm-7/lib/libLLVMBinaryFormat.a /usr/lib/llvm-7/lib/libLLVMSupport.a -lz -lrt -ldl -ltinfo -lpthread -lm /usr/lib/llvm-7/lib/libLLVMDemangle.a
    

After lots of ghosts chasing, I noticed -L/usr/lib/gcc/x86_64-linux-gnu/4.8 being passed to the linker when llvm is included. gcc 4.8 is installed on the docker, so apparently gcc 7.4 was used for compilation, and gcc 4.8 for linking, which is Undefined Behavior.
I'm also pasting the collect command (closer to the linker) from a slight variation (with -v) of #3. (all the libraries specified with -l* and without the full path (e.g. -lgcov, -lgcc_s, -lgcc) have the wrong version because will be picked up from the wrong directory):

/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/ccyDh97q.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -export-dynamic -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o Test /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.8 -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. CMakeFiles/Test.dir/main.cpp.o /usr/lib/llvm-7/lib/libLLVMCore.a /usr/lib/llvm-7/lib/libLLVMBinaryFormat.a /usr/lib/llvm-7/lib/libLLVMSupport.a -lz -lrt -ldl -ltinfo -lpthread /usr/lib/llvm-7/lib/libLLVMDemangle.a -lstdc++ -lm -lgcov -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.0

Some test commands placed in .travis.yml's after_script section, yielded the following output:

$ _GCOV_LIB7=/usr/lib/gcc/x86_64-linux-gnu/7/libgcov.a
$ (nm -S ${_GCOV_LIB7} 2>/dev/null | grep __gcov_exit) || echo ${_GCOV_LIB7}
0000000000001e40 000000000000008b T __gcov_exit
$ _GCOV_LIB48=/usr/lib/gcc/x86_64-linux-gnu/4.8/libgcov.a
$ (nm -S ${_GCOV_LIB48} 2>/dev/null | grep __gcov_exit) || echo ${_GCOV_LIB48}
/usr/lib/gcc/x86_64-linux-gnu/4.8/libgcov.a

So, libgcc (libgcov.a) was modified somewhere between the 2 involved versions (__gcov_exit symbol was added), and g++ knew it has one but ld didn't provide it (because of the wrong lib) hence the error.

Now, why does llvm add this library search path, I don't know (probably because it was built with gcc 4.8 - or smth close to it), but here's what I can think of:

  1. llvm is not meant to be used with gcc 7 (although I didn't find any such restriction while quickly browsing [LLVM]: Getting Started with the LLVM System)
  2. A bug in llvm (considering the other issue that I ran into, I tend to think this is the winner)

I found ways for both of them:

  1. Use an older g++ (4.8 - installed by default on the docker)

    • export CXX=g++ (might even not be necessary)
    • g++ (not cmake this time) doesn't know about cxx_std_17, so the easiest way is to remove the afferent target_compile_features (the drawback is that code won't be C++17 "compliant")
  2. Since I don't know how to "undo" passing the (faulty) library search path, the workaround that I came up with is to specify the correct one before it.

    CMakeLists.txt:

    cmake_minimum_required(VERSION 3.2)
    project(gcov_test)
    
    find_package(LLVM 7.0.0 REQUIRED CONFIG)
    message(STATUS "Found LLVM: ${LLVM_DIR} ${LLVM_PACKAGE_VERSION}")
    
    add_executable(Test
        "main.cpp"
    )
    
    target_compile_features(Test
        PUBLIC cxx_std_17
    )
    
    target_include_directories(Test
        PUBLIC ${LLVM_INCLUDE_DIRS}
    )
    
    target_compile_definitions(Test
        PUBLIC ${LLVM_DEFINITIONS}
    )
    
    if(LINK_WITH_LLVM)
        # @TODO - cfati: Everything in this block is to avoid hardcoding default libgcc path (/usr/lib/gcc/x86_64-linux-gnu/7)
        set(_COLLECT_LTO_WRAPPER_TEXT "COLLECT_LTO_WRAPPER=")
        execute_process(
            COMMAND bash -c "$0 -v 2>&1 | grep ${_COLLECT_LTO_WRAPPER_TEXT} | sed s/${_COLLECT_LTO_WRAPPER_TEXT}//" "${CMAKE_CXX_COMPILER}"
            OUTPUT_VARIABLE _GAINARIE_COLLECT_TMP_VAR
        )
        get_filename_component(_GAINARIE_GCC_DEFAULT_PATH ${_GAINARIE_COLLECT_TMP_VAR} DIRECTORY)
        set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${_GAINARIE_GCC_DEFAULT_PATH}")
        #set (GCCOPT "${GCCOPT} -L${_GAINARIE_GCC_DEFAULT_PATH}")
        # @TODO END
        target_link_libraries(Test
            PUBLIC LLVMCore
        )
    endif()
    

    Note: I tried to come up with something more elegant (I'm sure that it is), but I couldn't (tried using CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES, target_link_libraries, link_directories and a few more) at this point.

like image 105
CristiFati Avatar answered Sep 25 '22 15:09

CristiFati