You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何调试器显示的函数指针值与std::println输出不一致?

为什么调试器显示的函数指针值与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)**导致的:

  1. 跳转桩的生成:对于通过extern "C"声明的外部可见函数(包括静态库中导出的函数),链接器(尤其是启用增量链接/INCREMENTAL或函数级链接/Gy时)会生成一个跳转桩。这个桩是一段极小的代码,唯一功能就是跳转到函数的实际实现地址。
  2. 指针与打印的差异:当你直接取fun_in_lib这类函数的指针时,得到的是跳转桩本身的地址,std::println会直接打印这个桩的地址。
  3. 调试器的智能解析:调试器会自动识别并解析跳转桩,显示桩指向的实际函数入口地址,所以调试器中看到的地址和从.map文件中获取的实际地址完全一致。

对于静态函数,因为它们不属于外部可见符号,链接器不会为其生成跳转桩,所以通过.map获取的地址和函数实际地址完全一致,这也符合你的测试结果。

你可以通过VS的反汇编窗口验证:跳转桩地址处的代码会是类似jmp 0x实际函数地址的指令,清晰展示桩和实际地址的跳转关系。


内容的提问来源于stack exchange,提问作者BowenDean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 19:04:52