请教一个内存对齐的问题

crimx 发布于 2014年03月26日
无人欣赏。

最近在看lcc的源码,书本说这里ap->avail = (char *)((union header *)ap + 1);是为了保证avail总是指向一个对齐后的地址。不明白。。。

header就是一个blockalign组成的联合体,align是宿主机上最小对齐字节数。不理解为什么跨过一个header的大小就对齐了?

/* 
* 内存块
* limit是可分配地址的终点,
* avail是可分配地址的起点,
* avail前的地址都是已经分配了的。
*/
struct block {
  struct block *next;
  char *limit;
  char *avail;
};

/* 代表最小对齐字节数 */
union align {
  long l;
  char *p;
  double d;
  int (*f) ARGS((void));
};

union header {
  struct block b;
  union align a;
};
共15条回复
tinyfool 回复于 2014年03月27日

应该是char或者void/char都有可能被分配到不对齐的地址上,但是long,double之类的不会,所以union align的起点应该不会在一个不对齐的地址。因为对齐这样cpu和寄存器才好对他们进行操作,所以,编译器会自动把他们对齐了。

然后union你要是不明白什么意思的话,我建议你先看基础的书,后看更复杂的书。

abutter 回复于 2014年03月27日

首先需要明白 ((union header *)ap + 1) 是怎么回事,就是将 ap 指针转换成 union header *,加 1 操作就是指针指向地址再增加一个单元的偏移。

对于 union,编译器会取其总大的单元来作为整体长度,其对齐的地址取决于所有成员及其子成员单个最大的长度。

RolandXu 回复于 2014年03月27日

没看过原文,从你的描述来看,书里的意思大概是利用C编译器的这样的一个特点:

void func(void) {
      char c = 'x';
      int *ip = (int *)&c; /* This can lose information */
      char *cp = (char *)ip;

      /* Will fail on some conforming implementations */
      assert(cp == &c);}

当执行了(union header *)ap这步之后,会把不对其的部分截断掉,然后加1保证最终指向一个有效的地址。

但是,请非常小心的使用这个特性,因为在C99的标准中,这种行为是一个undefined behavior。不保证其他C的编译器的实现也是这样的。 C99 6.3.2.3

A pointer to an object or incomplete type may be converted to a pointer to a different object or incomplete type. If the resulting pointer is not correctly aligned for the pointed-to type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.

可以参考这里Do not convert pointers into more strictly aligned pointer types

玉楼 回复于 2014年03月27日

首先,你要知道相关变量占内存的大小。以64位机举例:

size of long: 8
size of char*: 8
size of double: 8
size of struct block: 24
size of union align: 8
size of union header: 24

上面的内存长度单位都是字节,不同硬件和操作系统可能会有差异,32位机上的指针应该都是4字节的。

好了,知道长度就可以继续了。表达式ap->avail = (char *)((union header *)ap + 1);要分开来理解。

  1. ap->avail我认为ap是一个struct block*类型,但不管是什么类型,(union header *)ap这句都把它强制转换成了union header*这样一个指针类型;
  2. 当一个指针变量+1时,变量中的地址就会增加指针指向类型的长度。注意:这里不是指针变量本身所占内存长度,无论什么指针变量,它本身所占内存在64位机里永远是8字节。指针指向类型的长度是什么意思呢?就是说,char *指向的地址是以char为基本构成单元的,即每个单元占1字节;struct block*指向的地址是以struct block为基本构成单元的,即每单元占24字节。这样可知:

————

char            *p1 = 0x100000;
struct block    *p2 = 0x200000;

//
p1++;         // p1 == 0x100001
p2++;         // p2 == 0x200024
  1. 这回我们就知道了,在强制转换后,变量ap加1后,其内的地址值实际上是加了24。
  2. 至于再强制转换为char*是因为ap->avail的类型是char*
crimx 回复于 2014年03月27日

谢谢各位回答

1楼 @tinyfool 4楼 @玉楼 问题没问好,我应该说明自己的水平的。我了解union是什么,明白指针是怎么增加的,也理解它这里是用align做单位去对齐内存,只是不明白为什么开始的时候要跨过一个header的大小,而不是一个align,去保证对齐。

3楼 @RolandXu 为什么开始的时候要用header而不直接用align去对齐?

cnsoft 回复于 2014年03月27日

因为header 内存占用大? 对齐应该是划分内存. 用小的 没办法存储大的. 是这意思么

玉楼 回复于 2014年03月27日

6楼 @cnsoft 不是。这里的对齐应该是和业务有关的,你这段代码干啥的我不知道,但对这段代码而言union header占用内存大小与struct block是相同的。把一个struct block*强制转换为union header应该是出于业务的考虑。

很明显对于union header而言struct blockunion align都是必须的,我们现在可知struct block占用内存大于union align,如果将来再在union header需要增加一个占用内存大于struct blockstruct xxx怎么办?虽然这两个指针的作用现在一样,将来未必一样。这应该就是这段代码要说的业务逻辑,而和哪个指针对齐多大内存无关。

