强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 17 章:问题排查

第 17 章:问题排查

17.1 配置阶段错误

17.1.1 CMake 版本不匹配

错误信息

CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
  CMake 3.22 or higher is required. You are running version 3.16.3.

解决方案

# 检查版本
cmake --version

# 升级 CMake
pip install cmake --upgrade
# 或
sudo snap install cmake --classic

# 或降低项目要求
cmake_minimum_required(VERSION 3.16)

17.1.2 编译器未找到

错误信息

CMake Error: Could not find compiler set in environment variable CXX:
  g++-15

解决方案

# 检查编译器
which g++
g++ --version

# 设置正确的编译器
export CXX=g++
export CC=gcc

# 或在 CMake 中指定
cmake -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc

# 或使用工具链文件
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake

17.1.3 find_package 找不到库

错误信息

CMake Error: Could not find a package configuration file provided by "OpenSSL"

排查步骤

# 1. 检查库是否安装
dpkg -l | grep openssl
# 或
apt list --installed 2>/dev/null | grep openssl

# 2. 检查开发包是否安装
apt list --installed 2>/dev/null | grep libssl-dev

# 3. 设置搜索路径
cmake -DCMAKE_PREFIX_PATH=/usr/local/ssl
# 或
cmake -DOPENSSL_ROOT_DIR=/usr/local/ssl

# 4. 使用调试模式查看查找过程
cmake --debug-find-pkg=OpenSSL -S . -B build

# 5. 查看缓存变量
cmake -S . -B build -L | grep -i openssl

17.1.4 源文件未找到

错误信息

CMake Error: Cannot find source file:
  src/main.cpp

解决方案

# 检查路径是否相对于 CMakeLists.txt 所在目录
# 正确
add_executable(app ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)

# 或使用相对路径(相对于当前 CMakeLists.txt)
add_executable(app src/main.cpp)

# 检查文件是否存在
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
    message(FATAL_ERROR "src/main.cpp 不存在!")
endif()

17.1.5 生成器不支持

错误信息

CMake Error: Could not create named generator Ninja

解决方案

# 查看可用生成器
cmake --help | grep generators

# 安装 Ninja
apt install ninja-build

# 或使用 Make
cmake -G "Unix Makefiles"

17.2 构建阶段错误

17.2.1 编译错误

常见错误

fatal error: some_header.h: No such file or directory

解决方案

# 添加包含目录
target_include_directories(myapp PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_BINARY_DIR}  # 生成的头文件
)

# 使用 SYSTEM 避免警告
target_include_directories(mylib SYSTEM PUBLIC
    /usr/local/include
)

# 检查是否链接了正确的库
target_link_libraries(myapp PRIVATE correct_lib)

17.2.2 链接错误

错误信息

undefined reference to `some_function()'

排查步骤

# 1. 检查符号是否存在
nm -C libsome.a | grep some_function
# 或
readelf -s libsome.so | grep some_function

# 2. 检查链接顺序
# 静态库链接顺序很重要:被依赖者在后面
target_link_libraries(app PRIVATE mylib dep_lib)

# 3. 检查是否遗漏链接库
# 常见遗漏:pthread, m, dl, rt
target_link_libraries(app PRIVATE Threads::Threads)

# 4. 检查 C++ 名称修饰
# 确保 C 和 C++ 库正确混合使用
extern "C" {
    #include "c_header.h"
}

链接库顺序问题

# 错误:依赖者在前面
target_link_libraries(app PRIVATE base mylib)  # mylib 依赖 base

# 正确:被依赖者在后面
target_link_libraries(app PRIVATE mylib base)

# 或使用链接组解决循环依赖
target_link_libraries(app PRIVATE
    -Wl,--start-group
    libA libB
    -Wl,--end-group
)

17.2.3 未定义符号错误

错误信息

CMake Error: Target "mylib" has dependency on "OpenSSL::SSL"
  but target was not found.

解决方案

# 确保 find_package 在使用前调用
find_package(OpenSSL REQUIRED)
target_link_libraries(mylib PUBLIC OpenSSL::SSL)

# 或检查目标是否存在
if(TARGET OpenSSL::SSL)
    target_link_libraries(mylib PUBLIC OpenSSL::SSL)
else()
    message(WARNING "OpenSSL 未找到")
endif()

17.2.4 重复定义错误

错误信息

multiple definition of `some_function'

解决方案

# 检查是否重复链接同一库
target_link_libraries(app PRIVATE mylib)
target_link_libraries(app PRIVATE mylib)  # 重复!

# 检查头文件是否在多个源文件中定义了全局变量
# 使用 extern 声明
# header.h
extern int global_var;  # 声明
# source.cpp
int global_var = 42;    # 定义

17.3 运行时错误

17.3.1 共享库找不到

错误信息

error while loading shared libraries: libmylib.so: cannot open shared object file

解决方案

