分享一个在项目中遇到的关于cmake使用的bug, 我从源码角度来分享下遇到的问题,平时我们C++项目中大部分都是使用cmake,不过有些用法也是得过且过,编译没毛病就完事大吉,这次我遇到了一个bug,让我重新回去搞明白了一个cmake的用法
我们整体项目是由可执行文件,so库等组成,每个业务项目组提供so库,框架组提供可执行文件负责将整个程序串联起来。平常使用ci流水线整体编译整个项目,不过我们平常调试就是修改自己的库单独编译然后替换旧的同名库来查看问题。正是我们自己调试时发生的问题。
我模仿我们整体程序列出来了简单的测试程序,大致文件结构是这样
# tree
├── CMakeLists.txt
├── main.cpp
└── src_lib
├── CMakeLists.txt
├── interface1
│ ├── interface1.cpp
│ └── interface1.h
├── interface2
│ ├── interface2.cpp
│ └── interface2.h
├── interface_api.cpp
└── interface_api.h
可执行文件就是main.cpp,然后他去链接src_lib的这个库。我们看下最外层的CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(target)
set(CMAKE_CXX_STANDARD 14)
add_executable(target main.cpp)
add_subdirectory(src_lib)
target_link_libraries(target PUBLIC
src_lib
)
是不是极其简单,就是使用main.cpp,然后去链接src_lib库。
然后再看下src_lib的CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(src_lib)
set(CMAKE_CXX_STANDARD 14)
add_library(src_lib SHARED interface_api.cpp)
target_include_directories(src_lib PUBLIC ./)
option(ENABLE_INTERFACE1 "" ON)
if (ENABLE_INTERFACE1 STREQUAL "ON")
target_sources(src_lib PUBLIC
interface1/interface1.cpp
)
target_compile_definitions(src_lib PUBLIC ENABLE_INTERFACE1)
else()
target_sources(src_lib PUBLIC
interface2/interface2.cpp
)
target_compile_definitions(src_lib PUBLIC ENABLE_INTERFACE2)
endif()
可以看到src_lib是一个动态库,会将interface_api.cpp编译进来,然后我们程序中可能会根据不同开关来指定要编译不同的文件,这里如果ENABLE_INTERFACE1开关开就去编译interface1.cpp,否则就去编译interface2.cpp。
另外interface_api.cpp这个文件就是很简单的去调用interface1或者interface2:
// interface_api.cpp
#include "interface_api.h"
#ifdef ENABLE_INTERFACE1
#include "interface1/interface1.h"
#else
#include "interface2/interface2.h"
#endif
void createInterface() {
#ifdef ENABLE_INTERFACE1
interface1 interface;
#else
interface2 interface;
#endif
}
可以看到如果ENABLE_INTERFACE1那么就创建interface1对象,否则创建interface2对象。我分别在interface1和interface2构造函数中打印了一下。
// interface1.cpp
interface1::interface1() {
std::cout << "interface1" << std::endl;
}
// interface2.cpp
interface2::interface2() {
std::cout << "interface2" << std::endl;
}
在main函数中就是调用了一下src_lib的函数:
#include
#include "src_lib/interface_api.h"
int main() {
createInterface();
return 0;
}
代码比较简单,就是main函数调用库中的一个函数,而这个库会根据不同开关编译不同的文件。我们看下输出:
# cmake -DENABLE_INTERFACE1=ON ..
# make -j8
# ./target
interface1
# cmake -DENABLE_INTERFACE1=OFF ..
# make -j8
# ./target
interface2
这时在开这个interface2的情况下,我打算改一下interface2中的代码:
// interface2.cpp
interface2::interface2() {
std::cout << "interface2 yoyo" << std::endl;
}
但是这时我只是编译src_lib这个库,替换之前的src_lib库:
# make src_lib
# ./target
interface2
我改了代码,重编编库,然后替换,输出却还是之前记录。这如果在线上环境,也够排查半天了,首先可以编过,问题不明显,可能一下子都看不出来是改的代码有问题,还是没有执行修改的代码。
以下也是我的排查流程:
# ldd target
linux-vdso.so.1 (0x00007ffede9a0000)
libsrc_lib.so => /root/target/build/src_lib/libsrc_lib.so (0x00007fb626fe9000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fb626c54000)
libm.so.6 => /lib64/libm.so.6 (0x00007fb6268d2000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb6266ba000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb6262f5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb6271eb000)
可以发现确实是链接最新的库,有的机器或者板子没有ldd命令,可以使用cat /proc/[pid]/maps
查看该可执行链接的所有的库。
# readelf -p .rodata src_lib/libsrc_lib.so | grep interface
[ 1] interface2 yoyo
我们使用readelf
命令来查看libsrc_lib.so中的内容,-p的意思是string输出,.rodata表示输出只读段,关于程序在内存中布局这里不多讲,elf文件和内存段相关可以参考我之前的文章,简单来说就是程序中字面量(这里输出的字符串)会放到只读数据段。那么我们去这个段中去查询就可以了,这里可以看到输出也是最新的打印。我们去看下可执行文件中会输出什么:
# readelf -p .rodata target | grep interface
[ 12] interface2
target_sources(src_lib PRIVATE
interface1/interface1.cpp
)
就可以了,原来PUBLIC表示依赖这个project的库或者可执行文件也会将interface1.cpp编译进来。
# cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DENABLE_INTERFACE1=ON ..
# make -j8
...
[100%] Linking CXX executable target
/usr/bin/cmake -E cmake_link_script CMakeFiles/target.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/target.dir/main.cpp.o
CMakeFiles/target.dir/src_lib/interface1/interface1.cpp.o
-o target -Wl,-rpath,/root/target/build/src_lib src_lib/libsrc_lib.so
...
可以看到在链接target时会把interface1.cpp.o 这个文件也链接进来,这就导致了我们遇到的问题,一个工程中有多个代码的副本。
关于对于刚刚cmake的讲解不是很详细,这里再来讲述下:
target_sources(
[items1...]
[ [items2...] ...])
中的INTERFACE,PUBLIC,PRIVATE关键字表示的一个范围,target_include_directories,target_compile_definitions等都有同样的概念。
PUBLIC:
当将源文件与目标关联时,PUBLIC关键字会将这些源文件的属性公开给所有依赖于该目标的其他目标。
这意味着其他目标可以访问这些源文件以及与它们相关的编译器选项和编译定义。
PUBLIC属性会传递给依赖于当前目标的目标。
PRIVATE:
PRIVATE关键字用于指定源文件的属性仅在当前目标内部可见,不会传递给依赖于当前目标的其他目标。
这意味着其他目标无法访问与这些源文件相关的属性,这些源文件对当前目标的编译是私有的。
INTERFACE:
INTERFACE关键字用于指定源文件的属性应该在当前目标和依赖于它的其他目标之间共享,
但不在当前目标内部可见。
这允许将属性共享给依赖项,但不会影响当前目标本身。
INTERFACE属性不会传递给当前目标。
这是chatgpt的解释,我再补一句,PUBLIC就是依赖该目标会看到他所定义的属性,PRIVATE则不会,INTERFACE确是就是给依赖自己的目标看到定义的属性,自己看不到。以target_compile_definitions为例,如果是PUBLIC,源项和被依赖的target都会知道定义来哪些宏,PRIVATE则是只有被依赖项知道,INTERFACE则是只有源项知道定义了哪些宏。
本文展示了一下工作中遇到的bug以及bug整体的排查流程,和一些相关的手段,也是比较常用的,大家也可以记录到自己的小本本上。