首页
社区
课程
招聘
关于结构体版本不同导致的问题
发表于: 2023-8-29 17:43 3653

关于结构体版本不同导致的问题

2023-8-29 17:43
3653

遇到的问题

最近在工作的时候需要将一个dxf文件读取用到的一个曲线处理的库移植到mac上去,这个库主要使用了开源库nurbs++,它是一个非常古老的库,vc6可以顺利编译通过,mac上使用clang编译器的会有很多语法不支持,错误一大堆非常难改,我就在网上找了个vs2008可以编译通过的,然后在升级到vs2015版本的,这样要改动的地方就非常少了,然后拿到mac上去又修改了几百处错误,终于编译出了动态库,但是这时调用了我的库程序就崩溃了,因为我这个库是一个中间的,是由我们的软件主程序调用 ReadApi.dll ,ReadApi 里边再调用 我的这个曲线处理库 BezierProcess.dll , 我这里只有主程序和BezierProcess的代码,调用我这个库的 ReadApi 的代码我是没有的。

解决问题的过程

然后我又将mac下编译通过的代码拿到win下边进行编译调试,我这个库里边就一个函数, 图片描述
参数一是传入的一个dxf数据,参数二是一个线段结构体是一个输出参数,是由外边new的一个结构体,类似于如图的结构体 图片描述我这里边要做的就是将dxf的数据转换成SMidShape,这里边的指针都是new出来的,在使用SMidShape结构的时候是通过判断 pPrev== pNext 的情况下表示结束,在我的代码中下断点观察两个入参应该都是没有问题的,在我这个函数的处理过程经过单步调试也没出现崩溃,在通过观察ppShape结构中的所有值都应该是正常的,但是只要出了我的函数返回到ReadApi中的时候程序必定崩溃,这时我有点怀疑是不是ReadApi中出了问题,但是我把BezierProcess.dll替换成以前正常的就没问题,只要使用我新编译的必出现崩溃,跟同事要来了ReadApi的pdb,还是没有源码,这时候我经过调试,发现在从BezierProcess返回到ReadApi中的时候,在反汇编中看到的崩溃确实是在ReadApi中使用SMidShape时出现访问无效内存或者空指针的现象,
在观察ReadApi反汇编中的SMidShape结构的时候发现pNext指针为空或者无效,而我通过调试发现我的代码在给 **ppShape 赋值的时候是没问题的,pPrev和pNext的值都是相同的,内存也是有效的,当返回到ReadApi中的时候pNext的值要么为空要么就是一个随机的值,导致程序崩溃,这时就有点晕了,**ppShape 是由ReadApi 传入进来的,我里边赋值也是正常的,返回后ReadApi 中也是直接使用,没有进行修改的,pNext怎么就会变了呢,通了调试看了好久也没发现是哪里修改了pNext,最后我通过取 *ppShape的地址,pShape 取地址后就变成3级指针了,在变量监视窗口中将三级指针的地址使用(SMidShape)0x015a0738 来解析,观察pNext的变化,观察到每次从BezierProcess返回到ReadApi 的时候 pNext就被改变了,在多次观察后发现变量监视窗口好像有跳动一下,感觉有些奇怪,然后我就使用截图工具截取了在BezierProcess 赋值后,还没返回之前SMidShape的结构数据,函数返回到ReadApi 的时候在截了一次 SMidShape的数据,通过对比发现 返回后的SMidShape中多了一个pData成员,在pPrev的上边,导致pNext被挤下去了,导致它变成随机值了,由此判断应该是BezierProcess中和ReadApi中使用的SMidShape的结构不一致导致的,将pData加入后果然问题解决。

模拟复现

1
2
以下是模拟ReadApi的代码
main.cpp
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <Windows.h>
#include <iostream>
 
#include "test.h"
 
typedef ITestApi* (__cdecl* API_NEW_INTERFACE)();
typedef void(__cdecl* API_RELEASE_INTERFACE)(ITestApi* pTestApi);
 
 
 
int main()
{
 
    char buffer[MAX_PATH];
    GetModuleFileNameA(NULL, buffer, MAX_PATH);
 
    // 提取目录部分
    std::string strCurrentDir = buffer;
    size_t pos = strCurrentDir.find_last_of("\\/");
    if (pos != std::string::npos)
    {
        strCurrentDir = strCurrentDir.substr(0, pos);
    }
 
    API_NEW_INTERFACE     pfnNewInterface     = nullptr;
    API_RELEASE_INTERFACE pfnReleaseInterface = nullptr;
 
    strCurrentDir += "\\TestDll.dll";
 
    std::cout << "filename: " << strCurrentDir.c_str() << std::endl;
 
    HMODULE hDll = LoadLibraryA(strCurrentDir.c_str());
    ITestApi* pTestApi = nullptr;
    if (hDll)
    {
        pfnNewInterface      = (API_NEW_INTERFACE)GetProcAddress(hDll, "NewInterface");
        pfnReleaseInterface  = (API_RELEASE_INTERFACE)GetProcAddress(hDll, "ReleaseInterface");
        pTestApi             = pfnNewInterface();
 
        if (pTestApi)
        {
            CTest* pTest = new CTest();
            pTestApi->GetData(&pTest);
 
 
            if (pTest->pNext)
            {
                std::cout << "pPrev: "<<pTest->pPrev << std::endl;
                std::cout << "pNext: "<<pTest->pNext << std::endl;
            }
        }
    }
    else
    {
        std::cout << "动态库加载失败:Error: " << GetLastError()<<std::endl;
    }
 
    if (pfnReleaseInterface && pTestApi)
    {
        pfnReleaseInterface(pTestApi);
    }
 
    system("pause");
}

