原创 字节对齐与内存访问

2013-2-20 20:22 1125 14 21 分类: 消费电子

关于字节对齐,最早是在《高质量程序设计》中看到的,当时明白一点,就是因为定义的数据字节大小不一(1字节,2字节,4字节,8字节),在内存中可能会有字节对齐的操作,就是数据在内存中的排放,不一定是连续的。因此也产生了一个疑问,就是对于数组而言,特别是结构体,如果不连续排放了。那么,当我们将结构体,当做连续的一段内存来访问时,

是不是会造成错误呢?
 
当时,虽然有疑惑,但是一直没有特别明白的理解。
后来以为是在操作系统上,因为虚拟内存的缘故,虽然物理内存的存储不连续,但是经过映射还是会有连续的虚拟内存地址,这样访问的时候,这些操作,由操作系统帮我们修正了。
 
但是,如果把这种情况放在单片机上呢?怎么处理?于是,没有了答案。
以至于,很长一段时间,不想使用结构体,因为担心字节对齐造成错误。
 
后来一个同事说,结构体成员在内存中的排放是连续的。
而且在CSAPP上也看到了,The implementation of structures is similar that of arrays in that all of the components of a structure are stored in a contiguous region of memory.
 
但是有一点还是不解,
既然有对齐,而结构体成员有连续存放,那么中间的操作时怎么完成的。
就像上边提到的,一开始以为是由操作系统的虚拟内存机制做了,那么单片机上呢?
 
直到,注意到CSAPP上这句话,
As these examples show, the selection of the different fields of a structure is handled completely at compile time.
即,结构体成员的访问。
由于汇编语言没有数据类型的概念,那么在翻译成汇编文件时,一定是没有数据类型的,所以一定是编译器操作在汇编时,完成了这些操作。
 
在实现字节对齐之后,
 
结构体成员在内存中的访问,
共用体成员在内存中的访问,
程序中变量的访问,
 
都已经由编译器把偏移后的地址结算好了。
 
应该说,这里至少访问是没有问题了,但是有一个问题来了。既然,结构体是在由内存中的连续区域实现的,在保证了对齐后,如果在将这个结构体当做一个数组来访问,会出现什么情况呢?
 
当然,在Linux,Windows等操作系统上,通过虚拟内存机制,能够形成连续的虚拟内存。
但是在单片机上呢?
单片机上使用的都是实地址,当然现在慢慢都有MPU对于内存有了保护。
 
那么对于单片机上的C程序中的结构体的连续访问会出现什么问题呢?
 
struct S1{
int i;
char c;
int    j;
};
 
0    4 5      9
+----+-+----+
|   i  |c| j    |
+----+-+----+
 
0     4 5      8    12 
+----+-+---+----+
|   i  |c|    |  j  |
+----+-+---+----+
 
明天去测试下,
在CM3上。
 
 
 
在编程时,不同体系结构的CPU对于字节对齐都有一定的要求。
对于ARM7而言,如果是不对其访问,就会造成读取数据失败,引起异常。
对于有些CPU特别是RICS架构,不支持非对齐访问。
ARM7要求是4bytes对齐。
 
Intel虽然,非对齐访问不会出错,但是会造成执行效率的降低。
加入对齐访问可以由一次内存访问,则非对齐访问,需要两个内存访问才能完成。
因此它推荐采用字节对齐,以提升内存系统的性能。
 
struct testStruct{
uint8_t  i;
uint32_t j;
uint8_t  l;
uint32_t k;
};
 
memset在Keil下的汇编语句
 
   148:         memset(&a, 0x63, sizeof(struct testStruct));
0x08009176 2263      MOVS     r2,#0x63
0x08009178 2110      MOVS     r1,#0x10
0x0800917A A806      ADD      r0,sp,#0x18
0x0800917C F7F7FD38  BL.W     __aeabi_memset (0x08000BF0)
将memset会变为STR语句在这里是4bytes对齐的。
 
struct testStruct{
uint8_t  i;
uint32_t j;
uint8_t  l;
uint32_t k;
};
     volatile uint32_t t1;
     volatile uint8_t t2;
     volatile uint8_t p[20];
 
memset(&a, 0x63, sizeof(struct testStruct));
     a.i = sizeof(struct testStruct);
     a.j = 0x68;
     a.l = 0x14;
     a.k = 0x81;
     a = a;
     memcpy(p, (void*)&a, a.i);
     memcpy(&b, (void *)&a, a.i);
     volatile struct testStruct a;
     struct testStruct b;
 
