不同编译器,对未初始化局部变量,在不同模式(RUN/DEBUG)下的处理

小编遇到了一个神奇的bug,花了整整两天时间修复,让我们一起看看吧

问题描述

  • 项目是最近的大作业LSM Tree,出现的bug如下
  • 出错的key是key为0的情况
平台/编译器 运行结果
Debian g++编译 直接运行 segmentation fault
Debian g++编译 gdb调试 segmentation fault
Qt MinGW 64-bit编译 直接运行 有时segmentation fault并且结果错误
Qt MinGW 64-bit编译 debug模式运行 正常,结果正确
VS2019 MSVC编译 直接运行 正常,结果正确
VS2019 MSVC编译 debug模式运行 正常,结果正确

问题分析

第一反应是编译器抽风了,但众所周知,以我拉跨的代码能力,必然是我写错了。开启了debug之旅。但是哪里错了???!!!

问题解决

经过排查,是skiplist边界处理有问题。

skiplist的某一行结构如下,两端的节点BoundaryNode并不储存实际的值。

BoundaryNode->node->node->BoundaryNode

修改前代码

image-20210529151558243

修改后代码

image-20210529112555018

相信大家一眼就可以看出我的错误,没有初始化key!!!!!!

让我们再看看另一部分skiplist的逻辑错误

修改前代码

image-20210529113342674

修改后代码

image-20210529113207687

这里竟然没有对BoundaryNode进行检查!!!!

变量初始化

问题是解决了,但为什么会出现开头这样的结果呢?

鉴于本人ICS学的相当拉跨,一下分析可能存在诸多错误,请诸位多多包涵。

最根本的原因在于,不同编译器初始化内存的方式不同

VS正常运行的原因

  • 寄存器数量有限,在运行时,很多局部变量和class、struct都放到了栈上
  • 一开始key没有初始化
  • 此处内存为0xccccc…
  • 当key为0时,并不会被当作BoundaryNode,所以逻辑正常

image-20210529114745032

g++下发生错误的原因

  • 此处内存为0x000….

将同一段代码用g++编译运行,可以看到输出为0

image-20210529133542439

为什么debug模式下和直接运行时结果不同?

我的理解:不同IDE和compiler的run和debug功能下,编译时,对于未初始化变量的实现不同

这就涉及到部分IDE的run和debug功能的实现,我在网上找到了一段解释。

When code is compiled in debug mode, the compiler may inject its own code that initializes empty variables to a default value. This is done to protect against vulnerabilities (more on this later), and to more easily detect bugs by giving the variable a bogus value that can be easily identified as uninitialized if it is for example printed to the screen.

简单的说说我的理解

  • 编译器可能会注入自己的代码来初始化尚未初始化的变量
  • 目的一:用于发现未初始化的变量,便于debug输出
  • 目的二:使用未初始化的变量很危险!

更详细的解释:

以下是foo构造函数的汇编代码(vs编译),好家伙,比纯c的汇编难懂多了,而且是Intel Syntax。

(以下推测不保证正确)

  • line276-line278 给%eax赋值0xcccccccc,将%eax写到栈上,再pop到%ecx
  • line279 给x赋值0xcccccccc
  • line281 call了一个debug相关的函数
  • line283 给y赋值0

image-20210529141154644

为什么是0xcccccccc?

在google上找了一下

就是说0xcc恰好是汇编中断的信号,会触发debugger break,便于debug

StackOverflow上的一个回答很好,贴在下面作为结尾

The value 0xcccccccc is magical, it is very good at crashing your program when you use an uninitialized pointer. Or generate a weird int value. Or crash your code when it goes bananas and start to execute data as though it is code. 0xcc is the x86 instruction for INT 3, it invokes a debugger break.

The “why this place” is part of the diagnostics you get from /RTC. It make the compiler allocate local variables with extra space between them. Filled by that magical value. Which makes it very simple to diagnose stack corruption caused by buffer overruns, it just needs to check if the magic values are still there when the function returns.

链接:https://stackoverflow.com/questions/17644418/why-is-the-stack-filled-with-0xcccccccc

彩蛋

vs经典:烫烫烫烫烫,屯屯屯屯屯

综上所述,我没初始化变量,我寄了,下次一定记得