结构体对齐的重要性

  最近在工作中被结构体对齐问题坑了一天的时间,郁闷的不行不行,特别记录下来,以供大家参考。
   事情是这样的,因业务需要增加了一个结构体,里面用到了信号量,当时写完联调的时候只测试了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

  1. 在Linux系统异常的真实原因其实是信号量地址未对齐,可以对比下自然对齐的sigStart地址0x7fffd02cbe98和强制1字节对齐的0x7fffda944e92。
  2. 在gcc version 4.8.5版本下,实际工程的现象是调用sem_wait直接返回,并且无任何错误。
  3. 另外,可以看到这两个平台的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()预处理宏一定要成对使用,否则莫名其妙的影响了别人,真的很不好发现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值