OC

Knowledge OS
鹦鹉螺口语

请教一个内存对齐的问题

crimx
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你要是不明白什么意思的话,我建议你先看基础的书,后看更复杂的书。

2楼 · 回复
abutter 回复于 2014年03月27日

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

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

3楼 · 回复
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

4楼 · 回复
玉楼 回复于 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*
5楼 · 回复
crimx 回复于 2014年03月27日

谢谢各位回答

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

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

6楼 · 回复
cnsoft 回复于 2014年03月27日

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

7楼 · 回复
玉楼 回复于 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怎么办?虽然这两个指针的作用现在一样,将来未必一样。这应该就是这段代码要说的业务逻辑,而和哪个指针对齐多大内存无关。

8楼 · 回复
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这个值,参考我前面的回复

9楼 · 回复
crimx 回复于 2014年03月28日

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

不知道 @RolandXu 有没有博客?

10楼 · 回复
RolandXu 回复于 2014年03月28日

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

11楼 · 回复
玉楼 回复于 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,而不会得出别的值。

12楼 · 回复
programath 回复于 2014年03月28日

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

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

13楼 · 回复
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] };
14楼 · 回复
RolandXu 回复于 2014年03月28日

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

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

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

15楼 · 回复
玉楼 回复于 2014年03月30日

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

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