我的C语言学习进阶之旅关于C/C++内存对齐读取文件产生的问题以及解决方法

Posted 欧阳鹏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的C语言学习进阶之旅关于C/C++内存对齐读取文件产生的问题以及解决方法相关的知识,希望对你有一定的参考价值。

一、问题描述

今天在使用OpenGL ES 加载一个 TGA 图片文件的时候,出现了加载失败的问题。

关于什么是TGA文件以及如何打开TGA文件?
可以参考我的博客:【我的OpenGL学习进阶之旅】什么是TGA文件以及如何打开TGA文件?

如下图所示,没有texture加载进来,黑黢黢的页面。

查看日志打印,发现加载tga图片失败,如下所示:

2021-11-25 15:42:31.690 6385-6548/com.oyp.openglesdemo I/NDK_JNI_LOG_TAG:
 [GLUtils.cpp][loadTgaTexture][240]: Error loading (texture/heightmap.tga) image.

关于日志打印的内容带有文件文件名、方法名、行号 等信息的实现方法
可以参考我的博客 【我的Android进阶之旅】NDK开发之在C++代码中使用Android Log打印日志,打印内容带有文件文件名、方法名、行号 等信息,方便定位日志输出的地方

二、分析问题

2.1 断点调试

  • loadTgaTexture 函数
    loadTgaTexture 函数代码如下:
//
// Load texture from disk
//
GLuint GLUtils::loadTgaTexture (const char *fileName )

	int width, height;
	char *buffer = esLoadTGA (fileName, &width, &height );
	GLuint texId;

	if ( buffer == nullptr )
	
		LOGI ( "Error loading (%s) image.\\n", fileName )
		return 0;
	

	glGenTextures ( 1, &texId );
	glBindTexture ( GL_TEXTURE_2D, texId );

	glTexImage2D ( GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, buffer );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

	free ( buffer );

	return texId;

可以发现,因为loadTgaTexture函数调用esLoadTGA函数返回的buffernullptr 所以报错了。

  • esLoadTGA 函数

esLoadTGA 函数代码如下:

//
// esLoadTGA()
//
//    Loads a 8-bit, 24-bit or 32-bit TGA image from a file
//
char * esLoadTGA (const char *fileName, int *width, int *height )

	char        *buffer;
	esFile      *fp;
	TGA_HEADER   Header;
	int          bytesRead;

	// Open the file for reading
	fp = esFileOpen (fileName );

	if ( fp == nullptr )
	
		// Log error as 'error in opening the input file from apk'
		LOGE ( "esLoadTGA FAILED to load :  %s \\n", fileName )
		return nullptr;
	
    LOGD ( "sizeof ( TGA_HEADER ) :  %d \\n", sizeof ( TGA_HEADER ) )
	bytesRead = esFileRead ( fp, sizeof ( TGA_HEADER ), &Header );

	*width = Header.Width;
	*height = Header.Height;

	if ( Header.ColorDepth == 8 ||
		 Header.ColorDepth == 24 || Header.ColorDepth == 32 )
	
		int bytesToRead = sizeof ( char ) * ( *width ) * ( *height ) * Header.ColorDepth / 8;

		// Allocate the image data buffer
		buffer = ( char * ) malloc ( bytesToRead );

		if ( buffer )
		
			bytesRead = esFileRead ( fp, bytesToRead, buffer );
			esFileClose ( fp );

			return ( buffer );
		
	

	return ( nullptr );

我们在这个esLoadTGA 函数打断点,调试一下看看:


定位到问题,因为Header.ColorDepth87 'W',既不等于8,也不等于24,还不等于32,所以最终返回了nullptr

2.2 为啥呢?

原来产生这个问题的原因和C/C++内存对齐有关。这里,读者可以参考下面的文章了解。

下面摘这篇文章内容贴在本篇博客里:

2.2.1 什么是内存对齐

还是用一个例子带出这个问题,看下面的小程序,
理论上,32位系统下,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte
但是实际上,通过运行程序得到的结果是 8 byte,这就是内存对齐所导致的。

//32位系统
#include<stdio.h>
struct
    int x;
    char y;
s;

