静态库那些事儿/MT /MD

本文探讨了单例模式在C++中的实现及其线程安全问题,并深入分析了跨模块内存分配与释放导致的崩溃原因及解决方案。

总结下,需要注意的是对于多个模块的开发,确保该模块的malloc自己free就OK了。

引用自

https://zhuanlan.zhihu.com/p/20628410?refer=jilinxiaohuo

https://www.zhihu.com/question/45753516

单例模式是一种很简单常用的设计模式,常见的做法可能是这样:

Renderer& getInstance()
{
    static Renderer renderer;
    return renderer;
}

当然,这个代码在不支持static local variable thread-safe init的编译器上,是没有办法保证线程安全的,c++11标准已经规定static local variable只会被初始化一次了,然而vs2013还没有实现,vs2015里才支持了这条标准.不过这条代码在我们的程序里只有一个线程访问,所以也就不存在线程安全的问题.

然后有一天,写了点代码,程序崩溃了:

Renderer::getInstance().xxxx();

分析发现是Renderer里的一个成员变量m_pMap == NULL了,而在项目的其他地方,也有用这个xxxx()函数的地方,竟然没有问题.

那就在xxxx()里下个断点,然后看下this指针的值吧,第一次命中时this的值是0x0456adc4, 第二次命中的时候是0x074324ad(这个地址是我随便写的,反正就是这两次命中断点this的值不一样).这是怎么回事呢?为什么出现了两个Renderer的实例呢?

细心的我发现这两个this指向的Renderer实例并没有在同一个模块内,一个在a.dll里,一个在main.exe里!造成这种现象的原因,是因为开篇的那个getInstance()方法所在工程是一个静态库,然后main.exe工程和a.dll工程均链接了这个静态库,导致main.exe里和a.dll里都存在一个renderer实例.而我们这个renderer实例在使用前,要这样:

Renderer::getInstance().Create();

然后才可以初始化m_pMap, 崩溃在main.exe里那行代码之前,并没有调用Create(),所以导致m_pMap == NULL崩溃了.

问题到这里已经水落石出了,想办法弄成在两个模块里共用一个实例就可以了!问题解决.

当然,如果文章就这样结束了,未免太没劲儿了吧,顺便说说另一个大家经常忽略的事情.

在开发dll的过程中,总会有意无意的写出一些跨模块分配释放内存的代码,比如在A模块malloc了一块内存,在B模块free,然后导致崩溃.然后将编译器选项由/MT改为/MD就可以解决问题.为什么会出现这种问题呢?我们来看看malloc的实现(vs2013 crt):

__forceinline void * __cdecl _heap_alloc (size_t size)
{

    if (_crtheap == 0) {
#if !defined (_CRT_APP) || defined (_DEBUG)
        _FF_MSGBANNER();    /* write run-time error banner */
        _NMSG_WRITE(_RT_CRT_NOTINIT);  /* write message */
#endif  /* !defined (_CRT_APP) || defined (_DEBUG) */
        __crtExitProcess(255);  /* normally _exit(255) */
    }

    return HeapAlloc(_crtheap, 0, size ? size : 1);
}

malloc最后会调用HeapAlloc来分配内存,msdn看看HeapAlloc的说明,

HeapAlloc Function

Allocates a block of memory from a heap. The allocated memory is not movable.

Syntax
LPVOID WINAPI HeapAlloc(
  __in  HANDLE hHeap,
  __in  DWORD dwFlags,
  __in  SIZE_T dwBytes
);

Parameters
hHeap A handle to the heap from which the memory will be allocated. This handle is returned by the HeapCreate or GetProcessHeap function.
,对着msdn的说明可以知道,malloc用的heap handle是 _crtheap,这个_crtheap是个全局变量,那我们看看是什么时候给_crtheap赋值的吧. 跟着红色箭头从上往下分析,首先在BaseThreadInitThunk上下个断点,然后F5执行,断点命中之后执行
x *!*_crtheap*
找到crtheap的地址,结果显示在0f7ec190这里,这里的值目前也是0x00000000,证明还没有被赋值,那就在这个地址上打个写断点,
ba w4 0f7ec190
F5执行,断点命中,输入kb显示调用栈,结果发现是在_heap_init里对_crtheap赋值的,看看heap_init的代码吧:
int __cdecl _heap_init (void)
{
        //  Initialize the "big-block" heap first.
        if ( (_crtheap = GetProcessHeap()) == NULL )
            return 0;

        return 1;
}
只是调用GetProcessHeap而已,那还得看看GetProcessHeap的实现:
KERNELBASE!GetProcessHeap:
75415620 64a118000000    mov     eax,dword ptr fs:[00000018h]
75415626 8b4030          mov     eax,dword ptr [eax+30h]
75415629 8b4018          mov     eax,dword ptr [eax+18h]
7541562c c3              ret
分析汇编可知,首先去TEB + 0x18里取值赋值给eax,根据上图可知,0x18为Self指针,其实就是TEB自己的地址,然后去TEB+0x30里取值赋值给eax,由图可知,0x30为ProcessEnvironmentBlock,即PEB, 然后去PEB+0x18里取值作为返回值,也就是ProcessHeap(见下图),那我们看看PEB+0x18是多少,

