V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
acros
V2EX  ›  C

VS2013 - C++碰到一个诡异的内存访问错误,很怀疑是 VS 的问题,要被逼疯了···求提点 T_T

  •  
  •   acros · 2015-07-26 14:33:20 +08:00 · 2062 次点击
    这是一个创建于 3407 天前的主题,其中的信息可能已经有所发展或是发生改变。
    先交代下环境吧。个人在做一个游戏的小功能(算是练习吧),用的cocos2d-x引擎,前些天从3.6版本升级到3.7(引擎内部好像有内存管理相关的改动),出现了一个奇怪的bug。
    平台是win7 64 + VS 2013 community updater 5。 工程target都是x86 machine

    下午再去xcode下搭建一个试试去,看看是不是ide的问题。

    就是这个
    https://github.com/acros/cocos2dx_qte

    问题是这样的:
    cocos2d-x里有个类叫Scene,我派生了class MainGameScene : public Scene,派生类中成员变量就加了两个RefPtr指针mUiLayer和mGameLayer。(注:RefPtr是cocos2d-x一个轻量级的shared_ptr类似存在)

    通过VS的工具输出,确认Scene和MainGameScene的内存布局如下(这里就给出类末尾一段内容):
    1> 708 | | _physicsWorld
    1> 712 | | _navMesh
    1> 716 | | _navMeshDebugCamera
    1> | +---
    1> 720 | ?$RefPtr@VLayer@cocos2d@@ mGameLayer
    1> 724 | ?$RefPtr@VLayer@cocos2d@@ mUiLayer
    1> +---

    _navMesh和_navMeshDebugCamera是Scene类最后两个成员变量。
    720偏移之前都是Scene的,这个表是正确的。

    这个工程里面,cocos2d-x引擎部分是导出成一个dll的,然后主项目去加载它,MainGameScene这个类是在我自己的项目里面创建的。然而将一个指向MainGameScene的Scene*类型传到引擎代码中时,对于scene*内存的读取就发生了诡异的变化!
    见下图:


    可以看到:scene*的地址是0x060BBC88,加上前面计算的720偏移量,0x060BBF58地址应该是mGameLayer的地址,实际这时通过scene*访问到_navMesh变量时,取到的是mGameLayer地址!!!

    图中下方窗口还可以看到,我加了个类型转换后再访问,地址就对了····>_<

    (*((qte.exe!cocos2d::Scene*)(&(*((qte.exe!MainGameScene*)(scene))))))._navMesh
    和&(scene)->_navMesh 取到的地址差 8 ??????

    看起来虚表并没有被破坏掉,而且编译器计算类内成员offset时,这个offset怎么会算错,个人完全没头绪啊····(类成员地址不是类地址+offset计算的吗?)

    注:scene*指针只在项目dll工程里面访问才会出现这个错误的偏移计算,我之前想过是不是项目配置错了,但问题是,什么东西配置错了会导致这种错误呢?!
    第 1 条附言  ·  2015-07-26 22:07:39 +08:00
    确认xcode下也是一样的问题,洗清VS的嫌疑了。
    不过xcode的debug显示_navMesh = null,然而
    在 if(_navMesh) { do.. sth..}语句中还是进去了。

    唔····估计得查查C++文档了我去····
    第 2 条附言  ·  2015-07-28 00:43:46 +08:00
    @forcecharlie
    @secondwtq
    @mljack
    @xdeng
    @bombless
    @endrollex
    @lcsky
    @jukka
    @XiXiLocked
    @finab

    谢谢各位的建议,查了两天终于查到了。
    把类内成员的offset在DLL内外输出了一遍,终于看到原因了····
    事因非常蛋疼,是cocos2d-x生成的项目配置问题,特地生成了个新项目看了下。

    3.7版本里面加了个宏:CC_ENABLE_BULLET_INTEGRATION=1
    这是在cocos2d-x DLL项目的Presscessor Definition中配置的(不是直接写在头文件里),但是主项目中并没有继承这个宏,导致引用DLL时,主项目编译的头文件跳过了CC_ENABLE_BULLET_INTEGRATION相关的两个指针成员变量(就是8byte差别的来源了),所以DLL外进行Scene类内寻址改成员之后的变量全部出错。
    修改方法就是在主项目预定义宏里面加上CC_ENABLE_BULLET_INTEGRATION=1·····解决问题了。
    顺带在git上给cocos2d-x的同学写了个issue,估计他们会马上改过来吧。

    汗颜一个先。
    26 条回复    2015-07-27 23:58:15 +08:00
    jukka
        1
    jukka  
       2015-07-26 17:23:30 +08:00
    show me your code.
    XiXiLocked
        2
    XiXiLocked  
       2015-07-26 17:26:01 +08:00   ❤️ 1
    你看直接读navMesh是有值的,但是指针转换之后读的navMesh是NULL,说明转的有问题
    信口开河一把,
    scene的内存布局是
    scene::vptr
    scene::members ...
    MainGameScene的里面有并不继承于scene的虚函数,所以他的虚表插在了前面,于是布局大概是这样
    scene:vptr
    MainGameScene::vptr <--这里多了虚表指针的8
    scene::members ...
    MainGameScene::members ...
    上面说的没有把握,但是你可以看看之前的成员变量的地址验证一下
    acros
        3
    acros  
    OP
       2015-07-26 17:39:13 +08:00 via iPhone
    @jukka 上面git地址就是啊。
    acros
        4
    acros  
    OP
       2015-07-26 17:40:06 +08:00 via iPhone
    @XiXiLocked
    _navMesh应该是空的。mGaneLayer不是null
    acros
        5
    acros  
    OP
       2015-07-26 17:40:50 +08:00 via iPhone
    @XiXiLocked 我去验证下..
    finab
        6
    finab  
       2015-07-26 17:56:03 +08:00
    代码出问题
    我从不怀疑编译器或IDE,肯定是我的问题!
    就是这么自信 ~~~
    fo2w
        7
    fo2w  
       2015-07-26 18:04:52 +08:00
    虽然说vs确实有bug, 但我不相信大多数人碰得到
    Athrob
        8
    Athrob  
       2015-07-26 18:07:04 +08:00
    确定不是代码的问题?我相信vs
    nozama
        9
    nozama  
       2015-07-26 18:56:03 +08:00 via Android
    vs2012 bug像这种“明明是对的,结果就是不对”倒是遇到过几次,在2013和appcode都没问题。也有几次,是变量忘了初始化引起的bug,clang会自动初始化为0,VC却是随机值。
    lcsky
        10
    lcsky  
       2015-07-26 19:05:38 +08:00   ❤️ 1
    可能是遗漏了某些概念,我随便拍拍脑袋能想到相关的可能有:
    1、struct、class成员的默认内存对齐方式(#pragma pack)
    2、不清楚C++其实是没有ABI兼容性的

    后面这个问题会导致不同的C++编译器、编译器版本、甚至只是不同的编译参数生成的二进制代码不兼容。你看系统库都是纯C接口的发现没有?因为纯C有函数调用的ABI兼容性标准:stdcall、cdecl、fastcall。而C++是彻底没有ABI兼容性的,能“差不多对上”只是运气好,或者编译器版本、参数完全一样。

    所以重点恐怕是检查你dll和主程序的编译参数有何区别
    acros
        11
    acros  
    OP
       2015-07-26 19:14:52 +08:00 via iPhone
    @Athrob
    @nozama
    @fo2w
    我也认为代码出错可能性更大。 一般代码灵异问题都是内存初始化或者溢出一类导致的,这次是摸不着头脑了....从内存布局上看,没有被覆盖的问题....回头再查查vs类底层虚表有没错误,哎,没学好汇编和编译原理,到底层就非常吃力。
    husinhu
        12
    husinhu  
       2015-07-26 20:59:46 +08:00   ❤️ 1
    同意@lcksy 如果真是ABI不同或者编译选项不同,建议同一编译选项,加一层SceneAdaptor来decouple
    lingo233
        13
    lingo233  
       2015-07-26 21:06:32 +08:00
    我受不了了明明2015已经发布了竟然不升级,要被逼疯了(╯-_-)╯╧╧
    nozama
        14
    nozama  
       2015-07-26 21:29:20 +08:00   ❤️ 1
    也许还有一种可能, 也是vs2012的bug: 某个源文件会莫名其妙被copy一份, 然后无论怎么修改, 编译的仍是原来的那个文件.
    于是就可能出现: 头文件更新了, dll其实还是以前的, 然后按照偏移量访问某个字段, 实际上是访问到了另一个字段.
    endrollex
        15
    endrollex  
       2015-07-26 22:09:46 +08:00   ❤️ 1
    http://blog.csdn.net/smstong/article/details/24455371
    如果C++编译器不能根据类声明推算出类型转换时的指针调整方式时,如果使用了强制类型转换,那么编译器只是简单的默默无作为
    bombless
        16
    bombless  
       2015-07-26 23:18:23 +08:00   ❤️ 1
    以前见到有个人有类似的问题,貌似是同一个变量名在不同的命名空间用上了,然后链接的时候出错(或者是调试器显示的有问题,忘记具体情况了)
    mljack
        17
    mljack  
       2015-07-26 23:25:22 +08:00   ❤️ 1
    检查下dll和主程序的工程设置中预编译宏是否一致,感觉像 #ifdef xxx 造成的问题,c/c++编译器对于一些.h 和.lib里的class是否一致并不做完整的检查。

    仔细看了一下,应该是编译 dll 时没定义CC_USE_NAVMESH,而使用 dll 时又定义了CC_USE_NAVMESH造成的。
    tushiner
        18
    tushiner  
       2015-07-26 23:29:28 +08:00
    我开发的时候也是用网易云音乐听歌
    acros
        19
    acros  
    OP
       2015-07-26 23:29:33 +08:00
    @mljack 这个我确认过了。如果CC_USE_NAVMESH关闭掉,读取错误的地址还是偏移8(这时候就是_navMesh前面的变量读取错误了),应该不是具体变量造成的。
    xdeng
        20
    xdeng  
       2015-07-26 23:31:09 +08:00   ❤️ 1
    有个东西 叫 字节对齐
    mljack
        21
    mljack  
       2015-07-27 00:06:22 +08:00   ❤️ 1
    在主程序和dll中分别把 每个scene成员的偏移都printf出来比比,应该就能看出来了
    #define offs(s,m) (size_t)&(((s *)0)->m)

    不行就合并到一个工程看看还有问题没
    secondwtq
        22
    secondwtq  
       2015-07-27 03:26:06 +08:00   ❤️ 1
    我个人倾向于@mljack 的观点。我最近恰好被这个问题坑得很惨(因为库和客户程序都是 Xcode 下类似的配置编译,所以并没有对齐问题,但是细节上有出入)。

    我并不是非常熟悉调试的那一套理论(尤其是 VS 下面调试),我认为楼主可以尝试探索周围的成员变量地址,看看到哪里的时候两边的 offset 是一致的。

    从截图中可以注意到:

    `(*((qte.exe!cocos2d::Scene*)(&(*((qte.exe!MainGameScene*)(scene))))))._navMesh`,使用的应该是客户程序的偏移量。

    `&(scene)->_navMesh` 貌似使用的是 dll 里的偏移量。这个调试信息有没有保存我并不清楚,是不是跟图中现在貌似正处于 dll 中代码的 context 下也不清楚。
    forcecharlie
        23
    forcecharlie  
       2015-07-27 10:20:34 +08:00
    @nozama 构建系统一般只会检测 源文件的时间戳,然而,不一定检测头文件的时间戳,只有当 目标的时间戳比依赖的时间戳旧时,构建才会再一次发生,重新生成不是这样。
    secondwtq
        24
    secondwtq  
       2015-07-27 14:56:48 +08:00
    @forcecharlie 此话怎讲?一般情况下,某个 Compilation Unit (.c/cpp) 所 include 的 header 应该算是其依赖吧,那么这些 header 中再次引入的其他 header 算不算呢?如果先改动了 header 再改动 cpp(这样 cpp 的时间戳是最新的),是否会触发编译呢?
    forcecharlie
        25
    forcecharlie  
       2015-07-27 22:53:55 +08:00
    @secondwtq 构建文件并没有解析 include 的能力,以 Makefile 为例,最好在依赖中显式的指明 头文件,如果一个构建系统并没有将头文件的添加到依赖,那么头文件的修改也不会有新的编译响应,当然,一些构建系统会根据文件 Hash 来检测,这些就需要存储到一个数据库文件或者是缓存文件。改动了 cpp 基本上都会触发编译。

    Hello.obj:Hello.cpp Hello.h
    cl -c Hello.cpp
    secondwtq
        26
    secondwtq  
       2015-07-27 23:58:15 +08:00
    @forcecharlie 这倒是,Makefile 这种比较直接的底层工具不会去检测这种特定的东西。

    高级的工具貌似有专门去解决,我一般用 CMake,在 generate 的过程中会提取出 header 的依赖(据说是根据编译器提供的信息)。Xcode 也会自动触发编译,VS 大概不会没有。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2765 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 11:44 · PVG 19:44 · LAX 03:44 · JFK 06:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.