分享
 
 
 

亲身经历SOLARIS下的内存Alignment错误

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

前一阵子在做一个程序模块,其基本功能是读取一个指定格式的文件,然后将文件中描速的内容组织成内存中的数据结构,在这个开发任务中,我们只需要生成内存数据就可以了,数据如何处理则是由其它小组来完成,因此数据结构也是由对方来定义的。经过一系列的设计、开发、测试,我们顺利的完成了模块在Windows上的开发工作,并且也由其它小组整合进了他们的程序之中。可是,不久前却传来Bug票,说我们的模块在Solaris下一运行就导致整个程序崩溃,可能将影响整个软件的按时发布。。。。为此作为这个模块的主要设计与开发人员,我迅速着手调试这个模块,可惜由于我们单位无法搭建出Solaris下的程序调试环境,所以整个调试过程只能在Windows下进行,然后再发送到Solaris上编译,最后执行程序观察其效果。经过仔细的调查,我确定程序从逻辑上没有任何问题,但是确实程序在Solaris上会崩溃,这可怎么办呢,后来询问了一位C++高手,才大致确定这属于程序的内存对齐(alignment)上的错误,然后,经过近一周的反复修改,我们才最终得以解决这个bug,下面就简要的介绍一下这个Bug的始末吧。

首先,我们要建立的数据是一套用struct定义的层层展开的树状结构,假设有如下几种类型(实际系统要复杂一些,为了说明方便,就简化一下了):

typedef struct DB {

Module** moduleList; /* Dynamic list of Modules */

} DB;

typedef struct Module {

struct Inst** instList; /* Dynamic list of Instances */

} Module;

typedef struct Inst {

char* name; /* Instance name */

} Inst;

它们的实际结构关系如下图:

正如你所看到的,最终的结果数据将有一个总的DB节点,它的moduleList将指向一连串Module节点,而它不是直接指向Module节点的,而是通过一个Module*的数组,再间接的找到真是的Module节点的。同样Module中有instList,它可以用来指定一连串挂在Module下的Inst,它们也是通过一个Inst*的数组来间接关联起来的。显而易见,上述使用的两个数组都必须是在连续空间内的,但是由于整个内存DB都是通过读取文件内容动态建立起来的,所以实际程序怎么知道自己的moduleList和instList中到底包含了多少个数据呢,答案就是Module*数组(和Inst*数组)上面的int length数据了,它放在moduleList指针所指向内存的上面,用一个int型的数值来表示数组的长度。当需要遍历DB下的moduleList时,就只需要根据moduleList指针,到上面一块内存地址中拿到数组长度,然后就可以象访问一般数组那样访问到各个Module了。说到这里,实在不得不佩服设计者构思的巧妙,大家知道在列表访问中,数组的速度是最快的,但是数组的最大缺点则是无法动态增加数据(除非用realloc()来重新分配内存),而使用了现在的结构,则将动态的数据组织成了数组的形式,对于数据的访问将是非常高效的。

但是这种结构却对我们的生成数据的模块提出了很大的难题,我们不可能知道实际文件中放了多少数据,如果按照一般做类似系统的经验,读到一条数据追加一条,那么反复的内存分配肯定是避免不了的,所以,经过研究,我们决定采用这样的方法:预先简单的遍历一遍整个文件,统计各个类型数据的总个数(还好,一般是每种类型一行文本,统计还是很方便的),然后分配所有需要的内存,接着再从头开始读文件,边读文件,边从预先分配的内存中分配空间给需要生成的对象。

整个处理过程还是比较简单的,假设我们已经有了DB节点,初始状态下,它的moduleList指针为NULL,我们预先分配的给Module用的内存块称为ModuleBlock(为byte*类型),同时有一个ModuleBlockCur(为int型)用来标明,当前这块内存被分配到什么位置了,同样的有InstBlock和InstBlockCur。当需要向其moduleList中追加一个Inst时,函数会做以下工作:

1)判断moduleList指针是否为空,若为空,则知道这是该数组中的第一个数据,那么将ModuleBlock的当前位置先划分出一个int的空间,对其赋上数值零,然后改变moduleList指针,然其指向int型空间后的位置。

代码如下:

if (*pModuleList == NULL) { //create an new module list

int *length = (int*)( ModuleBlock + ModuleBlockCur);

*length = 0;

mCurDBMem->ModuleBlockCur += sizeof(int);

M_ASSERT(mCurDBMem->ModuleBlockCur <= mCurDBMem->ModuleBlockSize);

*pModuleList = (Module**)(mCurDBMem->ModuleBlock + mCurDBMem->ModuleBlockCur);

}

这样,就完成了moduleList第一次分配时初始化的功能。

2)用moduleList指针(经过步骤1,moduleList指针一定是一个有效地址了)向上移动一个sizeof(int)的长度,就能取到当前数组的长度,对其数值加1,同时从ModuleBlock的当前位置开始,划分出一个sizeof(Module*)长度的空间,作为Module*,这样相当于增加了数组的长度。

