首页
论坛
课程
招聘
[原创]这个崩溃有点意思,你中过招吗
2022-9-23 14:11 12485

[原创]这个崩溃有点意思,你中过招吗

2022-9-23 14:11
12485

缘起

前几天,在加班赶进度时遇到了一个意想不到的崩溃。由于是新加的代码导致的问题,所以很快就定位到了问题代码。但是,看了好几遍也没看出问题在哪?虽然代码在逻辑上有漏洞——某些情况下没有返回值,但是在我的认知里,应该不会导致崩溃。本文记录了使用 IDA 静态分析反汇编代码定位这个问题的过程。

示例代码

因为整个定位过程非常简单,就不在这里啰嗦了。定位到问题后,我特意建了一个简单的测试工程。关键代码不多,就几行,我把测试代码粘贴如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "stdafx.h"
#include <string>
 
class ConfigParam
{
public:
    int option;
    std::wstring strValue;
};
 
ConfigParam GetParam(int option)
{
    ConfigParam result;
    result.option = option;
    result.strValue = L"default";
    if (option == 0)
    {
        return result;
    }
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    ConfigParam param = GetParam(1);
    return 0;
}

在开始分析之前,请先停下来思考一下,上面的代码有问题吗?会导致崩溃吗?

 

如果之前看到这段代码,你问我会不会崩溃。我的回答是:不会。但是现在我的回答是:。多么痛的领悟。

残酷的崩溃

vs 2010 中按 F5 调试启动,无情的中断下来了。入下图:

 

crash-when-destrcutor-called

 

惊不惊喜,意不意外?

 

GetParam() 反回一个 ConfigParam 类型的对象,这个反回的对象在析构的时候却崩溃了。如果仔细观察 GetParam() 的实现,可以发现 GetParam() 并不是所有分支上都有返回值,但是编译器应该会返回一个临时对象。难道这个反回的临时对象有问题?对于这种问题,唯有通过反汇编才能找到答案。

请出 IDA

使用 IDA 打开 对应的程序,找到 GetParam() 的反汇编,可以发现一个有意思的事情是 GetParam() 的形式。本来声明的是

 

ConfigParam GetParam(int option),在 IDA 中看到的却是 ConfigParam *__fastcall GetParam(ConfigParam *result, int option)。如下图:

 

GetParam-real-type

 

依稀记得多年前接触汇编的时候,了解到一种说法:如果返回值类型比较大(大家应该知道在 32 位程序中,函数的返回值基本是通过 EAX 反回的),那么会把返回值的地址当作第一个参数传递给函数,EAX 指向的是返回值的地址。正好跟 IDA 对应上了。

查看关键逻辑

代码中的 GetParam() 函数,当 option0 的时候,会反回局部的 result,否则什么都不做。看看编译器帮我们做了什么吧。编译器做的事情也是,当 option0 的时候,执行拷贝构造函数把局部的 result 返回出去,否则不会对参数中的 result 做任何操作。关键代码如下图所示:

 

key-logic-not-assign-result

 

那么在调用 GetParam() 函数的地方,会对 result 做什么初始化的工作吗?

查看 main 函数逻辑

从下图可以清楚的看到,main() 函数并没有对 result 做任何初始化就传递给了 GetParam() 函数。

 

result-not-initialized-in-main

 

所以,调用完 GetParam() 后,main() 函数中的 result 是一个未初始化的对象。而不是一个调用过构造函数的对象。所以后面再调用其析构函数的时候,发生什么事情都是正常的了。我在遇到这个问题之前,一直以为 GetParam() 函数返回来的是一个初始化过的对象,因为根据之前的认知,在对象产生的时候一定会调用构造函数。这里既没有调用构造函数,也没有调用拷贝构造函数。

vs 的 bug ?

