C语言中一个字节对齐问题的分析
C 语言中一个字节对齐问题的分析
█ 1
C语言中一个字节对齐问题的分析
李 云
Email: yunli.sharing@gmail.com
Blog: yunli.blog.51cto.com
摘要
字节对齐(alignment)是 CPU 在性能方面所面临的一个非常重要的问题。有些处理器能自动
的处理不对齐数据的访问(对字节对齐要求不严格),但是,有些处理器却无法处理(对字节对齐要
求很严格)。当处理器无法处理对齐问题时,其将引发一个异常(exception),当然从程序的角度来
说就...
C 语言中一个字节对齐问题的
█ 1
C语言中一个字节对齐问题的分析
李 云
Email: yunli.sharing@gmail.com
Blog: yunli.blog.51cto.com
摘要
字节对齐(alignment)是 CPU 在性能方面所面临的一个非常重要的问题。有些处理器能自动
的处理不对齐数据的访问(对字节对齐要求不严格),但是,有些处理器却无法处理(对字节对齐要
求很严格)。当处理器无法处理对齐问题时,其将引发一个异常(exception),当然从程序的角度来
说就是出错(crash)。
对于 C 程序员,大部分情况下我们并不考虑字节对齐问题,这并不是说我们不需要考虑,而是
因为碰到这种问题的情况很少。一方面要在特定的处理器上,而另一方面和我们写的程序也有关系,
只有两个条件同时满足时问题才会出现。因此,结果给我们的感觉是“字节对齐与我无关”。
本文通过对一小段简单的代码在不同处理器上的运行结果引出对字节对齐问题的关注,同时对
其原因进行了分析。
关键词
C 语言 字节对齐
参考资料
[1] The SPARC Architecture Manual v8
1 问题的引入
下面是一段被简化的程序,分别在 Windows(32 位的 x86 处理器)和 Solaris(32 位
的 SPARC 处理器)上编译和运行,其结果将完全不同。在 Windows 上程序运行正常,但
是在 Solaris 上程序运行会出错,并且会在终端上打印出“Bus Error”以及产生一个“Core
dump”文件。
main0.c
typedef struct
{
short mark;
char body[128];
} msg_t;
typedef struct
{
char *pointer;
} header_t;
int main ()
C 语言中一个字节对齐问题的分析
2 █
{
msg_t msg = {0};
void *p = ((header_t *)msg.body)->pointer;
return 0;
}
为什么这么简单的一个程序在不同的操作系统(其实是处理器)上的运行结果却决然
不同?这其实是一个 CPU 字节对齐所引发的问题,下面我们通过对字节对齐问题的分析来
探究其背后的原理。后面的分析我们全部是针对运行在 32 位 SPARC 处理器上的 Solaris
操作系统进行的。
2 为什么要字节对齐
简单的说来就是为了提高 CPU 的性能,或者说是提高程序运行的效率。当然,在其背
后更有简化 CPU
的功效。因此,我们所写的 C 程序为了得到尽可能高的效率就必须
最大限度的满足 CPU 对于字节对齐的要求,编译器在这当中起着至关重要的作用。
下面的 C 程序编译后运行,在终端上将会打印出“size of type_t is 8”。为什么是 8 而
不是 5 呢?这是因为编译器考虑到了运行效率从而将 type_t 结构进行了 4 字节边界对齐的
处理。
#include
typedef struct
{
char a;
int b;
} type_t;
int main ()
{
printf ("size of type_t is %d\n", sizeof (type_t));
return 0;
}
这里需要指出的是,编译器会根据具体的结构选择是 4 字节边界对齐还是 2 字节边界
对齐。比如,下面定义的 element_t 结构,其 sizeof 大小应当是 4,而不是 3,更不会是 8。
typedef struct
{
char a;
short b;
} element_t;
下面我们来分析为什么进行字节对齐能提高运行效率。要对数据结构进行更为高效的
C 语言中一个字节对齐问题的分析
█ 3
操作,从 CPU 的角度来看就是尽可能减少 CPU 对内存的访问次数。对于 type_t 结构,其
内存布局如图 1 所示,需要指出的是 SPARC 是 big-endian 模式,图中的 b=b0b1b2b3。
a
pad
pad
pad
b0
b1
b2
b3
0x0000
0x0004
a
b0
b1
b2
b3
0x0000
0x0004
采用字节对齐时 不采用字节对齐时
typedef struct
{
char a;
int b;
} type_t;
图 1 type_t 结构的内存布局示意图
在做进一步的分析之前,还需要清楚的是,对于 32 位处理器,其数据总线是 32 位的。
因此,CPU 从内存中存取数据时可以(也只能)一次读入 4 个字节。为此,CPU 从内存
中存取数据时总是以 4 字节为边界进行存取的。如果,我们所写的程序只需要访问内存中
的一个字节,此时也需要从内存中读入 4 个字节吗?是的。对于一次内存所存取的 4 个字
节中,我们是需要存取其中的 1 个字节、2 个字节或是全部 4 个字节,CPU 如何区分呢?
是:CPU 提供了不同的指令,而由编译器根据情况选择使用不同的指令。
现在,我们开始分析采用字节对齐和不采用字节对齐时,CPU 对于内存的访问次数有
何不同。回到图 1,先看看采用字节对齐时的情况,从图中可以看出,当 CPU 需要分别访
问 a 变量和 b 变量时,无论如何都只需要分别进行一次内存存取,图中的花括号表示一次
内存存取操作。对于不采用字节对齐的情况,a 变量无论如何只要进行一次内存操作的,
而 b 变量有可能需要进行二次内存操作,因为这一变量跨越了 4 字节的边界。这里之所以
说有可能,是因为有可能对 b 进行访问之前,可能刚好完成了对 a 的访问,而对 a 访问时,
b0、b1和 b2也同时读入(或写入)了,这种情况下,只需要读入(或写入)b3即可。
此外,更为麻烦的是对于边界不对齐的 b,还得将其合成一个 4 字节(一部分是来自
于一个 4 字节中的 b0、b1和 b2,另一部分来自于另一个 4 字节中的 b3),而这又增加了程
序的复杂性,即需要更多的指令来完成。
从以上分析可以看出,采用字节对齐能提高系统性能。而编译器在编译程序时,也会
根据需要选择不同的指令来完成对数据的存取操作。
3 问题的分析
知道了字节对齐的重要性后,我们看看 main0.c 在 Solaris 上是如何出现问题的。为了
方便我将 main0.c 再一次列出如下。
main0.c
typedef struct
{
C 语言中一个字节对齐问题的分析
4 █
short mark;
char body[128];
} msg_t;
typedef struct
{
char *pointer;
} header_t;
int main ()
{
msg_t msg = {0};
void *p = ((header_t *)msg.body)->pointer;
return 0;
}
由于 header_t 结构中只有一个指针变量 pointer,指针是 32 位的,易于字节对齐。因
此,编译器对于所有 pointer 的访问都采用 32 位的存取指令。这可以通过查看 main0.c 的
汇编代码 main0.s 来验证,如下所示。
main0.s
.file "main0.c"
.section ".text"
.align 4
.global main
.type main,#function
.proc 04
main:
!#PROLOGUE# 0
save %sp, -256, %sp
!#PROLOGUE# 1
add %fp, -152, %o0
mov 130, %o2
mov 0, %o1
call memset, 0
nop
ld [%fp-150], %o0
st %o0, [%fp-156]
mov 0, %o0
mov %o0, %i0
nop
ret
restore
.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2.3"
其中需要注意的是 SPARC 处理器中的 ld 指令,这一指令从内存中读入一个 4 字节的
C 语言中一个字节对齐问题的分析
█ 5
字(word),其对应于 C 程序中取得 header_t 结构中的 pointer 变量的值。在参考资料[1]
中的 13 页有如下一段话需要特别注意。
Alignment Restrictions
Halfword accesses must be aligned on 2-byte boundaries, word accesses must be
aligned on 4-byte boundaries, and doubleword accesses must be aligned on 8-byte
boundaries. An improperly aligned address in a load or store instruction causes a
trap to occur.
其中的 halfword 是指 2 个字节,word 是指 4 个字节,而 doubleword 是指 8 个字节。
这段话给我们的信息是:当使用 ld 指令从内存中读入一个 4 字节的字时,其地址必须是以
4 字节为边界对齐的。
前面说到 C 语言对于数据结构的对齐还有一个很重要的问题是:C 语言除了对结构进
行对齐外(从结构内部的角度),还需要将进行字节对齐处理过的结构变量(从结构的外部
角度)分配在以 4 字节为边界的地方才有意义,比如图 1 中的 type_t 变量如果不是放在
0x0000 地址(4 字节边界对齐)上,而是放在 0x0001 的地址上,则边界对齐的结构也会
变成边界不对齐。因此,我们可以推理,所有的全局变量或是局部变量,在内存中都应当
分配在 4 字节边界对齐的地方(这样的话编译器的设计最为简单),只有这样 C 语言(编
译器)对于边界对齐的处理才算是完整的。
回到 main0.c 我们可以分析出 main 函数中的 msg 变量是 4 字节边界对齐的,因此
msg.body 是边界不对齐的,其相对于 msg 的偏移是 2 个字节(其前面有一个 2 个字节的
mark 变量)。接着程序将 msg.body 强制转换成了 header_t 结构。最终结果是 pointer 也
是边界不对齐的,这违背了 SPARC 处理器中 ld 指令要求地址边界是 4 字节对齐的限制。
这就是为什么运行这一程序会出现“Bus Error”的原因。
下面的 main1.c 程序能正常的运行。
main1.c
typedef struct
{
short mark;
char body[128];
} msg_t;
int main ()
{
msg_t msg = {0};
msg.body[1] = 3;
return 0;
}
这是因为编译器知道 body[1]是一个字节,因此不会采用类似 ld 这样存取 4 字节的指
令,这可以从 main1.c 的汇编代码看出,如下所示。
main1.s
C 语言中一个字节对齐问题的分析
6 █
.file "main1.c"
.section ".text"
.align 4
.global main
.type main,#function
.proc 04
main:
!#PROLOGUE# 0
save %sp, -248, %sp
!#PROLOGUE# 1
add %fp, -152, %o0
mov 130, %o2
mov 0, %o1
call memset, 0
nop
mov 3, %o0
stb %o0, [%fp-149]
mov 0, %o0
mov %o0, %i0
nop
ret
restore
.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2.3"
其中的 stb 指令表示向内存中写入一个字节,当然,这一指令对于存取地址并无边界
对齐的要求,也就不会产生“Bus Error”这种问题了。
在默认的情况下 GCC 采用字节对齐的技术来提高程序的效率,但是有时我们不希望
这种字节对齐处理,比如两个主机进行网络通讯时,我们不需望因为字节对齐而传送多余
的字节。在这种情况下,可以在结构之前加上#pragma pack(1)编译预处理命令,它将告诉
GCC 对其后的数据结构采用一个字节对齐技术进行处理。仍然值得一提的是,不采用字节
对齐将影响程序的运行效率。下面是一段程序采用对齐技术和不采用对齐技术时汇编代码
的对比。
main0.c main1.c
typedef struct {
char a;
int b;
} type_t;
int main () {
type_t tp;
tp.b = 1;
return 0;
}
#pragma pack(1)
typedef struct {
char a;
int b;
} type_t;
int main () {
type_t tp;
tp.b = 1;
return 0;
}
C 语言中一个字节对齐问题的分析
█ 7
main0.s main1.s
图 2 type_t 结构采用字节对齐和不采用字节对齐时对程序效率影响对比
图 2 中 main0.c 是采用字节对齐的代码,从其汇编程序 main0.s 中可以看出,由于我
们的 C 程序中只对 tp.b 进行了赋值操作,所以只需要一个 st 指令(在 SPARC 处理器中,
这一指令向内存中写入 4 个字节)即可,而不需要用到 ld 指令。main1.c 是不采用字节对
齐技术的代码,从其汇编 main1.s 中可以看出,其需要 2 个 ld 指令和 2 个 st 指令,这是因
为在这种情况下变量 tp 跨越了 4 字节边界,这与我们在第 2 节中的分析是完全吻合的。
4 问题的解决方法
其实,main0.c 中的方法我们在程序设计中使用到的情况还是很多的,比如,进程间
通讯,为了方便我们会定义一个 msg_t 消息结构,其 body 可以存放小于 128 字节的任意
.file "main0.c"
.section ".text"
.align 4
.global main
.type main,#function
.proc 04
main:
!#PROLOGUE# 0
save %sp, -120, %sp
!#PROLOGUE# 1
mov 1, %i0
st %i0, [%fp-20]
mov 0, %i0
nop
ret
restore
.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2.3"
.file "main1.c"
.section ".text"
.align 4
.global main
.type main,#function
.proc 04
main:
!#PROLOGUE# 0
save %sp, -120, %sp
!#PROLOGUE# 1
ld [%fp-24], %i1
sethi %hi(-16777216), %i0
and %i1, %i0, %i0
st %i0, [%fp-24]
ld [%fp-20], %i1
sethi %hi(16776192), %i0
or %i0, 1023, %i0
and %i1, %i0, %i0
sethi %hi(16777216), %i1
or %i0, %i1, %i0
st %i0, [%fp-20]
mov 0, %i0
nop
ret
restore
.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2.3"
C 语言中一个字节对齐问题的分析
8 █
数据结构,然后从一个进程发送给另一个进程。收到消息的进程,根据 mark 的值将 body
转换成与发送端相同的结构进行相应的处理。
即然有这种应用,但我们同时也看到 main0.c 中这种直接进行数据结构转换将有可能
导致问题,如何解决呢?我想有两种方法可供选择。其一是改变 msg_t 结构的定义,如下
所示。
typedef struct
{
short mark;
short pad;
char body[128];
} msg_t;
其中,我们加入了一个 pad 从而使得 body 对于 msg_t 来说是边界对齐的。另一种方
法是采用宏来解决这一问题。如下所示。
typedef struct
{
short mark;
char body[128];
} msg_t;
typedef struct
{
char *pointer;
} header_t;
#define ALIGN4(addr) ((((char*)a) + 3)&~3)
#define CONVERT(from, to) (to)ALIGN4(from)
int main ()
{
msg_t msg = {0};
void *p = (CONVERT(msg.body, header_t *))->pointer;
return 0;
}
其核心思想是:不直接从 msg_t 中的 body 所在地址开始转换成 header_t 结构,而是
取得一个比 body 大(或相等)的、但以 4 字节为边界对齐的一个地址开始进行转换。
5 结论
C 程序设计中我们需要注意字节对齐问题,当然,在有些 CPU 上由于其对字节对齐要
求并不严格,比如 x86 处理器的指令不存在这样的限制,所以我们为了简化设计可以不考
C 语言中一个字节对齐问题的分析
█ 9
虑这一问题。但在像 SPARC、MIPS 这些对字节对齐要求很严格的 CPU 上设计程序时我
们必须考虑这类问题。
从程序的可移植性角度来看,字节对齐是不可回避的问题之一。
当在 Solaris 操作系统上运行程序出“Bus Error”时,通常都是由字节对齐所引起的。
本文档为【C语言中一个字节对齐问题的分析】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。