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

前一阵子在做一个程序模块,其基本功能是读取一个指定格式的文件,然后将文件中描速的内容组织成内存中的数据结构,在这个开发任务中,我们只需要生成内存数据就可以了,数据如何处理则是由其它小组来完成,因此数据结构也是由对方来定义的。经过一系列的设计、开发、测试,我们顺利的完成了模块在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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值