我引用了一个开源库 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是否等于infileinfile的值是否合理(不是0xCDCDCDCD或nullptr)- 加断点观察
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.lib或ucrtbase.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++ 提供的运行时库,包含了 fopen、malloc、printf 等函数的底层实现。
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.lib 或 ucrtbase.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.dllFILE*结构无法共享 → 崩溃
✅ 怎么验证?
你已经验证了:两个项目中 fopen 地址不同 → 明确是两个不同的 CRT
你也可以在调试器中使用:
cmd
x fopen
或在 Visual Studio 中查看:
-
调试时点击"模块"窗口(Debug > Windows > Modules)
-
查看加载了哪些 CRT DLL,如:
ucrtbased.dll→ /MDdlibcmt.lib→ /MTdmsvcrtd.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.dll、msvcrtd.dll、ucrtbase.dll等)或静态链接库中 - 不同的 CRT 版本对应不同的 DLL 或静态代码块
- 每个 DLL 被加载到不同的内存地址区段
2. 同一 CRT 的 fopen 地址是唯一的
- 同一进程内只加载一个版本的 CRT(一般是
msvcrtd.dll) fopen函数只存在于这一个 CRT DLL 里,所以地址唯一且固定
3. 不同 CRT 有不同的 DLL,地址必然不同
- 如果两个项目链接了不同 CRT(如一个是动态
/MDd的msvcrtd.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 的加载情况。这样理解会更直观。你觉得怎么样?