# 设置 RPATH
set_target_properties(myapp PROPERTIES
    INSTALL_RPATH "$ORIGIN/../lib"
    INSTALL_RPATH_USE_LINK_PATH TRUE
)

# 构建时设置 RPATH
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")

# 或禁用 RPATH(不推荐)
set(CMAKE_SKIP_RPATH TRUE)
set(CMAKE_SKIP_INSTALL_RPATH TRUE)
# 临时解决方案
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH

# 永久解决方案
echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/myapp.conf
sudo ldconfig

17.3.2 程序找不到资源文件

解决方案

# 定义数据目录
target_compile_definitions(myapp PRIVATE
    DATA_DIR="${CMAKE_INSTALL_PREFIX}/share/myapp"
)

# 或使用相对于可执行文件的路径
set_target_properties(myapp PROPERTIES
    INSTALL_RPATH "$ORIGIN"
)

# 在代码中获取可执行文件路径
# Linux: readlink /proc/self/exe
# macOS: _NSGetExecutablePath
# Windows: GetModuleFileName

17.4 变量调试

17.4.1 打印变量值

# 打印单个变量
message(STATUS "MY_VAR = ${MY_VAR}")

# 打印多个变量
message(STATUS "
  项目配置:
    PROJECT_NAME: ${PROJECT_NAME}
    PROJECT_VERSION: ${PROJECT_VERSION}
    CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}
    CMAKE_CXX_COMPILER: ${CMAKE_CXX_COMPILER}
    CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}
")

# 条件打印
if(VERBOSE_CONFIG)
    message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
    message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}")
endif()

17.4.2 列出所有变量

# 在 CMakeLists.txt 中
get_cmake_property(_vars VARIABLES)
foreach(_var ${_vars})
    message(STATUS "${_var} = ${${_var}}")
endforeach()

# 命令行方式
cmake -S . -B build -L       # 用户变量
cmake -S . -B build -LA      # 包含高级变量

17.4.3 条件断点

# CMake 没有断点,但可以模拟
function(debug_var var_name)
    if(DEBUG_CMAKE)
        message(STATUS "[DEBUG] ${var_name} = ${${var_name}}")
        if(${ARGC} GREATER 1)
            message(STATUS "[DEBUG] ${ARGV1}")
        endif()
    endif()
endfunction()

debug_var(CMAKE_BUILD_TYPE)
debug_var(PROJECT_VERSION "项目版本")

17.4.4 缓存变量问题

# 查看 CMakeCache.txt
cat build/CMakeCache.txt | grep -i my_var

# 删除缓存重新配置
rm -rf build
cmake -S . -B build

# 只删除特定变量
cmake -S . -B build -UMY_VAR

# 强制覆盖
cmake -S . -B build -DMY_VAR=new_value -DMY_VAR:STRING=new_value

17.5 生成器表达式调试

17.5.1 无法直接打印

# 不能在配置阶段打印生成器表达式
message(STATUS "$<CONFIG>")  # 输出:$<CONFIG>(字面量)

# 使用 file(GENERATE) 来调试
file(GENERATE
    OUTPUT "${CMAKE_BINARY_DIR}/debug_config.txt"
    CONTENT "Config: $<CONFIG>\nCompiler: $<CXX_COMPILER_ID>\n"
)

# 构建后查看
cat build/debug_config.txt

17.5.2 使用自定义目标调试

add_custom_target(debug_genex
    COMMAND ${CMAKE_COMMAND} -E echo "Config: $<CONFIG>"
    COMMAND ${CMAKE_COMMAND} -E echo "Compiler: $<CXX_COMPILER_ID>"
    COMMAND ${CMAKE_COMMAND} -E echo "Platform: $<PLATFORM_ID>"
    COMMENT "调试生成器表达式"
)

17.6 目标属性调试

17.6.1 检查目标属性

# 打印目标的所有属性
function(dump_target target)
    message(STATUS "=== 目标: ${target} ===")
    
    get_target_property(type ${target} TYPE)
    message(STATUS "  类型: ${type}")
    
    get_target_property(srcs ${target} SOURCES)
    message(STATUS "  源文件: ${srcs}")
    
    get_target_property(dirs ${target} INCLUDE_DIRECTORIES)
    message(STATUS "  包含目录: ${dirs}")
    
    get_target_property(libs ${target} LINK_LIBRARIES)
    message(STATUS "  链接库: ${libs}")
    
    get_target_property(defs ${target} COMPILE_DEFINITIONS)
    message(STATUS "  编译定义: ${defs}")
    
    get_target_property(opts ${target} COMPILE_OPTIONS)
    message(STATUS "  编译选项: ${opts}")
endfunction()

dump_target(myapp)

17.6.2 检查传递依赖

# 递归检查目标依赖
function(dump_target_deps target depth)
    math(EXPR next_depth "${depth} + 1")
    string(REPEAT "  " ${depth} indent)
    
    message(STATUS "${indent}目标: ${target}")
    
    get_target_property(libs ${target} LINK_LIBRARIES)
    if(libs)
        foreach(lib ${libs})
            if(TARGET ${lib})
                dump_target_deps(${lib} ${next_depth})
            endif()
        endforeach()
    endif()