RolandXu 回复于 2014年03月27日

5楼 @crimx

按我的理解来假设一个例子来说说明这个问题。

  1. 首先这个世界上的处理器多种多样,编译器也多种多样。当写代码的时候是不能假设struct blockalign的对齐要求。这里我假设假设某个情况下struct block是4 bytes对齐,大小是12 bytes,而align是8 bytes对齐,大小为8 Bytes
  2. ap应该是一个指向struct block的指针,它应该满足4字节对齐,假设其值是0x01234564,它并不满足align的对齐要求。
  3. 现在ap->avail需要指向一块地址,需要满足struct block的对齐要求,还要满足处理器本身align的对齐要求。
  4. 于是,就定义了一个union header,由于union的特性,union header的对齐要求取的是两者里面大的,也就是union header是8 bytes对齐,大小为16bytes
  5. 然后表达式(union header *)ap产生了一个满足对齐要求的值,在我的例子里面假设这个值是0x01234560
  6. 但是0x01234560并没有指向一个有效的地址,这样的话还需要做一个+1的操作。使得ap->avail的值为0x01234570
  7. 同时,ap->avail是可分配地址的起点,也就是说从地址0x01234570开始的内存可能分配给别人使用。但是程序还需要一个struct block的结构来记录next availlimit。所以从ap指向的内存开始,需要预留一个struct block的大小,而ap->avail只能从+1的位置开始。

关于第4点,为什么union header的大小是16?假设一个union header的数组,如果大小不是16,如何能保证第二个元素也是8 bytes对齐。

关于第5点,为什么产生了0x01234560这个值,参考我前面的回复

crimx 回复于 2014年03月28日

8楼 @RolandXu 感谢回答!分析很到位,终于理解了,原来我是忽视了(union header *)ap的作用,认为它仅仅是为了预留一个struct block大小的空间而已,其实在这里它还利用强制转换去实现对齐了。而用union header是因为struct block也要对齐。

不知道 @RolandXu 有没有博客?

RolandXu 回复于 2014年03月28日

9楼 @crimx 没有博客,人懒,博客已经荒废N年了。

玉楼 回复于 2014年03月28日

8楼 @RolandXu 你用的啥CPU、编译器我不知道,至少你说的情况在Linux+gcc里不存在。

首先,在32位机里struct blockunion alignunion header的长度分别为12、8、12,而不会是你说的12、8、16。 其次,在C语言中,对指针的运算和赋值就如同对整数操作一样,不存在你在第6点里说的什么有效地址一说,所以才会有“野指针”的问题存在。所以,如果ap == 0x01234567,那么也只会ap->avail = (char *)((union header *)ap + 1) = 0x1234567 + 12 = 0x1234573,而不会得出别的值。

programath 回复于 2014年03月28日

不理解为什么跨过一个header的大小就对齐了?

我觉得跨一个header的主要目的不是为了对齐,而是说跨一个header以后的空间才是available空间的起始位置。还有就是ap->avail指向的位置是否对齐还取决于编译器的对齐规则,即#pragma pack。编译器给header分配空间的时候会考虑#pragma pack、header中每个分量的大小、header中字节数最大的那个分量的大小,总之header最后是内存对齐的,然后跨过一个header仍然是内存对齐的。

crimx 回复于 2014年03月28日

11楼 @玉楼 我用32位 gcc union header的确是16字节大小,而 @RolandXu 的第5步也有点问题,经过(union header *)apap的地址还是原来的地址才对,只不过大小变成16字节了。

其实ap->avail = (char *)((union header *)ap + 1)是为了预留一个struct block大小的空间,同时保证接下来要对齐union align。作者利用了union对齐的特性,不同计算机、不同编译器都有可能产生不同的结果,但是一定是对齐了。

ap指向分配区struct block *ap = arena[a];,所以ap是不是应该总是对齐。 @RolandXu

static struct block
    /* 块头 */
    first[] = {  { NULL },  { NULL },  { NULL } },
    /* 分配区,即为对应块头链表中的最后一个 */
    *arena[] = { &first[0], &first[1], &first[2] };
RolandXu 回复于 2014年03月28日

13楼 @crimx 关于第5点,我还特意强调了参考我在前面三楼的回复。请仔细阅读那段摘自C99标准的英文,以及连接里面描述的问题。了解指针在两种对齐不一样的类型之间做类型转换的。

C语言的国际标准已经阐明了(union header *)ap会产生的效果。不要拿gcc来解释标准,标准是凌驾于实现以上的东西。

+1的行为在C标准里面是well defined的。如果(union header *)ap不产生一个对齐的地址,那么(union header *)ap+1也不会产生。

玉楼 回复于 2014年03月30日

14楼 @RolandXu 您给这链接开门就说是C11,没提C99啊。

本帖有15个回复,因为您没有注册或者登录本站,所以,只能看到本帖的10条回复。如果想看到全部回复,请注册或者登录本站。

登录 或者 注册
[顶 楼]
|
|
[底 楼]
|
|
[首 页]