int main()

    printf("%d\\n",sizeof(s);  // 输出8
    return 0;

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

2.2.2 为什么要进行内存对齐

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度.

现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。

假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作.

现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

2.2.3 内存对齐规则

  • 对齐系数
    每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。
    gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。

  • 有效对其值
    有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位

了解了上面的概念后,我们现在可以来看看内存对齐需要遵循的规则:

  • (1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

  • (2) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

下面给出几个例子以便于理解:

//32位系统
#include<stdio.h>
struct

    int i;    
    char c1;  
    char c2;  
x1;

struct
    char c1;  
    int i;    
    char c2;  
x2;

struct
    char c1;  
    char c2; 
    int i;    
x3;

int main()

    printf("%d\\n",sizeof(x1));  // 输出8
    printf("%d\\n",sizeof(x2));  // 输出12
    printf("%d\\n",sizeof(x3));  // 输出8
    return 0;

以上测试都是在Linux环境下进行的,linux下默认#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以s2来分析其内存布局:

首先使用规则1,对成员变量进行对齐:

  • sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;

  • sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;

  • sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;

然后使用规则2,对结构体整体进行对齐:

s2中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。

因此整体也是按照4字节对齐。由规则1得到s2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:

2.2.3.1 #pragma pack(n)

不同平台上编译器的 pragma pack 默认值不同。而我们可以通过预编译命令#pragma pack(n), n= 1,2,4,8,16来改变对齐系数。

例如,对于上个例子的三个结构体,如果前面加上#pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是6字节。

如果前面加上#pragma pack(2)有效对齐值为2字节,此时根据对齐规则,三个结构体的大小应为6,8,6。内存分布图如下:

经过上面的实例分析,大家应该对内存对齐有了全面的认识和了解,在以后的编码中定义结构体时需要考虑成员变量定义的先后顺序了。

2.2.3.2参考资料:

http://light3moon.com/2015/01/19/[%E8%BD%AC]%20%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90/
更多文章见本文公众号:码农有道
版权所有!

2.2.4 本Demo的结构体内存对齐解析

我在esLoadTGA函数里面有两句代码,如下所示,在读取TGA文件之前,打印了TGA_HEADERsizeof

LOGD ( "sizeof ( TGA_HEADER ) :  %d \\n", sizeof ( TGA_HEADER ) )
bytesRead = esFileRead ( fp, sizeof ( TGA_HEADER ), &Header );


打印出来的sizeof 大小为20,所以导致了无法加载TGA。

2021-11-25 15:42:31.688 6385-6548/com.oyp.openglesdemo D/NDK_JNI_LOG_TAG: 
[GLUtils.cpp][esLoadTGA][203]: sizeof ( TGA_HEADER ) :  20 
2021-11-25 15:42:31.690 6385-6548/com.oyp.openglesdemo I/NDK_JNI_LOG_TAG: 
[GLUtils.cpp][loadTgaTexture][240]: Error loading (texture/heightmap.tga) image.
  • 结构体TGA_HEADER
    结构体 TGA_HEADER 的定义如下,因为C/C++内存对齐,导致TGA_HEADER 的size为20,所以不对劲,如下所示:
typedef struct

    unsigned char IdSize,
            MapType,
            ImageType;
    unsigned short PaletteStart,
            PaletteSize;
    unsigned char PaletteEntryDepth;
    unsigned short X,
            Y,
            Width,
            Height;
    unsigned char ColorDepth,
            Descriptor;
 TGA_HEADER;

前面的2.2.1 到 2.2.3 章节,我们可以大概得出下面的结论:
对齐规则:

  • 1.基本类型的对齐值就是其sizeof值;
  • 2.结构体的对齐值是其成员的最大对齐值;
  • 3.编译器可以设置一个最大对齐值,怎么类型的实际对齐值是该类型的对齐值与默认对齐值取最小值得来。

    所以如上图所示:

三、解决问题

3.1 使用 __attribute__ ( ( packed ) )

参考下面博客:

使用该属性对struct 或者union 类型进行定义,设定其类型的每一个变量的内存约束。就是告诉编译器取消结构在编译过程中的优化对齐(使用1字节对齐),按照实际占用字节数进行对齐,是GCC特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc编译器不是紧凑模式的

3.1.1 使用 __attribute__ ( ( packed ) ) 修改结构体

如下所示:

typedef struct
//  C语言__attribute__的使用  https://blog.csdn.net/qlexcel/article/details/92656797
//  使用该属性对struct 或者union 类型进行定义,设定其类型的每一个变量的内存约束。
//  就是告诉编译器取消结构在编译过程中的优化对齐(使用1字节对齐),按照实际占用字节数进行对齐,是GCC特有的语法。
//  这个功能是跟操作系统没关系,跟编译器有关,gcc编译器不是紧凑模式的
__attribute__ ( ( packed ) )

    unsigned char IdSize,
            MapType,
            ImageType;
    unsigned short PaletteStart,
            PaletteSize;
    unsigned char PaletteEntryDepth;
    unsigned short X,
            Y,
            Width,
            Height;
    unsigned char ColorDepth,
            Descriptor;
 TGA_HEADER;

3.1.2 修改后的效果

改完之后,重新执行,正常运行。

断点调试,发现 ColorDepth8 ,所以正常加载了 TGA 图片

实现的效果如下所示:

3.1.3 分析结构体

打印出来的 sizeof ( TGA_HEADER ) 为 18,所以加载TGA文件成功。

2021-11-25 16:03:30.881 7080-7188/com.oyp.openglesdemo D/NDK_JNI_LOG_TAG: 
[GLUtils.cpp][esLoadTGA][203]: sizeof ( TGA_HEADER ) :  18 


3.2 使用 #pragma pack(1)

参考下面两篇博客:

指定结构、联合和类成员的封装对齐。其实就是改变编译器的内存对齐方式。这个功能对于集合数据体使用,默认的数据的对齐方式占用内存比较大,可进行修改。

在没有参数的情况下调用pack会将n设置为编译器选项/zp中设置的值。如果未设置编译器选项,windows默认为8,linux默认为4。

具体的使用方法为,其中n的取值必须是2的幂次方,即1、2、4、8、16等:

1. #pragma pack(show)     以警告信息的形式显示当前字节对齐的值.
2. #pragma pack(n)        将当前字节对齐值设为 n .
3. #pragma pack()         将当前字节对齐值设为默认值(通常是8) .
4. #pragma pack(push)     将当前字节对齐值压入编译栈栈顶.
5. #pragma pack(pop)      将编译栈栈顶的字节对齐值弹出并设为当前值.
6. #pragma pack(push, n)  先将当前字节对齐值压入编译栈栈顶, 然后再将 n 设为当前值.
7. #pragma pack(pop, n)   将编译栈栈顶的字节对齐值弹出, 然后丢弃, 再将 n 设为当前值.

8. #pragma pack(push, identifier)        将当前字节对齐值压入编译栈栈顶, 然后将栈中保存该值的位置标识为 identifier .
10. #pragma pack(pop, identifier)        将编译栈栈中标识为 identifier 位置的值弹出, 并将其设为当前值. 注意, 如果栈中所标识的位置之上还有值, 那会先被弹出并丢弃.
11. #pragma pack(push, identifier, n)    将当前字节对齐值压入编译栈栈顶, 然后将栈中保存该值的位置标识为 identifier, 再将 n 设为当前值.
12. #pragma pack(pop, identifier, n)     将编译栈栈中标识为 identifier 位置的值弹出, 然后丢弃, 再将 n 设为当前值. 注意, 如果栈中所标识的位置之上还有值, 那会先被弹出并丢弃.
   
注意: 如果在栈中没有找到 pop 中的标识符, 则编译器忽略该指令, 而且不会弹出任何值.

使用时最好是成对出现的,要不容易引起错误,设置之后记得用完给恢复,如:

#pragma pack(n) 	//设置以n个字节为对齐长度
struct 

	int  ia;
 	char cb;

#pragma pack ()  	//弹出n个字节对齐长度,设置默认值为对齐长度

如果想给单独一个结构体设置对齐长度还可以使用C++ 11 标准中的alignas。

但是不是设置#pragma pack(n)就按照设置的字节进行对齐呢?其实并不是这样的,实际的对齐字节数要符合以下规则:
1、若设置了对齐长度n,实际对齐长度=Min(设置字节长度,结构体成员的最宽字节数)。若没有设置n,实际对齐长度 = Min(结构体成员的最宽字节数,默认对齐长度)。
2、每个成员相对于首地址的偏移量(offset)都是实际对齐长度的整数倍,若不满足编译器进行填充。
3、数据集合的总大小为实际对齐长度的整数倍,若不是编译器进行填充。

假设默认对齐为8时,看几个例子:

#pragma pack(n)
struct Stu1

	short  	sa;	//2个字节
	char 	cb;	//1个字节
	int		ic;	//4个字节
	char 	cd;	//1个字节

#pragma pack()

cout << sizeof(Stu1) << endl;
  • 若n为1:实际对齐长度为1 = Min(1,8)。这个就不用解释了,相当于各个元素相加,总长度为8。
  • 若n为2:实际对齐长度为2 = Min(2,8)。sa占两个字节,不需要补齐。cb首地址偏移为2个字节,满足规则二。ic的首地址偏移3(2+1)个字节,不能满足规则二,填充一个字节到4。cd的首地址偏移为8个字节,满足规则。现在相加的8+1=9个字节,不满足规则三,填充一个字节,总长度为10;
  • 若n为4:实际对齐长度为4 = Min(4,8)。这个就不用介绍了,默认时就用的这个。总长度为12。
  • 若n>4:实际对齐长度为 4 = Min(设置字节长度,结构体成员的最宽字节数)。总长度为12。

3.2.1 使用 #pragma pack(1) 修改代码

// 注意点:保证内存是连续的,不然读取错误  使用  #pragma pack(1)  或者  __attribute__ ( ( packed ) ) 都可以
// C/C++内存对齐详解  https://zhuanlan.zhihu.com/p/30007037
// #pragma的常用方法讲解   https://blog.csdn.net/weixin_39640298/article/details/84503428
#pragma pack(push,x1)      // Byte alignment (8-bit)
#pragma pack(1)           // 如果前面加上#pragma pack(1),那么此时有效对齐值为1字节
typedef struct

    unsigned char IdSize,
            MapType,
            ImageType;
    unsigned short PaletteStart,
            PaletteSize;
    unsigned char PaletteEntryDepth;
    unsigned short X,
            Y,
            Width,
            Height;
    unsigned char ColorDepth,
            Descriptor;
 TGA_HEADER;
#pragma pack(pop,x1)

3.2.2 修改后的效果

效果和 使用 __attribute__ ( ( packed ) ) 修改结构体 的一样,这里不再重复描述

3.2.3 分析结构体

打印出来的 sizeof ( TGA_HEADER ) 为 18,所以加载TGA文件成功。

2021-11-25 16:11:12.936 7408-7542/com.oyp.openglesdemo D/NDK_JNI_LOG_TAG:
 [GLUtils.cpp][esLoadTGA][198]: sizeof ( TGA_HEADER ) :  18 

四、参考链接

参考下面博客:

以上是关于我的C语言学习进阶之旅关于C/C++内存对齐读取文件产生的问题以及解决方法的主要内容,如果未能解决你的问题,请参考以下文章

我的OpenGL学习进阶之旅C++如何加载TGA文件?

我的C/C++语言学习进阶之旅收集关于MODERN C++ 11/14/17/20/23 的一些资料

我的C/C++语言学习进阶之旅收集关于MODERN C++ 11/14/17/20/23 的一些资料

我的C/C++语言学习进阶之旅关于Vcpkg的参考资料:一个让你可以快速引用C++开源库的包管理工具Vcpkg,快来使用Vcpkg管理你的C++ 项目中的依赖项吧!

我的C/C++语言学习进阶之旅关于Vcpkg的参考资料:一个让你可以快速引用C++开源库的包管理工具Vcpkg,快来使用Vcpkg管理你的C++ 项目中的依赖项吧!

我的C/C++语言学习进阶之旅关于Vcpkg的参考资料:一个让你可以快速引用C++开源库的包管理工具Vcpkg,快来使用Vcpkg管理你的C++ 项目中的依赖项吧!