test.h 代码,这里边定义了类似SMidShape的结构,是最新完整的代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#ifndef TEST_H
#define TEST_H
 
typedef struct CTest
{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
 
    int* p;
 
    void*  pData;
 
    CTest* pPrev;
    CTest* pNext;
 
    CTest()
    {
        a = 1;
        b = 2;
        c = 3;
        d = 4;
        e = 5;
        f = 6;
        g = 7;
        h = 8;
 
        pData = nullptr;
         
        p     = nullptr;
        pPrev = nullptr;
        pNext = nullptr;
    }
 
}*PTest;
 
class ITestApi
{
public:
    ITestApi() {}
    virtual ~ITestApi() {}
 
    virtual void GetData(CTest** ppTest) = 0;
};
 
#endif // TEST_H

接下来的模拟BezierProcess.dll的代码,由CTestIns.h,CTestIns.cpp
和test.h组成,这里的test.h 和前边的不一样,结构中缺少一个pData的
CTestIns.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef TESTINS_H
#define TESTINS_H
 
#include "test.h"
 
class CTestIns :public ITestApi
{
public:
    CTestIns();
    ~CTestIns();
 
    virtual void GetData(CTest** ppTest);
};
 
#endif // TESTINS_H

CTestIns.cpp

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
27
28
29
30
31
32
33
34
35
#include "CTestIns.h"
 
CTestIns::CTestIns()
{
 
}
 
CTestIns::~CTestIns()
{
 
}
 
void CTestIns::GetData(CTest** ppTest)
{
    CTest* pTest = new CTest;
    *ppTest      = pTest;
 
    pTest->p     = (int*)0x12345678;
    pTest->pPrev = (CTest*)0xABCDEF00;
    pTest->pNext = (CTest*)0xAAAAAAAA;
 
}
 
 
extern "C" __declspec(dllexport) ITestApi * NewInterface()
{
    return new CTestIns;
}
extern "C" __declspec(dllexport) void ReleaseInterface(ITestApi * pInstance)
{
    if (pInstance)
    {
        delete pInstance;
    }
}

test.h

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#ifndef TEST_H
#define TEST_H
 
typedef struct CTest
{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
 
    int* p;
 
    CTest* pPrev;
    CTest* pNext;
 
    CTest()
    {
        a = 1;
        b = 2;
        c = 3;
        d = 4;
        e = 5;
        f = 6;
        g = 7;
        h = 8;
        
        p     = nullptr;
        pPrev = nullptr;
        pNext = nullptr;
    }
 
}*PTest;
 
class ITestApi
{
public:
    ITestApi() {}
    virtual ~ITestApi() {}
 
    virtual void GetData(CTest** ppTest) = 0;
};
 
#endif // TEST_H

然后我们使用vs建立一个新工程TestMain将模拟ReadApi的代码main.cpp和test.h包含进去,在项目属性设置c++中优化开启调试信息,链接器,调试中开启调试,设置完成后进行编译,编译完成后将 TestMain 工程的文件夹重命名一下,防止调试的时候进入源码了,这样才好模拟无源码的情况。 新建一个工程TestDll,模拟BezierProcess 的代码新建一个工程使用类似的设置,在常规属性中改成 动态库,编译完成后,将重命名后的 TestMain\Release 文件夹下边的TestMain.exe和TestMain.pdb拷贝到 Test\Release下边的文件夹中,在TestDll 工程属性设置中的调试 命令改为 刚才拷贝过来的 TestMain.exe的路径(D:\test\TestDll\Release\TestMain.exe)
,接下来就可以进行调试分析了,首先在void CTestIns::GetData(CTest** ppTest) 函数的最后一行下断点 图片描述断点下这里,在监视窗口中取ppTest的地址&ppTest 在使用(CTest***)解析取出的地址 (CTest***)0x12345678,然后再变量窗口观察数据是我们赋值的数据,将变量窗口中的数据截图保存,
图片描述
现在按F10让函数执行返回,将变量窗口截图和之前的进行对比 图片描述,发现pNext已经不是之前的0xaaaaaaaa了,通过观察发现pData占用了pPrev的值,pPrev占用了pNext的值,而pNext被挤下来了,所以成了一个无效的值,所以就出现了开头描述的问题,本来pPrev和pNext应该是相同的值,因为结构体不一致导致程序崩溃,问题也比较难查。

问题总结

像这类问题在多人开发过程中应该可能会遇到的,有可能是给别人写的库或者因为其他原因,是没有源码的,类似的调试手段还是有很大作用的


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-8-29 18:42 被yu781129965编辑 ,原因: 修改一下代码排版问题
收藏
免费 2
支持
分享
最新回复 (2)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
带虚函数的类的数据结构中会增加一个虚函数表指针
2023-8-29 17:53
0
雪    币: 203
活跃值: (1139)
能力值: ( LV9,RANK:195 )
在线值:
发帖
回帖
粉丝
3
楼上说的对,不知楼主是否知道虚表的概念
2023-8-31 15:09
0
游客
登录 | 注册 方可回帖
返回
//