c - 严格混叠警告和tcpdump示例代码

标签 c gcc strict-aliasing

简化概念后,严格的别名规则规定对象应该由兼容类型的指针或指向char的指针访问。这样,编译器可以对代码进行一些假设并进行一些优化。
尽管这条规则的解释会引起一些质疑和讨论,但它本身并不是什么国家机密。所以我的问题是:
为什么一些受人尊敬的组织(由经验丰富的程序员维护)经常提交不遵守严格别名规则的代码?我可以举一个例子:在他们网站的tutorialOntcpdump上有一个example code明显多次违反了严格的别名规则。我似乎也有很多其他代码这样做,特别是在处理网络数据包时。。
他们只是幸运的编译器没有完全破坏他们的代码,所以它没有被注意到吗?它们是否依赖于使用libpcap标志编译的用户?考虑到一些受人尊敬的程序员,这是一种可能性——我认为Linus Torvalds自己就是一个例子,正如我在一些邮件列表中看到的那样,一个Linux代码片段可能会在启用严格别名的情况下中断——不要真的认为通过严格别名获得的优化补偿了编译器可能做出的错误假设。或者,不幸的是,这仅仅是编程社区中固有的错误代码和错误实践?
另一个问题是,来自tcpdump-fno-strict-aliasing代码:为什么即使使用sniffex.cgcc -O5 -Wall -Wextra -Wstrict-aliasing=1 sniffex.c -lpcap编译,也不会对违反的严格别名规则发出任何警告?是不是因为它在没有地址运算符时不容易检测到这些类型的punnings?
我不想再提这个话题(因为还有很多其他的问题),但即使我明白这个规则,我似乎也不明白为什么在很多地方它总是被忽视。。
编辑:
显然违反严格别名规则的示例代码片段有:

void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet)
{
...
/* declare pointers to packet headers */
const struct sniff_ethernet *ethernet;  /* The ethernet header [1] */
const struct sniff_ip *ip;              /* The IP header */
const struct sniff_tcp *tcp;            /* The TCP header */
const char *payload;                    /* Packet payload */
...
/* define ethernet header */
ethernet = (struct sniff_ethernet*)(packet);

/* define/compute ip header offset */
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
...
/* define/compute tcp header offset */
tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
...
/* define/compute tcp payload (segment) offset */
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
...

在那里,它们做了某种形式的覆盖,覆盖了表示网络数据包不同部分的结构,以便更容易地访问每个字段。归根结底,它使用几个没有有效类型gcc 5.4.0(原始&类型)的指针来访问它,因此,我认为,违反了严格的别名规则。

最佳答案

严格的别名规则是有争议的。
背景位:
注意,“严格别名规则”不是一个正式术语,但它指的是关于有效类型的第6.5/6段和关于通过指针访问数据的第6.5/7段。后一段是实际严格的混叠规则,它一直是C的一部分,只要语言已经标准化,那么它的存在实际上不会对任何人产生冲击。从ANSI-C草案到C11,6.5./7中的文本几乎完全相同。
然而,这一部分在C90中并不清楚,因为它主要关注用于“左值访问”的指针的类型,而不是实际存储在那里的数据的类型。这使得您将指针强制转换为无效指针的情况变得不清楚,例如在使用memcpy时,或者在执行各种形式的类型双关时。
在C99中,有人试图通过引入有效类型来澄清这一点。实际上,这并没有改变严格别名规则的措辞,只是使解释变得更加清晰。(它仍然是标准中最难理解的部分之一。)
该规则的初衷是允许编译器避免奇怪的最坏情况假设,例如C99原理中的这个示例:

int a;
void f( double * b )
{
  a = 1;
  *b = 2.0;
  g(a);
}

如果编译器可以假设b不指向a,这应该是一个明智的假设,因为给定了非常不同的类型,那么它可以将函数优化为
a = 1;
*b = 2.0;
g(1); // micro-optimization, doesn't have to load `a` from memory

因此,尽管规则一直存在,但在C99上的某个地方,当gcc编译器特别决定乱用并滥用使用不同有效类型的情况时,这并不是一个问题。例如,此代码非常合理,但违反了严格的别名:
uint32_t u32=0;
uint16_t* p16 = (uint16_t*)&u32; // grab the ms/ls word (endian-dependent)
*p16 = something;
if(u32)
  do_stuff();

上面的代码在各种比特旋转和硬件相关编程中都是非常有用的。大多数编译器将生成程序员所期望的代码,即更改32位值的ms/ls字的代码,然后检查是否应该调用该函数。
但是,由于上述代码由于严格的别名冲突而在形式上是未定义的行为,所以像gcc这样的编译器可能会决定滥用它并生成始终从机器代码中删除对do_stuff()的调用的代码,因为它可能假设代码中的任何内容都不会从值0中更改u32
为了避免这种不需要的编译器行为,程序员必须走自己的路。要么使u32不稳定,这样编译器就不得不读取它-这会阻止对变量的所有优化,而不仅仅是不需要的优化。或者也可以提出一种自制的联合类型,其中包含一个uint32_t和两个uint16_t。或者可能访问每个字节的u32字节。很不方便。
因此,程序员倾向于违背严格的别名规则,编写依赖于编译器的代码,而不是基于严格的别名进行不可思议的优化。当您希望在不同的部分中分割一块数据时,存在许多有效的情况,例如当去序列化原始数据字节块时。
例如,如果我一字节接一字节地接收串行数据,并将其存储在uint8_t数组中,而程序员知道该数组包含一个uint16_t,那么我应该能够编写类似(uint16_t*)array的代码,而无需编译器做出“哦,看,这个数组从未使用过,让优化它走”或其他一些无意义的假设。
大多数编译器不会疯狂,而是生成预期的代码。但按照标准,他们是可以发疯的。而随着gcc在硬件相关编程中的日益普及,这对嵌入式行业来说正成为一个严重的问题,因为硬件相关编程是一项日常任务,而不是一个奇异的特例。
总的来说,标准委员会多次没有看到这个问题。
当然,很多程序员实际上一开始就不知道严格的别名规则,这就是为什么他们编写的代码违反了它的原因。

关于c - 严格混叠警告和tcpdump示例代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43270657/

相关文章:

c - 是否可以在没有源代码的情况下将 no-pie 可执行文件转换为 pie 可执行文件?

c++ - 这个严格的别名示例是否正确?

c - 不使用 malloc() 分配 struct dirent

c++ - 严格别名规则是否适用于函数调用?

c - 函数system()是否可以在线程中调用?

c - 使用两种不同的有符号整数二进制表示形式的程序

c - 将二进制搜索展平为有序的单链表 [C]

macos - 64 位 Mac OS X Lion 上的 nasm/gcc 问题

c - "undeclared"命令中的变量 "#pragma"?

在 Ubuntu 11.10 下从源代码编译ettercap 0.7.4.1(链接器错误)