【问题标题】:Alignment of char array struct members in C standardC标准中char数组结构成员的对齐方式
【发布时间】:2014-05-01 04:41:07
【问题描述】:

假设我想读/写一个 tar 文件头。 考虑到标准 C(C89、C99 或 C11), char 数组在结构中是否有任何特殊处理,关于填充?编译器能否为这样的结构添加填充:

struct header {
    char name[100];
    char mode[8];
    char uid[8];
    char gid[8];
    char size[12];
    char mtime[12];
    char chksum[8];
    char typeflag;
    char linkname[100];
    char tail[255];
};

我也看到它在网络上的代码中使用过。只是 freading,将这个结构写入一个块中的文件,假设不会有任何填充。当然也假设CHAR_BITS == 8。 我在想这样的 C 代码太常见了,标准会处理这种情况,但我就是在里面找不到,也许我不是一个好律师。

编辑

根据 C 标准之一,接受的答案将给出一个严格的或最严格的可移植实现,这让我可以使用标准库字符串函数来处理这些字段。考虑CHAR_BITS 和所有。我认为需要为此读取 512 uint8_t 的数组,然后可能将它们一个一个转换为字符。有更简单的方法吗?

【问题讨论】:

  • 当你可以成为一名程序员时,谁还想成为一名律师。 ;)
  • 它很常见,无论如何它通常都是个坏主意。数据全部为char 是一种形式。做到这一点的一种保证安全的方法是逐个成员,在读者和作者方面。是的,这很痛苦,是的,您可能会通过使用特定于实现的指令来“打包”您的结构(在两侧)来作弊。
  • 标准不保证对齐(编译器可以添加填充)。但是,您很难找到一个在所示结构中添加填充的编译器;没有明显的理由添加任何填充。如果要进行填充,它将在两个奇数长度字段之后 - typeflagtail。如果一台机器主要是面向字的(基本访问是偶数字节边界上的 16 位单元——我不知道有这样的当前机器),那么编译器可能会通过添加填充这些字段来获得更好的性能。 OTOH,即使对于这样的计算机,编译器也可能不会。

标签: c struct alignment standards


【解决方案1】:

C11(latest freely available draft)只说“结构对象内可能有未命名的填充,但不是在其开头”(§6.7.2.1 ¶15)和“结构的末尾可能有未命名的填充或联合”(§6.7.2.1 ¶17)。它对结构内的填充没有进一步的限制。

平台 ABI 可能对填充有更严格的要求,但这取决于平台特定,因为其他平台可能有其他填充要求。 x86-64 ABI for Unix/Linuxchar 1 字节对齐,并指定:

结构和联合假定其最严格对齐的组件对齐。每个成员都被分配到具有适当的最低可用偏移量 结盟。任何对象的大小始终是对象对齐方式的倍数。

数组使用与其元素相同的对齐方式,除了局部或全局 长度至少为 16 字节的数组变量或 C99 可变长度数组变量 始终具有至少 16 个字节的对齐方式4

结构和联合对象可能需要填充以满足大小和对齐方式 约束。任何填充的内容都是未定义的。


4对齐要求允许在对阵列进行操作时使用 SSE 指令。 编译器通常不能计算可变长度数组 (VLA) 的大小,但它是 ex- 预计大多数 VLA 将需要至少 16 个字节,因此强制 VLA 具有 至少 16 字节对齐。

这似乎意味着在这个平台上,结构内不会有填充。但是,在某些情况下,数组变量具有更严格的对齐限制,以便能够与向量指令一起使用;其他平台也可能对数组结构成员施加此类限制。

如果您想要便携,在一次调用中读取结构时,您可能需要查看readv。这是一个vectored or scatter/gather I/O operation,它允许您指定要读取的数组和长度的数组。例如,对于这种情况,您可以编写:

struct header h;
struct iovec iov[10];
iov[0].iov_base = &h.name;
iov[0].iov_len = sizeof(h.name);
iov[1].iov_base = &h.mode;
iov[1].iov_len = sizeof(h.mode);
/* ... etc ... */
bytes_read = readv(fd, iov, 10);

请注意,readv 是在 POSIX/Single Unix 规范中定义的,而不是在 C 标准中。在标准 C 中,最简单的做法是单独读取这些元素中的每一个(即使有可用的矢量 I/O,除非您绝对需要对整个 I/O 操作)。

在你的编辑中,你写:

根据 C 标准之一,接受的答案将给出一个严格的或最严格的可移植实现,这让我可以使用标准库字符串函数来处理这些字段。考虑到CHAR_BITS 等等。我认为需要为此读取 512 uint8_t 的数组,然后可能将它们一一转换为字符。有更简单的方法吗?

C 规范不保证 uint8_t 可用:“typedef 名称 uint<i>N</i>_t 指定宽度为 N 且没有填充位的无符号整数类型......这些类型是可选的。” (C11 草案,§7.20.1.1,¶2-3)。但是,如果 8 位值可用,则char 保证为 8 位值,因为它保证至少为 8 位并且保证是不是位域的最小对象(§5.2 .4.2.1 ¶1):

下面给出的值应替换为适用于#if 预处理指令的常量表达式。此外,除了CHAR_BITMB_LEN_MAX 之外,以下内容应替换为与根据整数提升转换为相应类型的对象的表达式具有相同类型的表达式。它们的实现定义值的大小(绝对值)应等于或大于所示值,符号相同。

——不是位域的最小对象的位数(字节)
CHAR_BIT 8

因此,如果您没有可用的 8 位字节,您将无法直接读取这些字段并将它们作为单独的数组元素访问八位字节;您必须使用位移和掩码手动拆分单个字节。但是,我知道没有现代架构缺少 8 位字节(对于通用计算,文件 I/O 完全是一个问题;一些 DSP 可能,但它们可能没有标准的 C 文件 I/O )。

如果您确实有一个 8 位字节,那么 char 保证为 8 位,因此除了使用 uint8_tchar 的清晰度之外没有太多好处。如果您真的很担心,我会确保您在构建过程中的某处检查 CHAR_BIT 是 8 并称之为好。

【讨论】:

  • 谢谢,我从中学到了新东西。特别是,它不能以可移植的方式在 C 语言“内部”完成。
  • 感谢iov 的想法!
  • 一条评论:我更喜欢iov[0].iov_len = sizeof (h.name); 而不是iov[0].iov_len = 100;
  • @Kay 当然,那可能会更干净一些。
【解决方案2】:

实际上填充、名称修饰等不受 C 标准约束,而是受特定 ABI 约束:http://en.wikipedia.org/wiki/Application_binary_interface

有明确的标准如何填充数据类型,以便它们可以在不同的编译器之间共享。您的手册页很可能会告诉您切换以更改 ABI。

【讨论】:

    【解决方案3】:

    C99 和 C11 标准草案在第 13 段中的 6.7.2.1 结构和联合说明符部分(C11 中的第 15 段)中说:

    [...]结构对象内可能有未命名的填充,但不是在其开头。

    15段(C11中的17段):

    结构或联合的末尾可能有未命名的填充。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-26
      • 1970-01-01
      • 1970-01-01
      • 2018-11-21
      • 2011-12-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多