3)分配一个Module类型的内存,将刚才新分配的Module指针指向该Module。这样就完成了一次分配Module,并加入列表的功能。

以上两步的代码为:

int *length = (int*)((T_INT8*)*pModuleList - sizeof(int)); //get the modulelist's length

Module** ppmodule = (Module**)( ModuleBlock + ModuleBlockCur); //get new memory address

ModuleBlockCur += sizeof(Module*);

M_ASSERT(ModuleBlockCur <= ModuleBlockSize);

*length += 1;

*ppmodule = (Module*)AllocMem(sizeof(Module)); //allocate module

其中AllocMem的功能可以看作和malloc()一样,只不过为了系统的性能,我们从采用了自己管理内存块列表的方法,来处理这些零散的内存,这里就不详细介绍了。

通过以上这些步骤,就可以将所有的Module*分配在一段连续的内存中了,同样Module中的Inst也用类似的方法建立在连续的内存中,速度上也不会有大的损失。开发完成后,我们就将模块交付给了对方,并且通过了各种测试(当然,只是Windows平台下的),证明这样的做法是相当健壮并且高效的。

但是,当Solaris下程序崩溃的问题被发现后,经过了这次长达一周的研究后的今天,我才意识到,当初其实范了相当严重的错误:

我以上的各种处理,都是建立在Windows平台上的,由于Windows平台是32位的操作系统,其数据指针的长度为4Byte,而int型数值的长度也为4Byte,那么最终我所生成的moduleList在内存中的分配将是这样的(假设moduleList指向的内存地址为0x0004):

可以看到,int形的length字段占据的空间正好和指针型的Module*字段占据的空间大小相同,这只是个巧合,如果不是用int型作为length的类型,而是用byte型,假设moduleList的地址还是0x0004,那么length的起始地址就变为0x0003了,在Windows系统下,这不会有错,当然,这归功于Windows平台强大的兼容性。

再来看一下Solaris下的内存分布情况,我们使用的是64位的Solaris系统,该系统下,int型的长度还是4byte,但是指针的长度为16byte(注意!!),那么经过我以上程序的处理后,内存将变为这样(还是假设moduleList的地址为0x0004):

内存分布确实不同了(红色标出了不同的内存地址),但是这应该是很正常的事啊,至少做惯了Windows下开发的我但是是这样想的。。。。但是很抱歉,这样的内存分布,在Solaris系统上就会引发Alignment错误,简单的来说,Solaris系统为了保证芯片运算的高效,所有指针都必须在16的整数倍空间地址内(除非经过特殊的指令),否则当你想访问该指针指向的数据时,就会引起访问错误,一般情况下,在solaris系统下用malloc分配的空间都会在16的整数倍地址上,我们以上分配的ModuleBlock也是这样,但是当ModuleBlock中被挖掉一块,用来分配给int型的length后,紧接着分配给Module*的地址则显然不是16的整数倍了,而在此时,我们只是使用了一个强制类型转换,就将指针指到了对Solaris系统来说非法的地址上去了,因此,当后续的代码要访问该内存时,程序崩溃了。。。。

知道了问题,解决方法当然也不难,我们只要保证Module*被分配到16的整数倍内存地址上就可以了,好,经过修改,我们的程序做了如下改动:

1)我们把ModuleBlock该成了Module**类型,ModuleBlockCur不再用来表示内存偏移量,而是用来表示当前被分配掉多少个Module*空间(这些修改其实与功能无关,但是反而是代码更清晰了,这也算重构吧)。每当需要分配新的ModuleList,我们不是分配一个int型的空间,而是分配一个Module*类型的空间(Module*为指针型,在Solaris平台下,它比int型的4Byte要大,在Windows平台下,则正好和int型大小相同)。

2)原先的使用的AllocMem()函数(虽然没有详细介绍,但其实它会把整个程序需要的各种零散的struct和char*字符串都用同一块预先准备好的内存来分配,char*字符串是不定长的,为它分配了内存的结果就是当给struct分配内存时,它有可能没有被分配在16的整数倍内存地址上,这也会导致Alignment错误,因此,为了各类struct的内存分配,由使用了另一个函数AllocStructMem()来专门负责分配struct的内存,因为没有使用紧凑struct的编译指令,所以每个struct的长度也都是16的整数倍,它将不会影响下一个struct的分配。)修改为使用AllocStructMem()函数了。

3)把函数改成了template,以便多个程序段调用,代码变成了这样:

template <class TYPE>

TYPE* AllocGeneralMem(TYPE** pBlock, size_t &pBlockCur, size_t pBlockSize, TYPE **&pList)

{

if (pList == NULL) {

TYPE** pplength = pBlock + pBlockCur;

memset(pplength, 0, sizeof(TYPE*));

pBlockCur ++;

M_ASSERT(pBlockCur <= pBlockSize);

pList = pBlock + pBlockCur;

}

char *pplength = ((char*)pList) - sizeof(int);

int length;

memcpy(&length, pplength, sizeof(int));

length++;

memcpy(pplength, &length, sizeof(int));

TYPE **ppdata = pBlock + pBlockCur;

pBlockCur ++;

M_ASSERT(pBlockCur <= pBlockSize);

*ppdata = (TYPE*)AllocStructMem(sizeof(TYPE));

return *ppdata;

}