endfunction()

dump_target_deps(myapp 0)

17.7 构建日志调试

17.7.1 详细构建输出

# CMake 配置时的详细输出
cmake -S . -B build --log-level=VERBOSE
cmake -S . -B build --log-level=DEBUG
cmake -S . -B build --log-level=TRACE

# 构建时的详细输出
cmake --build build --verbose
# 或
make VERBOSE=1
# 或
ninja -v

# CTest 详细输出
ctest --test-dir build -V
ctest --test-dir build -VV

17.7.2 编译命令数据库

# 生成 compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 使用 Ninja 生成器时自动产生
cmake -S . -B build -G Ninja
# build/compile_commands.json 包含所有编译命令
# 查看特定文件的编译命令
cat build/compile_commands.json | jq '.[] | select(.file | contains("main.cpp"))'

# 或使用 bear
bear -- make

17.8 常见陷阱和解决方案

17.8.1 变量作用域问题

# 问题:在子目录中设置的变量不影响父目录
# sub/CMakeLists.txt
set(MY_VAR "value")  # 仅在 sub/ 有效

# 解决:使用 PARENT_SCOPE
set(MY_VAR "value" PARENT_SCOPE)

# 或使用缓存变量
set(MY_VAR "value" CACHE STRING "描述")

17.8.2 include 与 add_subdirectory 区别

# add_subdirectory: 创建新作用域
add_subdirectory(sub)
# sub 中的变量不会影响当前作用域

# include: 在当前作用域执行
include(sub/CMakeLists.txt)
# sub 中的变量会影响当前作用域(注意!)

17.8.3 file(GLOB) 不自动更新

# 问题:添加新文件后需要重新运行 cmake
file(GLOB SOURCES "src/*.cpp")

# 解决方案一:手动列出文件(推荐)
set(SOURCES
    src/main.cpp
    src/utils.cpp
    src/network.cpp
)

# 解决方案二:使用 CONFIGURE_DEPENDS(CMake 3.12+)
file(GLOB SOURCES CONFIGURE_DEPENDS "src/*.cpp")

17.8.4 目标名称冲突

# 问题:多个目录使用相同的目标名
# dir1/CMakeLists.txt
add_library(utils src/utils.cpp)  # 冲突!

# 解决:使用命名空间或前缀
add_library(myproject_dir1_utils src/utils.cpp)
add_library(MyProject::dir1::utils ALIAS myproject_dir1_utils)

17.9 调试工具

17.9.1 CMake 内置调试

# 调试模式
cmake --trace-expand       # 展开所有变量
cmake --trace-source=file.cmake  # 追踪特定文件

# 性能分析
cmake --profiling-format=google-trace --profiling-output=trace.json
# 使用 chrome://tracing 查看

17.9.2 cmake_print_variables

include(CMakePrintHelpers)

cmake_print_variables(CMAKE_BUILD_TYPE PROJECT_VERSION)
cmake_print_target_properties(myapp)

17.9.3 try_compile

# 测试编译功能
try_compile(HAS_CXX17
    ${CMAKE_BINARY_DIR}/test
    SOURCES ${CMAKE_SOURCE_DIR}/cmake/test_cxx17.cpp
    CXX_STANDARD 17
)

if(HAS_CXX17)
    message(STATUS "编译器支持 C++17")
else()
    message(FATAL_ERROR "需要支持 C++17 的编译器")
endif()

17.10 业务场景

场景:系统化问题排查流程

问题出现
│
├── 1. 配置阶段错误
│   ├── 检查 CMake 版本
│   ├── 检查编译器
│   ├── 检查 find_package 结果
│   └── 查看 CMakeCache.txt
│
├── 2. 构建阶段错误
│   ├── 编译错误 → 检查包含目录、编译选项
│   ├── 链接错误 → 检查链接库、链接顺序
│   └── 使用 --verbose 查看完整命令
│
├── 3. 运行时错误
│   ├── 共享库找不到 → 检查 RPATH
│   ├── 资源文件找不到 → 检查路径
│   └── 使用 ldd 检查依赖
│
└── 4. 调试技巧
    ├── 清理重新构建:rm -rf build
    ├── 详细输出:--verbose
    ├── 变量检查:message()
    └── 目标检查:dump_target()

17.11 注意事项

问题常见原因解决方案
配置失败版本不匹配/依赖缺失升级 CMake/安装依赖
编译失败头文件路径/编译选项检查 target_include_directories
链接失败库未链接/顺序错误检查 target_link_libraries
运行时失败RPATH/动态库路径设置 INSTALL_RPATH
测试失败工作目录/环境变量设置 WORKING_DIRECTORY

17.12 扩展阅读


上一章:第 16 章 — Docker 与 CI/CD | 下一章:第 18 章 — 最佳实践 →