共享库中的位域可移植性
Posted
技术标签:
【中文标题】共享库中的位域可移植性【英文标题】:Bit fields portability in a shared library 【发布时间】:2021-02-03 06:43:47 【问题描述】:我很难理解 C 中位字段的可移植性。假设我有一个共享库,仅由两个文件组成,libfoobar.h
(公共标头)和libfoobar.c
,内容如下:
libfoobar.h
:
typedef struct some_bitfield_T
unsigned char foo:3;
unsigned char bar:2;
unsigned char tree:2;
unsigned char window:1;
some_bitfield;
extern unsigned int some_function (some_bitfield input);
libfoobar.c
:
#include "libfoobar.h"
unsigned int some_function (some_bitfield input)
return input.foo * 3 + input.bar + input.tree + 1 - input.window;
编译并安装库后,我使用名为test
的程序对其进行测试。
test.c
:
#include <stdio.h>
#include <libfoobar.h>
int main ()
some_bitfield my_attempt =
.foo = 6,
.bar = 3,
.tree = 1,
.window = 1
;
unsigned int some_number = some_function(my_attempt);
printf("Here is the result: %u\n", some_number);
return 0;
上面的test
程序是否有可能产生与以下输出不同的任何东西?
Here is the result: 22
如果是,什么时候?如果库是由我以外的其他人编译的怎么办?如果我为库和test
程序使用不同的编译器会怎样?
【问题讨论】:
位域通常不可移植。允许编译器重新排序成员,并且没有指定结构中可能的填充。 但这不适用于普通结构吗? 填充是的,所以共享库中使用的结构需要非常仔细地设计,成员之间没有任何填充。但对于普通结构,成员不能重新排列,这与位域不同。 但是为什么编译器要对一个字节内的成员重新排序呢? Tom,如果编译器确实可以按照他们所说的那样重新排序成员,那么它可能不会在所有地方都显示 22。 【参考方案1】:这里是 C11 标准的相关部分:
实现可以分配任何大到足以容纳位域的可寻址存储单元。如果有足够的空间,结构中紧跟在另一个位域之后的位域将被打包到同一单元的相邻位中。如果剩余空间不足,则将不适合的位域放入下一个单元还是与相邻单元重叠是实现定义的。单元内位域的分配顺序(高位到低位或低位到高位)是实现定义的。未指定可寻址存储单元的对齐方式。
§6.7.2.1 第 11 点。
这意味着编译器可以使用它喜欢的任何合适的类型来保存位域,并且它们将按照定义的顺序与每个位域相邻。
但是,编译器可以自行选择是从高到低还是从低到高排序。如果空间不足,它还可以选择重叠位域或分配新的存储单元(在您的情况下这不是问题,您只有 8 位)。
考虑到上述情况,我们可以回答您的问题。如果程序和库是使用相同编译器实现编译的,您可以仅保证您的测试程序将给出正确的答案。如果您使用两种不同的编译器,即使它们都使用(比如)unsigned char
来存储位字段,一个可以从字节的顶部开始,另一个可以从底部开始。
实际上,正如上面的 ensc 所说,平台 ABI 可能会定义一个位字段打包和排序标准,这使得同一平台上的编译器之间可以正常工作,但原则上并不能保证。
【讨论】:
谢谢你,杰里米。这是从 C 标准的角度来看的情况。但是,如果我很好地理解了 ensc 的答案,则似乎可以保证位域在同一个 ABI 中始终具有相同的顺序。因此,如果我的库是编译的,比方说,对于 x86-64 机器,它应该始终独立于我选择的编译器工作。还是我错过了什么? 例如,我在x86-64 ABI 中读到:“...位域遵循与其他结构和联合成员相同的大小和对齐规则。另外:位域从右分配到剩下 ...”。这使得位域几乎总是可移植的,因为为不同 ABI 编译的程序无论如何都无法链接。 @TheWebIsTheOldBlack 是的,假设编译器在各个方面都遵守 ABI,这很有可能在现在几乎每个人都使用 clang 或 gcc 时。就我个人而言,我并没有真正看到位域的意义,而且我从不使用它们。 知道这一点真是太棒了!当他们说位域的顺序是实现定义的时,他们忘记说它是在某个地方定义的 ... ABI! C 标准没有定义它们的事实并不意味着没有其他人这样做。如果您有两个几十个布尔值要发送到库位域确实是最实用的选择。我们可以使用位掩码,但它们会非常冗长。【参考方案2】:位域是实现定义的,不可移植。但是对于大多数相关平台,它们的提取/打包由 ABI 明确指定,以便它们可以安全地用于共享库中。
例如:
ARM EABI 在 7.1.7 “位域”中指定它们。 i386 ABI 在第 3-6 页上指定它们 x86-64 在第 15+ 页上指定它们 MIPS 在第 3-7 页上指定了它们【讨论】:
谢谢你,ensc。你是说我的图书馆在实践中总是有效的吗? 还有一个问题:假设我的库以二进制形式分发给特定平台,比如 Linux x86-64。会不会是使用它的程序不能正常运行? @TheWebIsTheOldBlack 实际上 mo(它们是兼容的)并且编译器尝试具有相同的行为。尽管如此,我还是会小心使用外来特性(易失性位域、显式对齐),因为在非主要平台上,由于其 ABI 规范的模糊性,可能存在编译器错误。 谢谢你,ensc。我认为使用没有外来限定符的普通位域应该是非常可移植的。【参考方案3】:我很难理解 C 中位域的可移植性
好吧,没什么好理解的 - 位域不可移植。
上面的测试程序是否有可能产生与下面的输出不同的东西?
最常见的情况是用户空间和内核空间之间的通信。有时通信使用指向结构的指针。由库实现者编写的标头,如 glibc
,包装内核系统调用有时会重复在内核源代码树中定义的相同结构。为了正确通信,这些结构中的填充必须在两边都相同 - 在内核编译时在内核一侧,在我们高兴地编译我们自己的项目时在用户空间一侧,即使在内核编译多年后。
在大多数架构 + 操作系统上,都存在一个“ABI”——一组规则,这些规则还确定应如何填充结构以及应如何打包位域。编译器可能遵守该 ABI 或不遵守。当 gcc 用于从 linux 交叉编译 windows 时,例如。需要使用__attribute__ ((ms_struct))
来确保使用与微软编译器的恶作剧兼容的正确结构封装。
所以回答:Is there any possibility
- 当然存在,不同的编译器标志或设置可能会导致结构成员之间的打包或填充不同所以我可以用gcc -fpack-struct=100
编译程序,你用gcc -fpack-struct=20
编译你的共享库和哎呀。但这不仅限于结构填充 - 其他人可以使用具有 64 位而不是 32 位的 unsigned int
编译您的程序,因此函数的返回值可能是意外的。
如果是,什么时候?
当使用不兼容的代码生成方法创建依赖指定 ABI 进行通信的机器代码时。从实际的角度来看,这曾经发生过吗?每个 linux 系统在/usr/lib
中都有 吨 个共享库。
如果库是由我以外的其他人编译的怎么办?
然后他可以为所欲为。但是,如果您真的愿意,您可以传达您的共享库遵循一些常见的 ABI 标准。
如果我为库和测试程序使用不同的编译器会怎样?
然后请务必阅读该编译器文档,以确保它遵循您需要的通用 ABI。
阅读:How to Write Shared Libraries. Ulrich Drepper - 第 3 节似乎是相关的。
【讨论】:
谢谢你,KamilCuk。不知何故,这使得位域情况看起来不那么悲惨。如果我很好地理解了这种情况,如果我使用 gcc for Linux 编译我的库而不指定任何外来编译器选项,我希望像 gcc 这样的好的编译器会简单地服从 ABI。因此,我的库的二进制文件将非常兼容除非您的程序不遵循 ABI。我做对了吗? 一般是的。还是不要。在标准委员会使位域可移植之前,一般建议是:不要使用它们。但我在grep -r ' *: *[0-9]\+;' /usr/include
树中看到了许多位域。
我在我的/usr/include
树中也看到了它们。在我看来,对位域的恐惧有点像对goto
语句的恐惧。我认为我们应该停止说位域不可移植。他们肯定是。 int
的大小也是实现定义的,但int
是可移植的,它的大小在某处定义——只是不是 C 标准。这是完全相同的情况。在我们和 C 标准之间,有几层中间委员会和标准化,最值得注意的是通用 ABI,它们确实定义了几乎所有内容。
噗,你提到了编译器之间代码的可移植性。这是 ABI 涵盖的简单案例。问题是 架构 之间的可移植性,当您期望不同架构(在不同 ABI 上)上的位域具有相同顺序时。这就是问题。 Bitfields 是由先知创建的,用于别名硬件位置,但由于它们不可移植,因此不能用于此目的。典型的例子是(曾经是?)内核中的 TCP/IP 层——我记得它经常使用位域,并且在任何字节序上都做了很多工作来将它们打包。
我猜这是使用union
访问位域的原始内容的情况?如果是,我不在乎。我只是希望位域在正确使用时始终是可移植的,也就是说,当它们被用于它们的本来面目时:位域,可以使用它们的名称访问。以上是关于共享库中的位域可移植性的主要内容,如果未能解决你的问题,请参考以下文章