继续上篇文章介绍的那个天气预报应用。天气预报信息从XML数据里解析出来后存在全局变量里面,这样一关机后这些天气信息就都丢失了。客户要求这些天气信息能够保存,这样关机后信息就不会丢失。于是很自然的我想到了使用NVRAM来保存得到的天气数据。添加NV数据块后发现程序在模拟器上表现正常,而在真机上会死机重启。经过重重排查发现此问题是因为VC编译器和ARM编译器内存对齐方式所引起的。
1. 数据结构
我们先看天气信息的数据结构。
typedef struct _accu_forecast_
{
kal_char obsdate[15]; //预报日期
S16 daytime_hightemperature; //白天最高气温
S16 daytime_lowtemperature; //白天最低气温
U8 daytime_weather_icon;
S16 nighttime_hightemperature; //晚上最高气温
S16 nighttime_lowtemperature; //晚上最低气温
U8 nighttime_weather_icon;
}accu_forecast;
typedef struct _accu_weather_
{
MMI_BOOL updated; // 标识变量,天气信息是否已更新
kal_char city[30]; //城市名
kal_char observationtime[10]; //获取信息的时间
S16 temperature; //温度
U8 weathericon; //当前天气图标
accu_forecast accu_forecast_info[FORECAST_DAY_TOTAL]; //预报天数里的天气信息
}accu_weather;
FORECAST_DAY_TOTAL是一个枚举值,值为5,accu_week_days是一个枚举。
2. 发现问题
添加NV数据块我们需要知道这个数据块所占字节的多少,也即确定确定下面代码中的NVRAM_EF_ACCU_WEATHER_INFO_SIZE值。开始的时候我在调试模式中通过使用sizeof(accu_weather)来得到数据块的大小为208。但使用这个值,在真机上却会死机重启。经过乱枪打鸟式地打trace终于发现程序挂在下面代码的return语句。该函数的作用是从NV中读取天气数据存放在变量NvramData中。
MMI_BOOL load_weather_info_from_nvram(void)
{
accu_weather NvramData;
S16 error;
ReadRecord(NVRAM_EF_ACCU_WEATHER_INFO_LID, 1, (void *)&NvramData, NVRAM_EF_ACCU_WEATHER_INFO_SIZE, &error);
if(0xff == NvramData.weathericon)//如果这个值为0xff的话,认为NV值未被初始化
{
return MMI_FALSE;
}
memcpy((void *)&g_cntx_accu.weather_info, (void *)&NvramData, NVRAM_EF_ACCU_WEATHER_INFO_SIZE);
return MMI_TRUE;
}
3. 寻找原因
返回调用函数的时候程序挂掉,这种问题一般是栈上的返回地址值被改变,程序返回的地址值是一个错误的值。但是这个返回值是怎么被破坏的呢?代码就那么几行,最大的嫌疑就是读NV的语句ReadRecord破坏了栈内容。会不会是Size的值不正确引起的?于是我用trace打印出sizeof(accu_weather)的值结果得到186(见图1)!NvramData只需要186个字节,但实际上写入了208个字节,这样栈内容就被破坏了。
4. 分析原因
sizeof(accu_weather)的值在VC编译器上得到的结果是208,在ARM编译器上得到的结果是186,为什么会有这差别?为了找寻答案我先分析sizeof(accu_forecast)的值。结果在VC编译器上sizeof(accu_forecast)的值为32,但是数来数去我觉得这个结构体只占29字节。问部门老员工,这才知道原来这里是采用了内存对齐所引起的。
网上查了下VC下结构体和类的默认对齐规则:
(1)各成员变量存放的起始地址相对于结构的起始地址偏移量必须为该变量类型所占用字节的倍数。
(2)结构体总大小必须为结构体的字节边界数(结构体中占用最大空间的类型所占字节数)的倍数。
如果采用紧凑内存存放的话,accu_forecast值为29,根据规则(2)accu_forecast结构体字节边界数为4,采用内存对齐的话,结构体占用空间32字节。在accu_forecast中添加一个double类型的值,再计算sizeof(accu_forecast)的话,值为40,符合规则(2)。
我们再看ARM编译器的内存对齐。我们先在trace中打印各种类型占用字节数的大小,结果见图1,其中ACCU_MON是一个枚举值。我们可以看到ARM编译器上布尔类型和枚举类型只占用1个字节,这跟VC编译器(占4个字节)差别很大。
图1 ARM编译器上各数据类型大小及accu_weather中各数据在内存中地址
采用紧凑内存计算的话sizeof(accu_forecast)值应该是26,但内存对齐后这个值为28,那内存是怎样对齐的呢。我们先打印出accu_weather中各个变量的起始地址,见图1。从图中可以看出0xF1B9A294到0xF1B9A295刚好1个字节,用于存放布尔变量; 0xF1B9A295到0xF1B9A2B3刚好30个字节用于存放city数组;0xF1B9A2B3到0xF1B9A2BE有11个字节,但声明的observationtime数组只有10个字节,这是怎么回事?原来接下来的数据成员是一个S16型变量,根据规则(1)该变量的内存的起始地址应该是个偶数,这样计算结构体大小的时候便多出了一个字节。
由此可见,改变结构体内数据摆放的位置,可以减小结构体所占用内存的大小。若在两个S16变量间再插入一个U类型的变量(见图2),那么结构体大小应该是增加2而不是1,若将两个U8类型变量摆放在一起(见图3),那么结构体大小可以减少2字节占用。图1证实了我们的猜想。
图2 两个S16变量间插入一个U8变量 图3 两U8变量紧邻摆放
5. 小结
VC编译器和ARM编译器数据类型所占字节大小有些许差别,VC编译器一般是四字节对齐而ARM编译器一般是双字节对齐,当然对齐方式在编译阶段是可以指定的。不管采用什么样的对齐方式,我们在添加NVRAM数据块的时候最好通过串口或者trace用真机打印出结构体的大小,这样便不会出现各种莫名其妙的问题了。