给Module分配内存时就这样:

Module* pmodule = AllocGeneralMem(ModuleBlock, ModuleBlockCur, ModuleBlockSize, pModuleList);

M_ASSERT(pmodule);

好了,到Solaris下编译,运行,程序不再崩溃了(当时,在折腾了近4天后第一次看到程序运行后不崩溃了,我可是兴奋得差点从椅子上掉下来啊!!)。

但是,似乎还是不太正确,因为同样的测试文件,在Windows下应该会有图形会显示出来,但是在Solaris下却什么都没有显示出来。唯一的可能是我们生成的DB内存数据在被外部程序读取时,没有找到应有的数据,怎么会呢,只好去看看外部程序是怎么干的,在代码中好一阵狂找,忽然发现在对方给我们的数据定义文件中有这么两个宏:

/* ===========================================================================

* Dynamic list

*

* Struct members named "*List" point to a list of objects plus the

* list length (before the first array element). All Lists must be built up

* like AnyList.

* Eventually we should check the pointer offset with:

* assert( (((char*)NULL)-((char*)&(((struct AnyList*)NULL)->entry))) %

* sizeof(int) == 0 )

* ===========================================================================

*/

struct AnyList {

int length;

void* entry[1];

};

#define INTOFF (((int*)NULL)-((int*)&(((struct AnyList*)NULL)->entry)))

#define zListLength(list) (((int*)(list))[INTOFF])

天啊,真是太绝了!对方其实早就有了数据对齐方面的考虑,因此写了这两个宏,而它们的使用将不会有任何平台方面的差异,只可惜,我一直到现在才发现这两个宝贝宏。。。我来简单的分析一下:

struct AnyList,它有两个字段:length和entry,length其实就是我们ModuleList(InstList)指向内存的上方的length数据,entry是void指针,由于这里没有用任何编译指令,因此编译器编译时,都会根据系统将struct中的每一个字段对齐到内存中能被快速访问的地址上,在Windows上,默认是8Byte的,也就是说,length占据4Byte,接着会有4Byte空闲不用的空间,紧跟着再是entry。而在Solaris下,length同样4Byte,而对齐方式是16Byte的,因此接着会有12Byte的空闲空间,再接着才是entry。这样的struct定义,在不同的系统上就将有不同的大小,而length和entry之间的距离也将是不同的。

接着的一个宏:INTOFF,通过对同一个数据(NULL)强制转换为struct AnyList后的length和entry的地址空间的相减,可以知道在当前系统下,它们的间距。

最后一个宏:zListLength,则可以对指定的List,通过INTOFF计算出的偏移量,往上找到int型length的所在地址,并取出其中的数据,使用这个宏就可以计算出List的长度了。

而我的代码,在需要计算List长度的地方,则是这样写的:

int *length = (int*)((byte*)*ModuleList - sizeof(int));

看出什么区别了吗?。。。对的,在Windows下,这两种取Length的方法得到的结果是相同的,但是在Solaris下则变成了这样:

我的程序从图中的橘黄色地址存放和取得length,而对方的程序则从图中的绿色地址取得length,其结果当然是对方的程序认为length为0拉(我对整个内存清零了,否则取到的将是不确定的数值)。唉,不得不佩服对方的设计人员。

好了,继续修改代码,变成这样:

template <class TYPE>

TYPE* AllocGeneralMem(TYPE** pBlock, size_t &pBlockCur, size_t pBlockSize, TYPE **&pList)

{

if (pList == NULL) {

pBlockCur ++;

M_ASSERT(pBlockCur <= pBlockSize);

pList = pBlock + pBlockCur;

int *plength = &(((int*)(pList))[INTOFF]);

*plength = 0;

}

int *plength = &(((int*)(pList))[INTOFF]);

M_ASSERT(zListLength(pList) == *plength);

*plength = *plength + 1;

TYPE **ppdata = pBlock + pBlockCur;

pBlockCur ++;

M_ASSERT(pBlockCur <= pBlockSize);

*ppdata = (TYPE*)AllocStructMem(sizeof(TYPE));

return *ppdata;

}

Module* pmodule = AllocGeneralMem(ModuleBlock, ModuleBlockCur, ModuleBlockSize, pModuleList);

M_ASSERT(pmodule);

这里还直接用到了上面的INTOFF宏,用来取得length的所在地址。再一次到solaris上编译,运行。。。。呵呵,结果总算完全正确了!

经过这整整一周的Bug修改,一方面学到了不少编译器和操作系统方面的知识,另一方面也磨练了在shell下闭着眼睛就能打出程序完整路径的绝活,呵呵,果然收获不少。也再一次意识到自己其实在技术上要学的还太多太多。。。。

欢迎访问图克斯软件:

http://www.tonixsoft.com

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有