在单片机中,不能直接将结构体按照连续内存排列来理解。
否则,会出现一项不不到的问题。
 
/*----------------------------------------------------------------------------------------------------------*/
 struct Str{
  5     unsigned int  j;
  6     unsigned int  k;
  7     unsigned char l;                                                                 
  8     unsigned char i;
  9 };
10 struct Str a;
11 unsigned char p[20];
 
(gdb) p &p
$1 = (unsigned char (*)[20]) 0x804971c
(gdb) x /32 0x804971c
0x804971c

:     0x00000000     0x00000000     0x00000000     0x00000000
0x804972c

:     0x00000000     0x00000000     0x00000000     0x00000000
0x804973c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804974c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804975c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804976c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804977c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804978c:     0x00000000     0x00000000     0x00000000     0x00000000
(gdb) s
18          j = sizeof(struct Str);
(gdb) p j
$2 = 0 '\0'
(gdb) s
19          a.i = 0x12;
(gdb) p j
$3 = 12 '\f'
(gdb) s
20          a.j = 0x21;
(gdb) 
21          a.l = 0x68;
(gdb) 
23          memcpy(p, &a, j);
(gdb) 
24          for(i = 0; i < j; i++ )
(gdb) x /32 0x804971c
0x804971c

+16>

:     0x00000021     0x63636363     0x63631268     0x00000000
0x804972c

:     0x00000000     0x00000021     0x63636363     0x63631268
0x804973c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804974c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804975c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804976c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804977c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804978c:     0x00000000     0x00000000     0x00000000     0x00000000

+16>
 
/*----------------------------------------------------------------------------------------------------------*/
 
 
4 struct Str{
  5     unsigned char i;
  6     unsigned int  j;
  7     unsigned char l;                                                                 
  8     unsigned int  k;
  9 };
10 struct Str a; 
11 unsigned char p[20];
 
(gdb) p &p
$1 = (unsigned char (*)[20]) 0x804971c
(gdb) x /32 0x804971c
0x804971c

:     0x00000000     0x00000000     0x00000000     0x00000000
0x804972c

:     0x00000000     0x00000000     0x00000000     0x00000000
0x804973c :     0x00000000     0x00000000     0x00000000     0x00000000
0x804974c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804975c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804976c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804977c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804978c:     0x00000000     0x00000000     0x00000000     0x00000000
(gdb) s
18          j = sizeof(struct Str);
(gdb) p j
$2 = 0 '\0'
(gdb) s
19          a.i = 0x12;
(gdb) p j
$3 = 16 '\020'
(gdb) s
20          a.j = 0x21;
(gdb) 
21          a.l = 0x68;
(gdb) 
23          memcpy(p, &a, j);
(gdb) 
24          for(i = 0; i < j; i++ )
(gdb) 
26               printf("%2x ", p);
(gdb) 
24          for(i = 0; i < j; i++ )
(gdb) x /32 0x804971c
0x804971c
+12>

+16>

:     0x63636312     0x00000021     0x63636368     0x63636363
0x804972c

:     0x00000000     0x63636312     0x00000021     0x63636368
0x804973c :     0x63636363     0x00000000     0x00000000     0x00000000
0x804974c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804975c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804976c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804977c:     0x00000000     0x00000000     0x00000000     0x00000000
0x804978c:     0x00000000     0x00000000     0x00000000     0x00000000
(gdb) 
+12>

+16>
/*----------------------------------------------------------------------------------------------------------*/
经测试,即使是在Linux系统上,结构体也存在字节对齐问题,这个要注意。
不能轻易将一个结构体,按照连续的内存来转换。
除非我们已经十分清楚,结构体在内存中的对齐方式。
 

文章评论7条评论)

登录后参与讨论

1989tie_959541171 2013-2-26 13:39

呵呵,我想说的是按照CPU架构要求的对齐方式去设计结构体,即先遵循CPU compiler要求,而不是C的通用标准。如果这样做不行,再去考虑优化,使用C通用的标准,让CPU compiler帮助我们实现要求。 谢谢你精彩的回复。

1989tie_959541171 2013-2-26 13:21

谢谢你的分享,谢谢你的祝福!

allen_zhan_752827529 2013-2-25 12:12

是的, 如果project 中 size 和 speed 都在计划范围内, 那么, 讨论节省和优化就是下一个工作计划了. 祝你元宵佳节愉快. 你的同学毕业答辩顺利.

