为何调试器显示的函数指针值与std::println输出不一致?
最小示例
#include <print> void fun() {} int main() { std::println("{}", (void *)fun); }
CMake配置
cmake_minimum_required(VERSION 3.15) project(test) set(CMAKE_CXX_STANDARD 23) add_executable(test main.cpp)
运行结果
- 调试器监视结果:
0x00007ff755be54e0 {test.exe!fun(void)} - std::println输出结果:
0x7ff755be158c
环境
使用VS2022 Community 17.12.4作为VSCode中CMake工具链的amd64平台编译环境,调试器为VSCode cpptools插件集成调试器。
含静态库与extern "C"的示例
注意:未提供库的头文件,此为预期设置。
main.cpp代码
// main.cpp #include <Windows.h> #include <cstdint> #include <fstream> #include <print> #include <sstream> extern "C" char fun_in_lib(int i); // extern "C" char static_fun_in_lib(int i); // 调用会导致链接错误 uintptr_t getSymbolAddressFromMap(const char *symbolName) { // 修改为test.map文件的实际路径,需与可执行文件同目录 const char *mapPath = "d:\\workspace\\print-pointer\\build\\debug\\test.map"; std::ifstream file(mapPath); if (!file.is_open()) { return NULL; } std::string line; while (std::getline(file, line)) { if (line.find(symbolName) != std::string::npos) { std::istringstream iss(line); std::string segOffset, name, rvaBase; if (iss >> segOffset >> name >> rvaBase) { if (name == symbolName) { unsigned long long addr = 0; std::istringstream addrIss(rvaBase); addrIss >> std::hex >> addr; auto base = GetModuleHandle(NULL); uintptr_t image_base = 0x140000000; addr = addr - image_base + (uintptr_t)base; return (uintptr_t)addr; } } } } return NULL; } int main() { using FunInLibType = char (*)(int); auto addr_of_fun_in_lib = (FunInLibType)getSymbolAddressFromMap("fun_in_lib"); auto addr_of_static_fun_in_lib = (FunInLibType)getSymbolAddressFromMap("static_fun_in_lib"); std::println("{}", (void *)fun_in_lib); std::println("{}", (void *)addr_of_fun_in_lib); std::println("{}", (void *)addr_of_static_fun_in_lib); // 验证可调用性 std::println("{}", fun_in_lib((int)'A' + 256)); std::println("{}", addr_of_fun_in_lib((int)'A' + 256)); std::println("{}", addr_of_static_fun_in_lib((int)'A' + 256)); // std::println("{}", static_fun_in_lib((int)'A' + 256)); // 调用会导致链接错误 }
lib.c代码
// lib.c static char static_fun_in_lib(int i) { return i % 256; } char fun_in_lib(int i) { // 防止static_fun_in_lib被内联 return static_fun_in_lib(i); }
CMake配置
cmake_minimum_required(VERSION 3.15) project(test) set(CMAKE_CXX_STANDARD 23) add_library(lib STATIC lib.c) add_executable(test main.cpp) target_link_options(test PRIVATE /MAP) target_link_libraries(test PRIVATE lib)
运行结果
- 调试器监视结果:
// copied from watch fun_in_lib: 0x00007ff73a8fd3b0 {test.exe!fun_in_lib(int)} addr_of_fun_in_lib: 0x00007ff73a8fd3b0 {test.exe!fun_in_lib(int)} addr_of_static_fun_in_lib: 0x00007ff73a8fd3d0 {test.exe!static_fun_in_lib(int)} - std::println输出结果:
0x7ff73a8c25db 0x7ff73a8fd3b0 0x7ff73a8fd3d0 A A A
原始场景
我正在分析一个纯C编写的静态库,将其链接到测试C++文件并调用其中的函数。该库缺少大量函数声明,但我可以通过extern "C" void fun();自行声明来调用函数。借助IDA我获取了内部函数名和参数,只需用兼容签名加extern "C"声明即可调用。
但发现部分函数是原目标文件中的static函数,无法通过此方法调用,因为它们不是外部符号,声明extern "C" void some_static_function();会导致链接时出现未解析符号错误。
于是我通过添加/MAP链接选项生成包含所有函数(包括各目标文件中的static函数)名称和地址的.map文件,在代码中搜索获取地址并偏移得到可调用的运行时地址,代码示例如下:
uintptr_t getSymbol(const char *symbolName){ std::ifstream file(MAP_FILE_PATH); uintptr_t addr = /* 省略符号地址搜索逻辑 */; addr = addr - 0x140000000 + (uintptr_t)GetModuleHandle(NULL); // 偏移计算 return addr; } using SomeFun = char(*)(void *, void *); SomeFun some_fun = (SomeFun)getSymbol("some_static_fun"); some_fun(NULL, NULL);
此方法可行,我成功获取并调用了内部static函数。
随后我尝试对所有函数(无论是否static)使用此方法,例如:
extern "C"{ char some_fun_in_lib_without_decl(void *, void *); // 可直接调用,运行正常 } using F = char(*)(void *, void *); F some_fun_alias = (F)getSymbol("some_fun_in_lib_without_decl"); // 同样可行 std::println("{:p} {:p}", (void *)some_fun_in_lib_without_decl, (void *)some_fun_alias); // 此处出现异常
异常现象:编辑器中悬停在extern "C"声明的函数名上,调试器显示的地址与监视窗口一致,但终端中std::println输出的地址却与调试器显示的不同。尝试取地址&some_fun_in_lib_without_decl输出,结果仍一致。
解答
这种差异是Windows平台(VS编译环境)特有的**函数跳转桩(Jump Stub)**导致的:
- 跳转桩的生成:对于通过
extern "C"声明的外部可见函数(包括静态库中导出的函数),链接器(尤其是启用增量链接/INCREMENTAL或函数级链接/Gy时)会生成一个跳转桩。这个桩是一段极小的代码,唯一功能就是跳转到函数的实际实现地址。 - 指针与打印的差异:当你直接取
fun_in_lib这类函数的指针时,得到的是跳转桩本身的地址,std::println会直接打印这个桩的地址。 - 调试器的智能解析:调试器会自动识别并解析跳转桩,显示桩指向的实际函数入口地址,所以调试器中看到的地址和从.map文件中获取的实际地址完全一致。
对于静态函数,因为它们不属于外部可见符号,链接器不会为其生成跳转桩,所以通过.map获取的地址和函数实际地址完全一致,这也符合你的测试结果。
你可以通过VS的反汇编窗口验证:跳转桩地址处的代码会是类似jmp 0x实际函数地址的指令,清晰展示桩和实际地址的跳转关系。
内容的提问来源于stack exchange,提问作者BowenDean