由图可知GetProcessHeap会返回005d0000,然后赋值给_crtheap.那和/MT /MD有什么关系呢?

/MT:

Causes your application to use the multithread, static version of the run-time library.

/MD:

Causes your application to use the multithread- and DLL-specific version of the run-time library.
如果使用/MT,会使用静态版本的运行时库,每个模块里都会有一个_crtheap全局变量,_heap_init就会在每个模块里都被调用一次,而使用/MD则使用动态库版本的运行时库,整个进程里只有运行时库里才有一份_crtheap全局变量,在crt的dll加载的时候调用一次_heap_init.但是分析_heap_init的实现可知,就算调用多次,返回的依然是PEB里的那个堆句柄啊,也不会导致不同模块里的_crtheap有不同的值,那HeapFree和HeapAlloc使用的handle就是同一个,也不应该引起崩溃啊.事实的确如此,我也是在今天写这篇专栏用windbg分析的时候才发现这个问题.是我之前记错了还是crt的实现有变化呢?于是我查看了下vs2003的crt的_heap_init的实现:
int __cdecl _heap_init (
        int mtflag
        )
{
        //  Initialize the "big-block" heap first.
        if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
                                     BYTES_PER_PAGE, 0 )) == NULL )
            return 0;

#ifndef _WIN64
        // Pick a heap, any heap
        __active_heap = __heap_select();

        if ( __active_heap == __V6_HEAP )
        {
            //  Initialize the small-block heap
            if (__sbh_heap_init(MAX_ALLOC_DATA_SIZE) == 0)
            {
                HeapDestroy(_crtheap);
                return 0;
            }
        }
#ifdef CRTDLL
        else if ( __active_heap == __V5_HEAP )
        {
            if ( __old_sbh_new_region() == NULL )
            {
                HeapDestroy( _crtheap );
                return 0;
            }
        }
#endif  /* CRTDLL */
#endif  /* _WIN64 */

        return 1;
}
这就明了了,如果在vs2003里开发dll,然后编译dll的时候选择/MT,那这个dll内部就会有一份_crtheap,那在加载这个dll的时候,会调用一次_heap_init,然后调用HeapCreate来初始化_crtheap.这也就意味着其他模块使用/MT选项也会如此,导致每个模块里的_crtheap值是不同的.这样跨模块执行free,就会导致在handle A上分配的内存去handle B上释放导致了崩溃.如果所有模块均使用/MD选项,则只有crt的dll里有一份_crtheap,在crt的dll被加载的时候执行一次HeapCreate并赋值给_crtheap,然后HeapAlloc与 HeapFree使用的HANDLE都是同一个_crtheap,就不会崩溃了.

问题到这里就分析完毕了。