allen_zhan_752827529 2013-2-25 12:02

我猜测你这里的意思, 是指按照 C language 的结构体的一般访问方式去写"普通"的代码? 这样的确不能得到最大的速度和size. 两个方面都会损失. 我从 IAR 的 IDE 中自带的 document 中读到的. 不仅仅如此, 我还自己写了 access function, 验证了这个 size 上的损失. 大致上我们写了一个最终几十k 的 bin 文件(请注意已经最高size优化), 但是放弃了 C compiler 中的一般访问 member 的用法, 而是自己用 offset 去 access member. 最终我们获得了超过 1.5k 的size 的节省(我印象中), 速度我们没有验证. -- 说给你共享```

1989tie_959541171 2013-2-21 20:53

感觉,先遵循CPU的特性来写程序,以得到最大化的性能(执行速度)。如果实在避不开,再来想解决之道。

1989tie_959541171 2013-2-21 20:21

这个确实是自己写的,只是昨天才放到这里。关于这个问题,是一个同学让我帮她看业设的代码时,想到有这个潜在的bug。如果处理不好,有出错的可能。所以想了下,正好当时看CSAPP,结合以前的《高质量程序设计》想了下。并在电脑和Cortex-M3核上,测试了下。发现确实存在问题。不过没有像你那样去深入解决这个问题,而是想办法避开了这个问题。这点应该向你学习学习。 关于你写的那两个博客都看了,分析的很深入,考虑的很多。更适合特定的情景使用。这这种方式需要设计者关注定义structure时,要想到访问的函数,增加了程序的复杂度。即一种structure需要对应一种访问函数。 因为程序是写给人看的,增加复杂度就降低了可读性。造成阅读与维护的困难。 当然,首先是保证程序运行正常,其次再考虑别的。

allen_zhan_752827529 2013-2-21 16:29

无意中浏览到楼主的帖子, 是否你的原创? 你也曾忧心过结构体的 pad bytes beause of unaligned 现象? ; P 我曾创作过一篇 blog 对使用 IAR compiler 编译 arm7 中的结构体访问, 做了一个很详细的讨论, 你可以参考下. 基本上, 使用C languare 的做用法, 你能够自如 access any struction you defined. 但是正如你担心的, compiler 喜欢被对齐的结构体成员. 所以 compiler 自己做了很多工作. 这种工作往往是巨大的 code size 和 speed 上的双重浪费. 让我假设你在为 mcu 缺乏 enough code memory 发愁, 那么你可以借鉴我的方法来做. - Allen Zhan http://forum.eet-cn.com/BLOG_ARTICLE_3846.HTM
相关推荐阅读
catch2000 2015-07-19 11:44
信号线小电阻的作用
在一块新的PCB上,测试系统能否正常运行的时候,发现系统上电后没有正常启动。  系统框图如下:   在上电的时刻,CPU A(GPIO电平2.6V)会向串口发送启动日志数据,CPU A启动后,...
catch2000 2015-07-05 17:04
协议设计中ACK机制的影响
在TCP/IP中,延时ACK和Nagle算法。  TCP为了同时处理成块数据(通常为512字节的用户数据)和交互数据(通常用户数据比较少,例如不大于10个字节),采用了延时ACK和Nagle算法...
catch2000 2015-05-23 15:48
话说物联网操作系统
最近好多家都宣布推出自己的物联网操作系统。   1. Google将要在Google I/O大会发布的Brillo; 2. 三星推出的Artik芯片搭载Mentor Graphics的...
catch2000 2015-03-31 23:52
不要采用异或来交换两个变量
在进行两个变量的时候,经常会看到有些书误人子弟的推荐使用异或的方式: 方式一 {   x = x ^ y;   y = x ^ y;   x = x ^ y; } 而不是...
catch2000 2014-10-09 07:28
为什么要测试先行
在产品的研发过程中,测试一项至关重要。不论是软件还是硬件。   软件的测试先行,在研发过程中,就做到质量的保证,因为在出现Bug的时候,容易定位Bug,而且即使是在客户端出现Bug,也能够...
catch2000 2014-10-09 07:26
C语言的面向对象编程(一)
一、前言 对于编程而言,重要的是解决问题的方式,而不是语言本身。面向对象与面向过程是解决问题和思考问题的方式。C语言常说是面向过程开发的语言,因为其缺少很多对于面向对象特性的支持。但,这并不影...
我要评论
7
14
关闭 站长推荐上一条 /2 下一条