最近在看lcc的源码,书本说这里ap->avail = (char *)((union header *)ap + 1);
是为了保证avail
总是指向一个对齐后的地址。不明白。。。
header
就是一个block
和align
组成的联合体,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;
};
应该是char或者void/char都有可能被分配到不对齐的地址上,但是long,double之类的不会,所以union align的起点应该不会在一个不对齐的地址。因为对齐这样cpu和寄存器才好对他们进行操作,所以,编译器会自动把他们对齐了。
然后union你要是不明白什么意思的话,我建议你先看基础的书,后看更复杂的书。
首先需要明白 ((union header *)ap + 1) 是怎么回事,就是将 ap 指针转换成 union header *,加 1 操作就是指针指向地址再增加一个单元的偏移。
对于 union,编译器会取其总大的单元来作为整体长度,其对齐的地址取决于所有成员及其子成员单个最大的长度。
没看过原文,从你的描述来看,书里的意思大概是利用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
首先,你要知道相关变量占内存的大小。以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);
要分开来理解。
ap->avail
我认为ap是一个struct block*
类型,但不管是什么类型,(union header *)ap
这句都把它强制转换成了union header*
这样一个指针类型;char *
指向的地址是以char
为基本构成单元的,即每个单元占1字节;struct block*
指向的地址是以struct block
为基本构成单元的,即每单元占24字节。这样可知:————
char *p1 = 0x100000;
struct block *p2 = 0x200000;
//
p1++; // p1 == 0x100001
p2++; // p2 == 0x200024
ap
加1后,其内的地址值实际上是加了24。char*
是因为ap->avail
的类型是char*
。6楼 @cnsoft 不是。这里的对齐应该是和业务有关的,你这段代码干啥的我不知道,但对这段代码而言union header
占用内存大小与struct block
是相同的。把一个struct block*
强制转换为union header
应该是出于业务的考虑。
很明显对于union header
而言struct block
和union align
都是必须的,我们现在可知struct block
占用内存大于union align
,如果将来再在union header
需要增加一个占用内存大于struct block
的struct xxx
怎么办?虽然这两个指针的作用现在一样,将来未必一样。这应该就是这段代码要说的业务逻辑,而和哪个指针对齐多大内存无关。
按我的理解来假设一个例子来说说明这个问题。
struct block
和align
的对齐要求。这里我假设假设某个情况下struct block
是4 bytes对齐,大小是12 bytes,而align
是8 bytes对齐,大小为8 Bytesap
应该是一个指向struct block
的指针,它应该满足4字节对齐,假设其值是0x01234564
,它并不满足align
的对齐要求。ap->avail
需要指向一块地址,需要满足struct block
的对齐要求,还要满足处理器本身align
的对齐要求。union header
,由于union的特性,union header
的对齐要求取的是两者里面大的,也就是union header
是8 bytes对齐,大小为16bytes(union header *)ap
产生了一个满足对齐要求的值,在我的例子里面假设这个值是0x01234560
。0x01234560
并没有指向一个有效的地址,这样的话还需要做一个+1
的操作。使得ap->avail
的值为0x01234570
ap->avail
是可分配地址的起点,也就是说从地址0x01234570
开始的内存可能分配给别人使用。但是程序还需要一个struct block
的结构来记录next
avail
和limit
。所以从ap
指向的内存开始,需要预留一个struct block
的大小,而ap->avail
只能从+1
的位置开始。关于第4点,为什么union header
的大小是16?假设一个union header
的数组,如果大小不是16,如何能保证第二个元素也是8 bytes对齐。
关于第5点,为什么产生了0x01234560
这个值,参考我前面的回复
8楼 @RolandXu 你用的啥CPU、编译器我不知道,至少你说的情况在Linux+gcc里不存在。
首先,在32位机里struct block
、union align
和union header
的长度分别为12、8、12,而不会是你说的12、8、16。
其次,在C语言中,对指针的运算和赋值就如同对整数操作一样,不存在你在第6点里说的什么有效地址一说,所以才会有“野指针”的问题存在。所以,如果ap == 0x01234567
,那么也只会ap->avail = (char *)((union header *)ap + 1) = 0x1234567 + 12 = 0x1234573
,而不会得出别的值。
不理解为什么跨过一个header的大小就对齐了?
我觉得跨一个header的主要目的不是为了对齐,而是说跨一个header以后的空间才是available空间的起始位置。还有就是ap->avail
指向的位置是否对齐还取决于编译器的对齐规则,即#pragma pack。编译器给header分配空间的时候会考虑#pragma pack、header中每个分量的大小、header中字节数最大的那个分量的大小,总之header最后是内存对齐的,然后跨过一个header仍然是内存对齐的。
11楼 @玉楼 我用32位 gcc union header
的确是16字节大小,而 @RolandXu 的第5步也有点问题,经过(union header *)ap
后ap
的地址还是原来的地址才对,只不过大小变成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] };