C++ 源码保护终极玩法:把实现 “锁” 进库里,只露接口给别人用;以及发现库重名,CPU该如何“抉择呢”

张开发
2026/4/8 14:44:15 15 分钟阅读

分享文章

C++ 源码保护终极玩法:把实现 “锁” 进库里,只露接口给别人用;以及发现库重名,CPU该如何“抉择呢”
你是否遇到过这些痛点写完核心算法 / 底层驱动不想暴露源码逻辑交付给别人时怕代码被抄、被篡改项目模块化后头文件乱糟糟内部实现和外部接口混为一谈其实 C 早就给了一套优雅的解决方案 ——编译为静态 / 动态库 接口与实现分离把核心逻辑彻底藏进二进制库对外只暴露干净的调用接口。既实现了源码隐藏又能做到模块化解耦安全又优雅。一、核心思路接口与实现彻底分家核心原则非常简单头文件只声明源文件只实现。对外提供.h头文件只写类、函数、宏的声明不写任何业务逻辑别人能看、能调用但看不到具体实现。内部实现写.cpp文件编译时直接打包成.a静态库或.so动态库纯二进制格式反编译难度极高核心逻辑完全隐藏。外部使用者只需要头文件 库文件即可正常调用功能完全无需源码二、具体实现以及代码结构1.目录结构mysdk/ ├── include/ # 对外头文件给用户 │ └── mysdk.h ├── src/ # 内部实现隐藏 │ └── mysdk.cpp ├── lib/ # 编译生成库 │ └── libmysdk.so └── demo/ # 用户示例 └── main.cpptips CMakeLists.txtRUNTIME的加入到bin/library加入到lib/demo生成可执行文件2.对应代码1)mysdk.h#ifndef __MY_SDK_H__ #define __MY_SDK_H__ #pragma once #ifdef __cplusplus extern C { #endif // 初始化SDK int sdk_init(); // 计算加法 int sdk_add(int a, int b); // 释放资源 void sdk_deinit(); #ifdef __cplusplus } #endif #endif2)mysdk.cpp#include iostream #include mysdk.h // 内部私有变量用户不可见 static int g_initialized 0; int sdk_init() { std::cout [SDK] init\n; g_initialized 1; return 0; } int sdk_add(int a, int b) { if (!g_initialized) { std::cout [SDK] not initialized\n; return -1; } int result a b; std::cout [SDK] add: result std::endl; return result; } void sdk_deinit() { std::cout [SDK] deinit\n; g_initialized 0; }3)CMakeLists.txtcmake_minimum_required(VERSION 3.15) project(my_sdk_demo LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin) add_library(mysdk STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/mysdk.cpp) target_include_directories(mysdk PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) add_executable(mysdk_demo ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) target_link_libraries(mysdk_demo PRIVATE mysdk) target_include_directories(mysdk_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)3.小结add_library(mysdk STATIC${CMAKE_CURRENT_SOURCE_DIR}/src/mysdk.cpp)可以设置STATICSHARED关键字将其设置为动态库.so或者静态库.a的形式当然静态库和动态库的这个案例是个无论社招还是校招都会考的点大家多多掌握。三、有两个动态库重名会发生什么在我年少轻狂的时候我就栽到了这道题是导致我与大厂失之交臂的一个原因。先说结论根据头文件是否包含inline有关加了则会在编译阶段报错如果没有加那运行报错与否就随天意了。1.场景简介主程序运行过程中动态链接库.so / .dll存在两个头文件里面都有重名函数没有 namespace主程序调用这个函数问题主程序到底会调用哪个函数会出错吗2、C 链接机制回顾1编译阶段.o 文件生成当你写#include a.h #include b.h int main() { foo(); }编译器会根据当前包含的头文件生成符号name mangling情况1头文件只 声明// a.hvoid foo();// b.hvoid foo();只声明不定义编译阶段 不会报错编译器只知道有一个 foo 符号存在情况 2头文件里 inline / 定义// a.hvoid foo() { /*...*/ }// b.hvoid foo() { /*...*/ }编译器会报错error: redefinition of foo因为同一个 翻译单元TU, .cpp文件 中出现了两个同名函数的定义。2链接阶段.so / .a / .dll如果两个动态库都有 foo() 实现主程序链接到其中一个ld.so / dynamic linker 会根据 load order 决定ELF / Linux 下默认符号解析规则全局符号首先查找主程序本身的符号然后查找按加载顺序的动态库找到第一个匹配的符号就用如果有多个定义但 load 顺序不确定可能运行时调用错函数3.可能出现的问题情况1头文件没有 namespace会导致 符号冲突编译器不会因为头文件名不同自动区分函数2动态库加载假设liba.so - foo()libb.so - foo()main.exe 调用 foo()谁先被 dlopen() 或链接器加载谁的符号会被用默认全局符号如果没有显式限定库或作用域可能调用到“错误”的 foo”不会编译报错但会出现运行时逻辑错误3有可能的异常如果两个库 foo 内部实现冲突例如不同参数或不同行为调用可能导致崩溃undefined behavior调用了错误实现数据损坏Linux ELF 动态链接器对全局符号第一个匹配原则Windows DLL 也类似4.如何避免1.使用 namespace2.避免全局符号污染3.动态加载时使用 dlopen dlsym 显式选择符号void* handle dlopen(liba.so, RTLD_NOW);auto foo_a (void(*)()) dlsym(handle, foo);handle dlopen(libb.so, RTLD_NOW);auto foo_b (void(*)()) dlsym(handle, foo);

更多文章