山东001在线

 找回密码
 立即注册
搜索
查看: 103|回复: 0

说说这个崩溃有点意思,你中过招吗2023/3/27 8:30:10

[复制链接]
  • TA的每日心情
    奋斗
    2023-6-30 13:23
  • 签到天数: 105 天

    [LV.6]常住居民II

    发表于 2023-3-27 08:30:09 | 显示全部楼层 |阅读模式

    缘起[url]http://www.yxfzedu.com/HOOK[/url]的相关问题可以到网站了解下,我们是业内领域专业的平台,您如果有需要可以咨询,相信可以帮到您,值得您的信赖![align=center]http://resources.yxfzedu.com/images/other_images/jing_video.png[/align]
    前几天,在加班赶进度时遇到了一个意想不到的崩溃。由于是新加的代码导致的问题,所以很就定位到了问题代码。但是,看了好几遍也没看出问题在哪虽然代码在逻辑上有漏洞——某些情况下没有返回值,但是在我的认知里,应该不会导致崩溃。本文记录了使用IDA静态分析反汇编代码定位这个问题的过程。


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







    1


    2


    3


    4


    5


    6


    7


    8


    9























































    #""


    #






    CP


    {


    :




    ;



    ::V;


    };





    CPGP(

    )


    {



    CP;




    =
    ;



    V
    =
    L
    ""
    ;




    (
    =
    =
    0
    )



    {




    ;



    }


    }






    _(

    ,_TCHAR
    *
    [])


    {



    CP
    =
    GP(
    1
    );




    0
    ;


    }





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





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


    残酷的崩溃
    在中按F5调试启动,情的中断下来了。入下图:











    惊不惊喜,意不意外





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


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





    CPGP(),在IDA中看到的却是CP*__GP(CP*,)。如下图:











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


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











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


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











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



    这个问题比较先是在上发现的,我还以为是的,于是试了、、,发现都会崩溃。但是每个版本的都会给出一个警告:C47:'GP':。











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



    不知道大家是否还记得我之前分享过的一个宝藏址(),可以查看各种编译器对同一段代码的编译结果。下图是52中GP()函数的反汇编代码。











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





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





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











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



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


    简单搜了一下,平台64应用程序的调用约定还真是这样的,具体可以参考这篇文章7737885。






    综析,同样的代码在52中的结果是正确的。





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


    翻看标准
    在-JTC1SCWG上找到了++标准的草稿。我参考的版本是N32。这个是版的草稿。上的原话是



    A






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








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


    参考资料
    N32-JTC1SCWG42





    调用约定7737885。





    查看反汇编代码的宝藏址
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Archiver|山东001在线 ( ICP11027147 )

    GMT+8, 2026-4-4 13:42 , Processed in 0.038343 second(s), 19 queries , Gzip On.

    Powered by Discuz! X3.4

    © 2001-2023 Discuz! Team.

    快速回复 返回顶部 返回列表