CRT 不同会导致 fopen 地址不同

我引用了一个开源库 libjpeg。一个工程运行成功,一个使用google test测试崩溃

找了一天的原因。

分析

感谢你提供的完整 example.c 内容。因为两个项目都使用同样的 example.c,所以:


🔍 崩溃分析聚焦点:fopen + jpeg_stdio_src + jpeg_read_header

从这段代码可以确认:

c 复制代码
FILE* infile = fopen(infilename, "rb");
jpeg_stdio_src(cinfo, infile);
jpeg_read_header(cinfo, TRUE); // ← 崩溃点

那么如果 example.c 是一样的,问题就出在两个项目的

✳️ FILE* 是不是在两个项目中指向了不同的 CRT 堆(heap)结构体


✅ 原因回顾:FILE* 来自不同运行库(CRT)

FILE* 是一个内部结构体,不同的 CRT(msvcrt.dll vs msvcrtd.dll)维护各自独立的 FILE* 表。如果你用 A 的 fopen() 打开文件,却传给 B 的 fread(),就会崩。


✅ 排查路径建议

我们现在知道 example.c 是一样的,核心思路是:

✅ 1. 打印 fopen 的地址(CRT 来源)

添加如下调试代码来对比两个工程中 fopen 的实际地址:

c 复制代码
printf("fopen: %p\n", fopen);
printf("infile: %p\n", infile);
理想情况(不会崩的工程):
复制代码
fopen: 0x7ffb12345678
infile: 0x000002345678abcd
崩溃工程:

如果 fopen 地址不同(来自另一个 DLL),那么 FILE* 的结构不兼容,后续 fread() 调用会直接崩溃在 fill_input_buffer()


✅ 2. 在崩溃工程中确认 CRT

打开调试器,在 jpeg_read_header() 崩溃前:

查变量:
  • cinfo->src->infile 是否等于 infile
  • infile 的值是否合理(不是 0xCDCDCDCDnullptr
  • 加断点观察 fill_input_buffer()fread() → 崩溃地址在哪个 DLL

✅ 3. 检查静态链接库(gtest)是否引入 CRT 混用

错误工程中有这两个链接库:

text 复制代码
gtestd.lib
gtest_maind.lib

它们可能是用 静态 CRT (/MTd) 编译的。而你的项目是 动态 Debug CRT (/MDd)

📌 *静态 CRT 会引入独立 FILE 表!**

这就导致:

  • gtestd.lib 中的 fopen()libjpeg 使用的 fread()
  • FILE* 指针结构不兼容,最终 jpeg_read_header() 崩溃

✅ 解决方案

方案 描述
方案 1(推荐) 暂时移除 gtestd.lib / gtest_maind.lib,重新运行,验证是否崩溃
✅ 方案 2 确认 GoogleTest 是否使用 /MDd 编译。你可以自己用 CMake 重新编译一份
✅ 方案 3 你可以把 fopen() 包装起来,打印 CRT 来源的 DLL(例如用 GetModuleHandleEx

🧠 进阶技巧:查看 fopen 来源 DLL

可以在调试器中,运行:

复制代码
x fopen

然后 fopen 指向哪个模块就一目了然:

  • 如果来自 msvcrtd.dll → 正确
  • 如果来自 gtestd.libucrtbase.dll → 不同 CRT,错误!

✅ 总结

项目 说明
example.c 相同 ✔️ 没问题
项目设置都是 /MDd ✔️ 没问题
崩溃点是 jpeg_read_header() 内部 fread() ❗ 问题核心
极可能原因 静态链接的 gtest 使用了不同 CRT,导致 FILE 崩溃*
验证方法 打印 fopen 地址、临时移除 gtest.lib、确认 CRT 模块

如你愿意,我可以:

  • 帮你生成一个适合 /MDd 的 GoogleTest .lib 文件构建脚本;
  • 给你一个能直接在程序中输出 CRT DLL 名称的小工具函数(基于 VirtualQuery + GetMappedFileName);

我的打印结果如下

fopen infile
正确的工程 fopen: 00007FFB762CA6F0 infile: 000001CAFCB0FC60
错误的工程 fopen: 00007FFB764BA6F0 infile: 000001E3122819F0

结论

你问得非常好,我们来深入解释为什么两个 fopen 地址不同是 CRT 冲突的根本证据 。这不是偶然,而是Windows 上运行时系统结构 + Visual Studio 链接机制的必然结果。


✅ 背景知识:CRT(C Runtime Library)是什么?

CRT 是 C/C++ 提供的运行时库,包含了 fopenmallocprintf 等函数的底层实现。

Visual Studio 提供了多个 CRT:

类型 编译选项 目标 DLL
动态 Debug CRT /MDd msvcrtd.dll + ucrtbased.dll
动态 Release CRT /MD msvcrt.dll + ucrtbase.dll
静态 Debug CRT /MTd 编译进 .exe,不会依赖外部 DLL
静态 Release CRT /MT 同上

📌 为什么 CRT 不同会导致 fopen 地址不同?

📍 原因:每个 CRT 是独立的一套代码库

  • 当你链接不同的 CRT(比如 /MTd/MDd)时:

    • Visual Studio 会将多个不同版本的 CRT 链接进来
    • 每个 CRT 都有自己的 fopen()fread() 实现
    • 它们会分布在不同的 DLL 或 .lib 静态段

🔍 你看到的情况:

工程 fopen 地址 含义
正确工程 00007FFB762CA6F0 可能是来自 msvcrtd.dll(/MDd)
错误工程 00007FFB764BA6F0 来自另一个 CRT,比如 libcmt.lib(/MTd),或 GoogleTest 链接进来的静态 CRT

两个 fopen() 函数地址不同,说明链接进来的不是同一个 CRT,FILE* 的结构体也不一样!


📉 为什么这会导致崩溃?

🤯 你用的 fopen() 得到一个 FILE* 指针:

c 复制代码
FILE* f = fopen("image.jpg", "rb"); // 来自 CRT A(如 msvcrtd.dll)

但后面的 fread()(比如 jpeg_read_header() 中内部使用)来自 CRT B(如 libcmt.libucrtbase.dll)。

问题就在于:
  • FILE* 是一个指向 CRT 内部的结构体指针
  • 不同 CRT 的内部结构体大小、布局不一样
  • 一个 CRT 的 fopen() 分配的 FILE*,另一个 CRT 的 fread() 解读不了这个结构
  • 所以它会读错指针,最终导致崩溃

比如:

复制代码
FILE* → CRT A 内部的 结构体 FILE {
   magic_number
   buffer
   read_pos
   ...
}

但 CRT B 看到的是另一种 FILE 结构,读取 offset 时读取到了无效地址 → 崩!

🧪 举个真实例子

场景:

  • 你用了 gtestd.lib(静态库,编译时用的是 /MTd,静态 CRT)
  • 主程序用了 /MDd,用的是 msvcrtd.dll

链接结果:

  • 两个 CRT 并存在内存中
  • fopen 来自 libcmt.lib 的静态实现(通过 gtest 链接进来)
  • jpeg_read_header 使用的 fread() 来自 msvcrtd.dll
  • FILE* 结构无法共享 → 崩溃

✅ 怎么验证?

你已经验证了:两个项目中 fopen 地址不同 → 明确是两个不同的 CRT

你也可以在调试器中使用:

cmd 复制代码
x fopen

或在 Visual Studio 中查看:

  • 调试时点击"模块"窗口(Debug > Windows > Modules)

  • 查看加载了哪些 CRT DLL,如:

    • ucrtbased.dll → /MDd
    • libcmt.lib → /MTd
    • msvcrtd.dll → /MDd

一旦出现多个 CRT,即可确认冲突。


✅ 总结

结论 说明
fopen 地址不同 来自两个不同的 CRT,说明链接了多个版本的 C Runtime
FILE* 崩溃 因为结构体 FILE 不同,一个 fopen() 分配的结构无法被另一个 CRT 的 fread() 正确访问
最终后果 在如 jpeg_read_header() 这类库中崩溃,通常是 fill_input_buffer()fread() 报错
根本解决方法 所有代码 + 静态库 + 第三方库都要统一使用同一个 CRT,比如 /MDd

如你有更多工程要集成第三方库,我建议你默认使用动态 CRT /MD/MDd,并统一编译所有依赖库,否则 CRT 冲突永远是隐患。

非常好的问题,解释时我带上具体原理和调试思路,帮助你理解 为什么通过 fopen 函数地址能判断 CRT 不同,以及这背后的技术细节。


1. 进程运行时,函数地址的含义

当 Windows 程序运行时:

  • 程序会加载自己和依赖的 DLL(动态库),比如 C 运行时库 DLL(CRT)
  • 所有调用的函数都是通过进程内存中的某个地址执行的

fopen 是 CRT 提供的函数:

  • 它位于 CRT DLL(如 msvcrt.dllmsvcrtd.dllucrtbase.dll 等)或静态链接库中
  • 不同的 CRT 版本对应不同的 DLL 或静态代码块
  • 每个 DLL 被加载到不同的内存地址区段

2. 同一 CRT 的 fopen 地址是唯一的

  • 同一进程内只加载一个版本的 CRT(一般是 msvcrtd.dll
  • fopen 函数只存在于这一个 CRT DLL 里,所以地址唯一且固定

3. 不同 CRT 有不同的 DLL,地址必然不同

  • 如果两个项目链接了不同 CRT(如一个是动态 /MDdmsvcrtd.dll,另一个是静态 /MTd,代码编译进 exe)
  • 静态链接 CRT 代码是 exe 自身代码段的一部分,动态链接 CRT 是 DLL 代码段
  • 由于加载地址和代码内容不同,fopen 函数实际位置不同,地址自然不同

4. 实际调试中怎么看地址?

  • 用调试器(Visual Studio)断点或观察 fopen 符号地址,或者用 x fopen 命令(Windbg)
  • 看到不同地址说明调用的函数代码不在同一模块

5. 为什么这就能判断 CRT 不同?

  • fopen 函数是 CRT 的核心函数之一,不可能在多个不同模块里共存(正常程序不会这样做)
  • 如果同一程序内 fopen 地址不止一个(比如一个 DLL 一个 EXE),说明加载了多个 CRT 版本
  • 多个 CRT 版本共存导致结构体(FILE*)不兼容,产生崩溃

6. 举个比喻

你有两台工厂,工厂A用的机械手臂型号是X,工厂B用的是型号Y。两个型号的机械手臂结构和操作方式不同。

你从工厂A拿来的零件(FILE*结构)不能直接放到工厂B的机械手臂里用(fread调用),否则机械手臂坏了(程序崩溃)。

这里 fopen 地址不同就是你看到机械手臂型号不同的明显标志。


7. 总结

观察 说明
不同项目 fopen 地址不同 加载了不同 CRT 模块(DLL或静态库)
同一项目内 fopen 地址相同 只加载了一个 CRT 版本
CRT 版本不同导致 FILE* 不兼容 fopen 返回的指针传给其他 CRT 读取时崩溃

如果你想,我可以帮你写个简单的代码和脚本,演示怎么在调试器里查看 fopen 地址,验证不同 CRT 的加载情况。这样理解会更直观。你觉得怎么样?

相关推荐
岁忧1 小时前
(LeetCode 面试经典 150 题 ) 11. 盛最多水的容器 (贪心+双指针)
java·c++·算法·leetcode·面试·go
aramae6 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
lixzest6 小时前
C++ Lambda 表达式详解
服务器·开发语言·c++·算法
_Chipen7 小时前
C++基础问题
开发语言·c++
灿烂阳光g7 小时前
OpenGL 2. 着色器
c++·opengl
AA陈超9 小时前
虚幻引擎UE5专用服务器游戏开发-20 添加基础能力类与连招能力
c++·游戏·ue5·游戏引擎·虚幻
mit6.8249 小时前
[Meetily后端框架] AI摘要结构化 | `SummaryResponse`模型 | Pydantic库 | vs marshmallow库
c++·人工智能·后端
R-G-B9 小时前
【02】MFC入门到精通——MFC 手动添加创建新的对话框模板
c++·mfc·mfc 手动添加创建新的对话框
linux kernel9 小时前
第七讲:C++中的string类
开发语言·c++