最近在工作中被结构体对齐问题坑了一天的时间,郁闷的不行不行,特别记录下来,以供大家参考。
事情是这样的,因业务需要增加了一个结构体,里面用到了信号量,当时写完联调的时候只测试了windows平台,因为win32/linux代码几乎一样,就没测试linux平台,可后来linux平台居然出现了莫名其妙的问题,在调用sem_timedwait等待信号量的时候直接就返回了,还没报错。
好了,我先将简化后的代码整理如下:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <string>
#if defined _WIN32 || defined _WIN64
#include <windows.h>
#else
#include <semaphore.h>
#endif
#define TEST_PACK
#define MY_PRINTF(arg) printf("%s\t = %p, sizeof = %lu\n", #arg, &arg, sizeof(arg))
#ifdef TEST_PACK
#pragma pack(1)
#endif
struct obj_releaslive_meeting
{
obj_releaslive_meeting()
{
#if defined _WIN32 || defined _WIN64
sigStart = CreateEvent(NULL, false, NULL, NULL);
sigStop = CreateEvent(NULL, false, NULL, NULL);
#else
sem_init(&sigStart, 0, 0);
sem_init(&sigStop, 0, 0);
#endif
devNo = 0;
meetingId = "";
startErrCode = -1;
stopErrCode = -1;
}
~obj_releaslive_meeting()
{
#if defined _WIN32 || defined _WIN64
if (sigStart)
{
CloseHandle(sigStart);
sigStart = NULL;
}
if (sigStop)
{
CloseHandle(sigStop);
sigStop = NULL;
}
#else
sem_destroy(&sigStart);
sem_destroy(&sigStop);
#endif
}
uint16_t devNo;
std::string meetingId;
#if defined _WIN32 || defined _WIN64
HANDLE sigStart;
HANDLE sigStop;
#else
sem_t sigStart;
sem_t sigStop;
#endif
int startErrCode;
int stopErrCode;
};
#ifdef TEST_PACK
#pragma pack()
#endif
int main()
{
struct obj_releaslive_meeting *ptr = new obj_releaslive_meeting();
MY_PRINTF(ptr->devNo);
MY_PRINTF(ptr->meetingId);
MY_PRINTF(ptr->sigStart);
MY_PRINTF(ptr->sigStop);
MY_PRINTF(ptr->startErrCode);
MY_PRINTF(ptr->stopErrCode);
#if defined _WIN32 || defined _WIN64
int ret = WaitForSingleObject(ptr->sigStart, 5000);
#else
int ret = sem_wait(&ptr->sigStart);
#endif
printf("ret = %d, error = %s\n", ret, strerror(errno));
return 0;
}
然后我们分别看一下win32和linux平台的情况,简单说明一下,代码里使用TEST_PACK宏控制是结构体默认对齐还是按1字节对齐。
当时找了好久才发现,是别人的一个头文件里只写了#pragma pack(1),后面却没写#pragma pack(),然后他的头文件在某个地方又出现在我定义结构体的头文件前面了,所以影响到了我的结构体的对齐方式。
1.默认结构体对齐方式
1.1 Linux平台(x86-64, gcc version 7.4.0)
编译后执行,结果如预期一样可以被阻塞住:
0 14:49:07 ~/test $ ./a.out
ptr->devNo = 0x7fffd02cbe70, sizeof = 2
ptr->meetingId = 0x7fffd02cbe78, sizeof = 32
ptr->sigStart = 0x7fffd02cbe98, sizeof = 32
ptr->sigStop = 0x7fffd02cbeb8, sizeof = 32
ptr->startErrCode = 0x7fffd02cbed8, sizeof = 4
ptr->stopErrCode = 0x7fffd02cbedc, sizeof = 4
^C
1.2 Win32平台(x86, vs2013)
编译的时候,需要在工程配置 -> C/C++ -> 代码生成 -> 安全检查 选项中设置禁用安全检查功能。结果同样正确:
ptr->devNo = 00C74A38, sizeof = 2
ptr->meetingId = 00C74A3C, sizeof = 28
ptr->sigStart = 00C74A58, sizeof = 4
ptr->sigStop = 00C74A5C, sizeof = 4
ptr->startErrCode = 00C74A60, sizeof = 4
ptr->stopErrCode = 00C74A64, sizeof = 4
ret = 258, error = No error
2.按1字节对齐方式
2.1 Linux平台
在gcc version 7.4.0(ubuntu1~18.04)下,程序收到来自系统的SIGABRT信号,异常退出:
0 15:14:15 ~/test $ ./a.out
ptr->devNo = 0x7fffda944e70, sizeof = 2
ptr->meetingId = 0x7fffda944e72, sizeof = 32
ptr->sigStart = 0x7fffda944e92, sizeof = 32
ptr->sigStop = 0x7fffda944eb2, sizeof = 32
ptr->startErrCode = 0x7fffda944ed2, sizeof = 4
ptr->stopErrCode = 0x7fffda944ed6, sizeof = 4
The futex facility returned an unexpected error code.Aborted (core dumped)
在gcc version 4.8.5 20150623 (Red Hat 4.8.5-36)下,程序在调用sem_wait时候失败返回-1,提示无效的参数:
[root@localhost /]# ./test3
ptr->devNo = 0x23c8010, sizeof = 2
ptr->meetingId = 0x23c8012, sizeof = 8
ptr->sigStart = 0x23c801a, sizeof = 32
ptr->sigStop = 0x23c803a, sizeof = 32
ptr->startErrCode = 0x23c805a, sizeof = 4
ptr->stopErrCode = 0x23c805e, sizeof = 4
ret = -1, error = Invalid argument
- 在Linux系统异常的真实原因其实是信号量地址未对齐,可以对比下自然对齐的sigStart地址0x7fffd02cbe98和强制1字节对齐的0x7fffda944e92。
- 在gcc version 4.8.5版本下,实际工程的现象是调用sem_wait直接返回,并且无任何错误。
- 另外,可以看到这两个平台的string实现机制不一样,在gcc 7.4.0版本占用32个字节,而在gcc 4.8.5版本只占用了一个同机器字长的指针大小。
2.2 Win32平台(x86, vs2013)
32位结果正常:
ptr->devNo = 00164A38, sizeof = 2
ptr->meetingId = 00164A3A, sizeof = 28
ptr->sigStart = 00164A56, sizeof = 4
ptr->sigStop = 00164A5A, sizeof = 4
ptr->startErrCode = 00164A5E, sizeof = 4
ptr->stopErrCode = 00164A62, sizeof = 4
ret = 258, error = No error
64位结果同样正常:
ptr->devNo = 0000019FF0101CC0, sizeof = 2
ptr->meetingId = 0000019FF0101CC2, sizeof = 40
ptr->sigStart = 0000019FF0101CEA, sizeof = 8
ptr->sigStop = 0000019FF0101CF2, sizeof = 8
ptr->startErrCode = 0000019FF0101CFA, sizeof = 4
ptr->stopErrCode = 0000019FF0101CFE, sizeof = 4
ret = 258, error = No error
总结
先看一下结构体为什么要对齐:
结构体对齐的原因:
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈
由此说明,不同平台对于结构体不对齐的情况,处理方式是不一样的。
对于Linux平台一个信号量占用32个字节,如果出现不对齐的情况,系统可能出于操作效率或者安全的考虑,直接就不支持/不允许这种操作,所以结果肯定不是正确的,但对于不同内核版本系统做出的行为也不太一样,如果像第一种情况直接收到SIGABRT还好,能够及时发现问题并改正,但如果是第二种情况并且sem_wait没有报错,这就给问题排查带来了很大的困难。
对于Win32平台虽然运行结果正确,但我认为这并不能说明其就能正确的处理结构体不对齐的情况,因为Win32的信号量只有4/8个字节,不足够长,还不能完全说明问题。
PS:最后再说一遍#pragma pack(1),#pragma pack()预处理宏一定要成对使用,否则莫名其妙的影响了别人,真的很不好发现。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)