首先声明:我根本不是个C高手,只是碰到这个问题,一路查下去才得到这些信息的。这篇帖子放在这里做个记录,也方便后来人参考。不知道里面有没有错误,欢迎批评指正。
我是在果冻的百合blog上看到这篇题目的,原题是在水源上。题目是这样的
#include <stdio.h>
void main()
{
union
{
struct
{
unsigned short s1:3;
unsigned short s2:3;
unsigned short s3:3;
}x;
char c;
}v;
v.c=100;
printf("%d\n",v.x.s3);
}
问打印的结果是啥:A:4 B:0 C:3 D:6
100就是0×64。乍看起来,100写成0110 0100,从左到右,依次把这些位分给s1,s2,s3,s3,分别是3,1,0。答案好像很明显。但其实,用我们最普通的32位x86机器跑gcc得到的结果是1,不是0。
细细考究起来,这里的问题还是很多的:
- 在计算机内存里面100这个十进制数的表示形式是什么样的?
- 对于一个给定的二进制串,语言规则或者编译器是如何放置位域的?按照声明的顺序从左到右?还是反过来?按照其他的顺序?这个问题还隐含了另外一个问题,就是当一个字节的剩余比特位没办法放下一个完整的位域的时候,这个时候如何处理?
先搞清楚第一个问题吧。查过Wikipedia以后,才知道这个术语叫做Bit numbering(应该是翻译为位序吧),和所谓字节序(Byte numbering或者Byte endianess)并不是一回事,也并不总是一致的。通常来说,小端机器总是用的是LSB 0 bit,而大端机器就得具体问题具体分析。把这个问题翻译为术语,就是说某个机器是LSB 0 bit numbering,还是MSB 0 bit numbering?通俗点说就是,从bit 0到bit 8,0×64被表示为0110 0100还是0010 0110?
第二个问题,这个涉及到语言标准和编译器实现细节,并不是那么容易一下子搞清楚的。那就先做两个想当然的假设吧:假设位域是按照声明的顺序从左到右放置的;假设当一个字节放不下一个位域的时候,从下一个字节借几位过来凑够一个位域。这样的话,
- 对于LSB 0 bit的机器,s1:001(LSB 0 bit的4),s2:001(4),s3:100(1)
- 对于MSB 0 bit的机器,s1:011(MSB 0 bit的3),s2:001(1),s3:000(0)
这里还有个假设:借过来的那一位是个0,但事实证明那一位并不一定总为0。从题目来看,union v至少会占据2个字节,而那个赋值操作却只会初始化最低的那一个字节,另一个字节没有被初始化。
回到第二个问题。虽然我对语言标准是一抹黑,但我还是硬着头皮把C标准给下下来了。用最原始的办法,搜索bitfield和bit-field,一个个看下去,还真是找到了和这个问题相关的那一段(P102,S6.7.2.1 Structureand union specifiers):
10 An implementation may allocate anyaddressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.
从这段标准来看,第二个问题的两小问都是implementation-defined。那就简单了,翻开gcc的info,C Implementation->Structures unions enumerations and bit-fields implementation,发现这两个问题下面都有一行:“Determined by ABI.”。这个ABI是何物?Wikipedia告诉我们,它叫Application Binary Interface。我认为ABI应该是做处理器的写的吧,不该是gcc去弄吧,可是苦苦追寻居然在Intel的网站上找不到正式的关于ABI的文档,在Intel的Developer’s Manual也没有找到。倒是在SCO的网站上有这么一个pdf,是给x86的。x86-64的ABI好像是这个。gcc的info里面提到的3.2以后的那个正式ABI在这里。SCO的那个ABI确实很详细,里面各种乱七八糟的问题,包括alignment都有涉及。对于前面那些问题的解答在这个时候才真正地浮现出来了,我就不一个个抄出来了,太麻烦了。
最后一个问题就是当s1,s2,s3都是unsigned char的时候,有人发现结果和unsigned short是不一样的。这个就是到当一个位域可能会横跨一个storage unit的时候如何处理的问题。如果前面的文档都有读过的话,这个问题其实也不难解答。我得到的一个知识点就是位域前面的数据类型声明原来就是用来声明一个位域应该处于一个什么样的storage unit内。也就是说char位域要放在一个char“变量”里面,而short位域要放在一个short变量里面。如果s1,s2,s3都是unsigned char的时候,在s1和s2被安顿好以后,s3就放不下了。如果s3被声明为一个short的位域的话,它会紧挨着s1,s2继续放下去,也就是之前所猜想的那样。但是如果它是一个char位域的话,就不一样了,因为放置s1和s2的那个storage unit(是个char)放不下s3了,gcc会把s3放在下一个字节里面,这样的话,同样是一个LSB 0 bit机器,大家都是short的时候,s3应该是1,而大家都是char的时候,s3是0(假设没有被初始化的位都是0)。
好像要说的问题都说了,就此打住。这篇blog草稿打了很久,终于给写完了。嗯,最后再JW一下吧,这个题作为一道笔试题目,可以说是很不好的,答案不唯一,又只是考些边边角角的东西,但是一路跟踪下来,还是有不少收获的。
这个sf其实应该留给果冻同学…
赞技术帖…偶居然也看懂了一些
Lindt —— 2009/09/10 @7:52 pm
那看样子这篇帖子还是比较通俗易懂的了,哈哈哈
phio —— 2009/09/10 @10:22 pm
技术贴要支持的,而且这个考题,其实我们公司的笔试题里有一道类似的,嘿嘿
zweily —— 2009/11/14 @10:34 pm
献丑献丑,欢迎Wei哥指教
phio —— 2009/11/14 @10:44 pm
你的解释应该是不正确的。x86是MSB0 (bit)的,但是是LSB (byte)的。所以,在内存里面, V的存放为:
Byte 0: 0110 0100
Byte 1: 0000 0000
根据i386 ABI,因为i386是LSB (byte), 16bit short 应该这么去解释
在寄存器里
0000 0000 0110 0100
[ byte 1 ] [byte 0 ]
MS LS
所以
s1= 0×100 4 (LS 低三位)
s2= 0×100 4 (LS 中间三位)
s3= 0×001 1 (byte0/byte1 组合起来)
对于MSB (byte) 来说 16 bit short 为 byte 0, byte 1
0110 0100 0000 0000
[byte 0 ] [ byte 1]
MS LS
s1=011
s2=001
s3=000
ignace —— 2009/11/26 @5:25 pm