[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
“茴”字有几种写法:结构体占多少空间你造吗?
by Chen Jie of TinyLab.org 2014/10/15
前言
近来无意卷入某个考试,题风颇为远古,中有若干结构体空间占用的题目,i.e. “sizeof(struct …)”,你懂的。问谷歌,发现几处文章均不十分符实验。
本文据网上文章所留线索,结合实验总结而来,似是考据,呈现在此亦有如孔乙己那个名问:“茴字有几种写法,你造么?”,算是抒发一下无奈之情吧。
言归正传,关于C的基本数据类型,有几套说法,叫做 LP,ILP,LLP:
- LP:long 和指针的位宽一致
- ILP:int、long 和指针的位宽一致
- LLP:long long 和指针的位宽一致
目前最常见的大概是 LP64 吧,即 long 和指针都是64位的。在此环境中,通常 GCC 等编译器还会自动定义宏“__LP64__”,因此可用此宏来判断当前是否为“LP64”。
那么问题来了,在 LP64 环境中,结构体的空间占用如何估算?
来自 sizeof(struct …) 的疑问
简单地说,sizeof(struct …) 并不是各成员的大小总和,还有若干padding:
- 成员间的 padding,满足成员自然对齐要求而引入的 padding。
- 结构体尾部的 padding。
结构体成员间的 padding
struct stru_1 {
unsigned char mem_a;
int mem_b;
};
/* sizeof(struct stru_1) = ? */
答案是 8。若地址从 0 起,那么 mem_a 之后为 1,不满足 int 类型“自然对齐”的要求,故插入 padding,使得 mem_b 从 4 开始。
结构体尾部的 padding
struct stru_2 {
unsigned char mem_a;
int mem_b;
char mem_c;
};
/* sizeof(struct stru_2) = ? */
答案是 12。因为结构体大小还需要是结构体的对齐要求的整数倍。进一步解释如下:
- 在汇编层面,访存指令对访问的地址有对齐要求,例如 64 位访存指令访问的地址需要是 64 位对齐的,不然或性能下降或有异常。对此,编译器在栈中分配变量时,会插入一些 padding 来使变量对齐其宽度。
- 结构体中间各成员也遵从上点,故有成员间的 padding。那么,结构体变量的对齐有何要求呢?
- 结构体变量对齐要求,取其各成员对齐要求中的最大。例如“struct stru_2 abc“,假设 abc 地址为“addr_abc”,则 mem_b 地址为 “addr_abc + 1(mem_a)+ 3(padding)”,须是 4 字节对齐才好。算术是数学老师教的童鞋马上就发现了,abc_addr 必整除 4,即结构体地址是 4 字节(32 位)对齐的。
- 最后,让我们来考虑“struct stru_2 abc2”,如上点,abc1 的地址必是 4 字节(32 位)对齐的。若 abc[0]的大小是“1(mem_a)+ 3(padding)+ 4(mem_b) + 1(mem_c)= 9”,加上 abc[0] 的起始地址(4 字节对齐),怎么算也是奇数啊!于是,必须再给结构体尾部加一些 padding,使得结构体总长是结构体对齐的整数倍。
bitfield 怎么算?
提前向各位看官说明:以下例子需要用到不同编译器做对比。而说好的 LP64 环境来举例,却因为旅途中条件艰苦,没有合适的对比环境,只好用个 mingw32(gcc on Windows, 32bit) 来凑数。由于未用到 long 和指针,姑且将就下。
struct stru_4 {
char mem_a:1;
int mem_b:4;
int mem_c:4;
};
/* sizeof(struct stru_4) = ? */
结果依赖编译环境:
- 在64位 os x,clang 600.0.51 版本上,答案是 4。猜测相邻三个成员所占用位数加在一起,未超过三成员中最宽的 int 类型的位数。故实际只用 1 个 int 搞定。
- 在 mingw32 环境中,gcc 4.8.1 上,答案是 8。猜测相邻同类型会合并,即 mem_a 享有一个 char,mem_b 和 mem_c 共享一个 int。char 和 int 之间有 3 字节的 padding。
以下把这两环境简称为 clang 和 gcc。
struct stru_5 {
char mem_a:1;
int mem_b:4;
int mem_c:4;
int mem_d:24;
int mem_e:9;
};
/* sizeof(struct stru_5) = ? */
在两编译环境中,答案均是 12。对于该结果的猜测:
- clang 上,mem_a、mem_b 和 mem_c 享有一个 int,mem_d 独享一个,mem_e 独享一个。即 mem_d 自身各个位,没有被分到两个 int 类型存储上。
- gcc 上,mem_a 享有一个 char,之后 3 字节 padding。接下来 mem_b、mem_c 和 mem_d 共享一个 int,mem_e 享有最后一个 int。
union和其他
union uni_1 {
struct stru_2 mem_a;
long mem_b;
};
/* sizeof(union uni_1) = ? */
答案是 16。首先 union 类型空间占用取其各成员中最大者,其次,同样考虑尾部 padding,还必须进一步增肥至_ union 对齐的整数倍_。
struct stru_3 {
char mem_a;
struct stru_2 mem_b;
};
/* sizeof(struct stru_3) = ? */
答案是 16。
#pragma pack(N) 指示
至此,我们讲的都是自然对齐的情况。
然而可以通过“#pragma pack(N)“指示编译器,手工调整对齐。其中 N 可以为空(不填),此时就是自然对齐。
N 可以取 1, 2, 4, 8, …。当 N 指定时,结构体成员实际对齐要求为 MIN(N, 类型的自然对齐要求),例如 N = 2 时,对于 int 型成员而言,其实际对齐要求为 MIN (2, 4) = 2。
另外,在空间占用估算中用到的结构体自身对齐要求,也成了 MIN(N, 结构体的自然对齐要求)。
以下给出上述结构体在一组 N 值下的大小:
N == 1:stru_1到5大小依次为 5,6, 7, 2(clang, gcc 上为 5), 6(clang,gcc 上为9)。可见此时没有 padding。
N == 2: stru_1到5大小依次为 6, 8, 10, 2(clang,gcc 上为 6), 6(clang,gcc 上为10)。
上述结果中含有 bitfield 的结构体再次亮了。随便挑个奇怪的结果来看,例如 N == 1 时, sizeof(stru_5) 在 clang 上为 2 字节,通过反汇编可知全部 bitfiled 被拼在了一起(64 位寄存器喔)。再次说明 bitfield 这货被编译器玩坏了。
大小尾端与位序
通常,多字节数据交换会考虑大小尾端的问题。例如 32 位(4 字节)数 “0×12 34 56 78”:
- 小尾端:数值低位在前(在内存的低地址),故本数在内存中,四个字节的排列为”0×78“,“0×56”,“0×34”,“0×12”。
- 大尾端:数值高位在前(在内存的低地址),故本数在内存中,四个字节的排列为“0×12”,“0×34”,“0×56”,“0×78”。
上述提的是字节序。
再问,对于大小尾端,一个字节内,数值高、低位在内存中的顺序是什么?也就是位序的问题。
答案是与字节序一致。例如以下代码:
struct {
unsigned char f1:3;
unsigned char f2:4;
} bif;
char *bif_p = (char *) &bif;
memset (&bif, 0, sizeof(bif));
bif.f1 = 2;
bif.f2 = 5;
printf ("0x%02x\n", bif_p[0]);
在小尾端上,打印输出为 0x2a。
成员 f1 内存地址该靠前,故对其赋值影响“字节数值”的低位。而 f2 由于地址靠后,赋值影响“字节数值”的高位。因此:
0(not used) 0 1 0 1(f2) 0 1 0(f1) (按照数值位高低排列,与地址序相反)
即, 第 0-2 位,属于 f1;第 3-6 位,属于 f2。
在处理大小尾端时,大部分情况下不用考虑位序问题。例如在处理网络协议中,常用到htonl(s),来将本机字节序转成网络序(网络序规定为大尾端)。该函数不对位序处理。在实际传输发生时,通过内存中“一字节的 8 个位”和“网卡 8个位” 的连线,即完成了转换。
对于将一个字节还要掰成许多位域的应用来说,那就要编程者自己费心了,总之字节的值是不会变的。以本节例子来说,大尾端上用 struct bif 对例中赋值进行解读,会得到错误的结果:
0 0 1(f1) 0 1 0 1(f2) 0(not used) (按照数值位高低排列,与地址序一致)
即,f1 的值变成了 1,f2 的值恰好还是 5。
可以在大尾端上定义新的结构体,来正确解读:
struct {
unsigned char not_used:1;
unsigned char f2:4;
unsigned char f1:3;
}
小结
费心思去了解一个结构体的内存布局,常见于对协议的解析,例如“p = (strcut ip_header *)(buffer);”。然而,使用某些库提供的 serialize/deserialize 功能(例如 GVariant)会是个更好的选择。
至于 bitfield,不要试图通过定义一个含位域的结构,来解析协议,因为不同编译器结果不同。乖乖地手动使用位操作来提取,虽然难看些,但毕竟可移植。
最后,大小尾端的位序由底层硬件来处理,通常无需考虑。
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |