在程序开发过程中,调试是不可或缺的一环。调试不仅可以帮助开发者发现错误,还能提供程序运行时的内部状态信息。然而,在调试过程中,开发者有时会遇到一些奇怪的字符。这些乱码通常是由内存状态的特殊标记,或者字符集不匹配导致的。在本文中,我们将探讨这些乱码的由来,以及它们在程序开发中的含义。
内存调试与特殊填充值
在Visual Studio(MSVC)和Clang等编译器中,为了帮助开发者发现和调试内存使用错误,编译器和运行时库会使用特殊的填充值(也称为幻数,Magic Numbers)来标记未初始化的内存、新分配的堆内存以及已经释放的内存。这些填充值有助于识别内存损坏和使用未初始化的变量等问题。
MSVC的内存填充值
在Microsoft Visual C++ (MSVC) 编译器中,Debug模式下会使用以下填充值来标识内存状态:
-
未初始化的栈内存:通常填充为
0xCC
。在x86架构的汇编中,0xCC
对应的指令是INT 3
,它是一个软件中断指令,常用于调试断点。如果代码尝试执行这部分内存,程序会因为触发了断点而停止。 -
新分配的堆内存:常被填充为
0xCD
。这个值在调试器中没有特定的指令对应,但它可以帮助开发者快速识别出使用了未初始化的堆内存。新分配的但还未提交的内存页通常也填充为0xCD
-
已经释放的堆内存:通常填充为
0xDD
。这样做是为了标记内存已经不再有效,如果代码试图访问释放的堆内存,这个填充值可以帮助开发者发现悬挂指针(dangling pointer)问题。为了方便记忆,可以理解成DeleteDelete(DD)。
在MSVC中,我们还可以通过更细致的配置来优化调试体验:
- _CRTDBG_MAP_ALLOC:通过定义此宏,可以使得malloc、calloc等内存分配函数映射到调试版本,从而在调试输出中提供更详细的分配信息。
- _CRTDBG_FILL_MEMORY:在调试版本中,可以设置此宏来控制自由内存块的填充模式和值,进一步定制化内存检查策略。
- /RTC 编译选项:使用运行时错误检查选项,如/RTC1(默认)会检测未初始化的局部变量,而/RTCc会强制所有堆和栈变量初始化。
Clang的内存填充值
Clang编译器,也会使用特殊的填充值来帮助开发者识别内存错误。AddressSanitizer是一个快速的内存错误检测器,可以检测出包括使用后释放(use-after-free)、堆栈缓冲区溢出、堆缓冲区溢出、全局缓冲区溢出、使用前初始化(use-before-init)等多种内存错误。
当使用AddressSanitizer时,Clang会对内存进行特殊的处理:
-
堆内存:新分配的堆内存会被初始化为特定的模式,通常是一系列的
0xAB
字节。而一旦堆内存被释放,它会被填充为一系列的0xEF
字节。这些值有助于识别使用后释放和堆溢出错误。可以记忆成:ABCDEF,用之前是AB,用之后是EF。 -
栈内存:未初始化的栈变量通常会被填充为一系列的
0xAB
字节。这有助于发现栈变量的使用前初始化错误。 -
全局变量:同样,全局或静态变量会被填充为特定的字节模式,以帮助识别未初始化的使用。
AddressSanitizer还会在分配的内存块周围插入"红区"(red zones),这是一些额外的非可访问区域,用以检测缓冲区溢出错误。
需要注意的是,这些填充值和行为是AddressSanitizer的特性,并不是Clang编译器在所有编译模式下的默认行为。在不使用AddressSanitizer的情况下,Clang编译器默认并不会自动对未初始化的内存进行填充。但是,如果开启了某些特定的调试或安全特性,Clang可能会有类似的行为。
GCC的情况
GCC(GNU Compiler Collection)本身并不像MSVC那样在Debug模式下自动填充未初始化的内存。然而,当使用GCC配合特定的内存错误检测工具,如Valgrind或GCC的Sanitizers(例如AddressSanitizer, MemorySanitizer)时,会有类似的行为。
AddressSanitizer(ASan)和MemorySanitizer(MSan)是用于快速检测各种常见的内存错误和未定义行为的工具。
-
AddressSanitizer(GCC和Clang均支持):它会在堆内存分配时使用特殊的填充值,并在释放后也会标记这些内存。新分配的堆内存通常填充为
0xAB
字节,释放后的堆内存填充为0xEF
字节。AddressSanitizer还会在分配的内存块前后插入保护区,帮助检测堆溢出和使用后释放错误。 -
Valgrind:Valgrind是一个开源的软件工具,它提供了一系列的工具用于调试内存管理和线程错误,以及分析性能。Valgrind被广泛用于发现Linux和其他类Unix系统上C和C++程序中的内存管理错误,如内存泄露、不正确的内存分配和释放、使用未初始化的内存、数组越界、不正确的使用malloc/new和free/delete等。
简而言之,GCC并不会自动在Debug构建中填充栈内存。如果需要检测对未初始化栈内存的使用,可以使用工具如Valgrind或GCC的 -finit-local-zero
选项,后者会初始化所有局部变量为零,但这会导致性能下降,不建议在生产环境中使用。
总结来说,GCC自身并不指定特定的填充值来标记未初始化的内存或者释放后的内存,而是依赖于外部工具或编译器选项来帮助开发者发现内存相关的错误。
烫烫烫,屯屯屯”的由来
由于Visual Studio调试器默认使用的是多字节字符集(MBCS),在MBCS中0xCCCC对应中文字符“烫”,因此未初始化的栈内存显示为连串的“烫”。另一方面,新分配的堆内存会被初始化为0xCD,在MBCS中0xCDCD对应的是中文字符“屯”。
锟斤拷的由来
在Unicode中,当遇到无法表示的字符时,会使用一个特殊的占位符来表示,这个字符是U+FFFD REPLACEMENT CHARACTER。任何无法识别的字符都会被替换为这个字符,其UTF-8编码是0xEFBFBD。如果连续出现多次无法显示的字符,对应的编码序列就是连续的0xEFBFBD,在UTF-8编码下16进制表示为:
0xEF 0xBF 0xBD 0XEF 0xBF 0xBD
以上这段编码,如果放到GBK中进行解码的话,因为GBK中一个下汉字两个字节,那么
结果就是:
0xEF 0xBF , 0xBD 0XEF , 0xBF 0xBD
这个结果对应的字符就是:锟(0xEFBF),斤(0xBDEF),拷(0xBFBD)。
所以,以后再见到银斤拷,第一时间想到UTF-8和GBK的转换问题准没错。
可见,“锟斤拷”这样的乱码是由于新旧编码系统转换不匹配造成的。