这个问题最先是在 vs2019 上发现的,我还以为是 vs2019bug,于是试了 vs2017vs2013vs2010,发现都会崩溃。但是每个版本的 vs 都会给出一个警告:warning C4715: 'GetParam' : not all control paths return a value

 

not-all-control-paths-return-a-value-warning

 

虽然给了警告,但是多少还是觉得 vs 的处理不太合理,难道所有编译器都是这个行为吗?试试 gcc 中的行为。

gcc

不知道大家是否还记得我之前分享过的一个宝藏网址(https://gcc.godbolt.org/),可以查看各种编译器对同一段代码的编译结果。下图是 gcc5.2GetParam() 函数的反汇编代码。

 

view-disassembly-code-of-getparam-in-gcc5.2

 

可见,逻辑十分清晰, 56 行中的 rdi 指向的是返回值地址,第 60 行会先调用构造函数,传递的对象地址就是 56 行的 rdi (虽然中间经过 [rbp-24]rax 倒了两手)。第 69 行判断 option 是否为 0,但是第 70 行直接来了个强制跳转(并没有根据比较结果跳转,这个编译器有点屌),跳转到了 .L8 的位置,后面几行是函数返回的处理。

 

可见,gcc 生成的代码会在 GetParam() 内部会先初始化,再返回。这样就避免了崩溃问题。

 

再看看 main() 函数的反汇编代码,入下图:

 

view-disassembly-code-of-main-in-gcc5.2

 

逻辑非常清晰易懂。第 89 行把局部变量的地址加载到 rax 中,第 90 行把 1 赋值到 esi 中,第 91 行把 rax 的值放到 rdi 中,第 92 行 调用 GetParam() 函数。

扩展: 感觉 gcc 生成的反汇编对应的调用约定是这样的 :函数的第一个参数通过 rdi 传递,第二个参数通过 rsi 传递。

简单搜了一下,linux 平台 x64 应用程序的调用约定还真是这样的,具体可以参考这篇文章 https://www.cnblogs.com/shines77/p/3788514.html。

 

综上分析,同样的代码在 gcc 5.2 中的结果是正确的。

 

函数有返回值但是却不反回,这应该不算是正常情况,也许在标准中对这种行为有描述?是未定义行为?编译器可以根据自己的喜好发挥?一切还要到标准中找答案。

翻看标准

在网站 https://open-std.org/JTC1/SC22/WG21/docs/standards 上找到了 c++ 标准的草稿。我参考的版本是 N3242。这个是 2011 版的草稿。网站上的原话是

A draft for the 2011 edition is available in N3242.

 

在第 6.6.3 节中有一段简单的描述:有返回值却不返回值的情况是未定义的行为。原文截图如下:

 

undefined-behaviour-in-cpp-standard-draft

总结

如果一个函数是有返回值的,但是却不返回值,这个行为是未定义的。每个编译器可以自由发挥。很多版本的 vs 会給警告。一定要重视编译器的警告!!!

参考资料

N3242 https://open-std.org/JTC1/SC22/WG21/docs/papers/2011/n3242.pdf

 

调用约定 https://www.cnblogs.com/shines77/p/3788514.html。

 

查看反汇编代码的宝藏网址 https://gcc.godbolt.org/


[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~

收藏
点赞4
打赏
分享
最新回复 (23)
雪    币: 52
活跃值: 活跃值 (1317)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
实都 活跃值 2022-9-23 15:26
2
0
图片全挂了?
雪    币: 3
活跃值: 活跃值 (1708)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
咖啡_741298 活跃值 2022-9-23 18:04
3
0
这代码会不会崩溃不一定,但是肯定是无法预测结果。  这代码咋写出来的? 不是一眼看上去就有问题吗
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-9-24 16:06
4
0
咖啡_741298 这代码会不会崩溃不一定,但是肯定是无法预测结果。 这代码咋写出来的? 不是一眼看上去就有问题吗
嗯,不一定崩溃。拿出来,一眼能看出问题,在一坨代码中就不是那么明显的了。
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-9-24 16:07
5
0
实都 图片全挂了?
我这里能看到,可能有时候看不到
雪    币: 10732
活跃值: 活跃值 (6304)
能力值: ( LV13,RANK:375 )
在线值:
发帖
回帖
粉丝
TkBinary 活跃值 5 2022-9-30 10:02
6
0
总结: GetParam() 函数没有默认返回值. 我遇到过确实很坑.特别是在线上环境要下来找Bug.加上代码一多头都浑了. 后来看汇编解决的. 从此长了记性.
雪    币: 1136
活跃值: 活跃值 (430)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
书生怕怕 活跃值 2022-9-30 10:30
7
1
总结啥啊!如果这个结构体中wstring 换成整数类型就不会出现崩溃
雪    币: 2870
活跃值: 活跃值 (4389)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
舒默哦 活跃值 1 2022-9-30 11:19
8
0
我用vs2022测试了,debug模式,编译通不过;
release模式编译能通过,运行结束时没有崩溃。打印的结果,数值是随机的
总结:
正式的工程项目,应该勾选”启用所有警告"。
雪    币: 542
活跃值: 活跃值 (402)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tian_chen 活跃值 2022-10-1 09:24
9
0
就是后面有返回也是局部变量,如果析构使用,不还是一样.....
雪    币: 899
活跃值: 活跃值 (1187)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10ngvv 活跃值 2022-10-1 12:41
10
0

一开始看代码觉得没返回值觉得奇怪,虽然说函数内不会返回局部变量result,但是想着编译器按理说应该会有一个右值返回才对,跳出函数这个右值调用拷贝构造函数初始化param,感觉代码又毛病但不至于奔溃。

 但时,没想到vs直接代码调试发现实际运行了一次构造和两次析构,外部param初始化并没有调用到拷贝构造函数以至于后续析构时异常产生。

 当然上面是在调试情况下的,release下编译器会进行优化,这个问题由于代码优化不会产生。 以上复现代码可以简化成下面的样子:

class ConfigParam2
{
public:
	std::wstring m_str; // 保证构造函数的调用
};

ConfigParam2 GetParam()
{
	if (false) { return ConfigParam2(); }
}

int _tmain(int argc, _TCHAR* argv[])
{
	ConfigParam2 param = GetParam();
	return 0;
}

上面代码在vs的debug环境下,param不会有构造行为,于是在程序结束时,调用虚构函数释放资源时就出现异常。


总结:和楼主说的一样,不要忽略warning(我以前都时直接无视的)

最后于 2022-10-1 12:53 被10ngvv编辑 ,原因:
雪    币: 11348
活跃值: 活跃值 (8002)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
一半人生 活跃值 5 2022-10-1 20:29
11
0


这种问题比较晦涩,根编码习惯和编译有关系。避免返回对象和指针,尽可能返回常量,另外推荐使用智能指针

bool GetParam(std::shared_ptr<ConfigParam>& ptrConfParam)
{
        const int option = 0;
        
        if (ptrConfParam)
            return true;
        
        do {
            if (option != 0)
                break;
         
            ptrConfParam = std::make_shared<ConfigParam>();
            if (!ptrConfParam)
                break;
                
            ptrConfParam->option = option;
            ptrConfParam->strValue = L"default";
            return true;
        } while (false);
 
        return false;
}

int _tmain(int argc, _TCHAR* argv[])
{

        std::shared_ptr<ConfigParam> ptrConfParam = nullptr;
        const bool nRet = GetParam(ptrConfParam);
        if (nRet && ptrConfParam)
        {
            ptrConfParam->option;
            ptrConfParam->strValue;
        }
        return 0;
}




最后于 2022-10-1 20:29 被一半人生编辑 ,原因:
雪    币: 223
活跃值: 活跃值 (186)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chenteng 活跃值 2022-10-3 08:57
12
0

这个很明显新手写出来的代码,判断条件无法保证一定有返回值,关键你返回的还是对象,如果返回的只是真假,最多编译报个警告.这说明写代码的人逻辑不够严谨,这在c/c++语言中是很致命的问题,因为不知道啥时候就因为逻辑不严谨造成程序莫名崩溃.也许这个程序员之前是写java,c#这类的吧.(之前在一个程序员群里讨论java也会内存泄露,一大堆写过java的表示完全不知道,后面看了文章才发现原来java也会内存泄露.)

最后于 2022-10-3 09:03 被chenteng编辑 ,原因:
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:09
13
0
TkBinary 总结: GetParam() 函数没有默认返回值. 我遇到过确实很坑.特别是在线上环境要下来找Bug.加上代码一多头都浑了. 后来看汇编解决的. [em_77]从此长了记性.
哈哈 原来大佬也会犯这种低级错误
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:10
14
0
书生怕怕 总结啥啊!如果这个结构体中wstring 换成整数类型就不会出现崩溃
谁说不是呢
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:10
15
0
舒默哦 我用vs2022测试了,debug模式,编译通不过; release模式编译能通过,运行结束时没有崩溃。打印的结果,数值是随机的[em_1] 总结: 正式的工程项目,应该勾选”启用所有警告&qu ...
嗯,要重视警告
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:11
16
0
tian_chen 就是后面有返回也是局部变量,如果析构使用,不还是一样.....
如果是这样 可能还真不一样
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:14
17
0
10ngvv 一开始看代码觉得没返回值觉得奇怪,虽然说函数内不会返回局部变量result,但是想着编译器按理说应该会有一个右值返回才对,跳出函数这个右值调用拷贝构造函数初始化param,感觉代码又毛病但不至于奔溃。 ...
细!vs 中确实没有调用构造函数
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:17
18
0
一半人生 这种问题比较晦涩,根编码习惯和编译有关系。避免返回对象和指针,尽可能返回常量,另外推荐使用智能指针。bool&nbsp;GetParam(std::shared_ptr&lt;Conf ...
确实比较隐晦。注意编译器的警告。用智能指针也是一种方案,不过对于项目中的那种情况有点大炮打蚊子的意思
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-30 22:18
19
0
chenteng 这个很明显新手写出来的代码,判断条件无法保证一定有返回值,关键你返回的还是对象,如果返回的只是真假,最多编译报个警告.这说明写代码的人逻辑不够严谨,这在c/c++语言中是很致命的问题,因为不知道啥时候 ...
对,但是有时候写代码就是会粗心大意那么几下。我经常把  != 和 == 写反。
雪    币: 2235
活跃值: 活跃值 (1352)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
无心红叶 活跃值 2022-10-31 01:43
20
0
C4715这种warning本质就是error。除非你明确知道这个warning的影响,否则不应该轻易放过任何一个warning
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-10-31 12:30
21
0
无心红叶 C4715这种warning本质就是error。除非你明确知道这个warning的影响,否则不应该轻易放过任何一个warning
谁说不是呢,vs 应该直接报个错 多省心
雪    币: 73
活跃值: 活跃值 (676)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hixhi 活跃值 2022-10-31 17:30
22
0
"不要返回对局部对象的引用”,C++Primer这本书上的这句话大家都忽略了吧?
雪    币: 6869
活跃值: 活跃值 (5800)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-11-1 12:31
23
0
hixhi "不要返回对局部对象的引用”,C++Primer这本书上的这句话大家都忽略了吧?
没人返回局部对象的引用啊?
雪    币: 73
活跃值: 活跃值 (676)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hixhi 活跃值 2022-11-4 18:08
24
0
编程难 没人返回局部对象的引用啊?[em_4]

是我错了,sorry

最后于 2022-11-4 18:09 被hixhi编辑 ,原因:
游客
登录 | 注册 方可回帖
返回