## 什么是静态链接 CRT(/MT)? **`/MT` 是 Microsoft Visual C++ 编译器中的一个编译选项,表示:将 C 运行时库(C Runtime Library, CRT)以“静态库”的方式链接到你的可执行文件中。** 这意味着: ✅ 所有标准 C 函数(如 `printf`, `malloc`, `fopen`, `strcpy` 等)的代码都会被直接打包进你的 `.exe` 或 `.lib` 文件中,**不需要外部 DLL 支持。** --- ## 🔧 `/MT` 的全称和变体 | 选项 | 含义 | |------|------| | `/MT` | 使用 **多线程、静态链接** 的 Release 版本 CRT(对应库:`libcmt.lib`) | | `/MTd` | 使用 **调试版本** 的静态 CRT(对应库:`libcmtd.lib`),用于调试构建 | > ⚠️ 注意:`/MT` 中的 "T" 不是 "Thread" 的缩写!它代表 **"static library" (单个 T 表示 MT 模型)**,这是历史命名。 --- ## 📦 工作原理(对比动态链接 /MD) | 特性 | `/MT`(静态链接) | `/MD`(动态链接) | |------|------------------|------------------| | CRT 存放位置 | 直接嵌入 `.exe` 或 `.dll` | 外部 DLL(如 `ucrtbase.dll`) | | 是否需要部署 CRT DLL | ❌ 不需要 | ✅ 需要安装 VC++ Redistributable | | 可执行文件大小 | 较大(自带 CRT 代码) | 较小(只留接口) | | 多个程序共享 CRT | ❌ 每个程序各有一份 | ✅ 共享同一份 DLL | | 安全更新 | ❌ 必须重新编译才能更新 CRT | ✅ 系统更新 DLL 即可修复漏洞 | | 内存占用 | 高(重复加载多个副本) | 低(共享内存页) | --- ## 💡 示例说明 假设你写了这样一个程序: ```cpp // hello.cpp #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } ``` ### 情况 1:使用 `/MT` 编译 ```bash cl /c /MT hello.cpp // 编译为对象文件 link hello.obj // 链接(自动选择 libcmt.lib) ``` 👉 结果: - 生成的 `hello.exe` 包含了 `printf` 的实现代码 - 不依赖任何外部 CRT DLL - 可以直接拷贝到其他电脑运行(即使没有安装 VC++ 运行库) --- ### 情况 2:使用 `/MD` 编译 ```bash cl /c /MD hello.cpp link hello.obj ``` 👉 结果: - `hello.exe` 很小,但依赖 `ucrtbase.dll` - 如果目标机器没有这个 DLL,程序无法启动 - 优点是多个程序共用同一个 CRT,节省内存 --- ## 🗂️ 静态链接时实际链接了哪个库? 当你使用 `/MT`,链接器会自动链接以下库之一: | 编译模式 | 自动链接的库 | |--------|-------------| | Release (`/MT`) | `libcmt.lib` | | Debug (`/MTd`) | `libcmtd.lib` | 这些 `.lib` 是 **静态库文件**(不是导入库),里面包含了完整的函数机器码。 你可以通过以下命令查看链接了什么: ```bash dumpbin /DIRECTIVES hello.obj ``` 输出可能包含: ``` Linker Directives ----------------- /DEFAULTLIB:libcmt /DEFAULTLIB:uuid.lib ``` 这表示编译器告诉链接器:“请使用 `libcmt.lib`”。 --- ## ✅ 何时应该使用 `/MT`? | 场景 | 推荐使用 `/MT`? | |------|------------------| | 发布独立工具(绿色软件) | ✅ 是,避免依赖问题 | | 嵌入式系统或无网络环境 | ✅ 是,确保兼容性 | | 插件(.dll) | ❌ 否,容易与宿主冲突 | | 团队协作项目 | ❌ 否,建议统一用 `/MD` | | 需要安全热更新 CRT | ❌ 否,应选 `/MD` | --- ## ⚠️ 使用 `/MT` 的潜在问题 1. **CRT 状态不共享** - 如果你同时有 `.exe`(用 `/MT`)和 `.dll`(用 `/MD`),它们各自维护自己的堆、文件句柄、全局变量。 - 在一个模块中 `malloc`,在另一个中 `free` → **崩溃!** 2. **体积膨胀** - 每个程序都带一份 CRT,导致磁盘浪费。 3. **安全补丁滞后** - 即使微软修复了 `strcpy` 的漏洞,你的程序仍使用旧版,除非重新编译。 --- ## 🛠️ 如何在项目中设置 `/MT`? ### 方法一:命令行 ```bash cl /MT your_file.cpp ``` ### 方法二:Visual Studio 项目设置 1. 右键项目 → **Properties** 2. Configuration Properties → C/C++ → Code Generation 3. 将 **Runtime Library** 设置为: - Release: `Multi-threaded (/MT)` - Debug: `Multi-threaded Debug (/MTd)` --- ## ✅ 总结 > `/MT` 表示 **静态链接 C 运行时库**,即将 `printf`, `malloc` 等标准函数的代码直接打包进你的程序,无需外部 DLL。 📌 优点: - 独立发布,免依赖 - 移植性强 📌 缺点: - 文件大 - 无法享受系统级 CRT 更新 - 多模块混合时易出错 > 🎯 推荐原则: > - 工具类小程序 → `/MT` > - 正式大型项目、DLL、插件 → `/MD` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值