GNU 编译器的奇怪行为

Strange behavior from the GNU Compiler

今天我在编写一些代码来收集 Linux 上的网络接口信息时遇到了一些奇怪的事情。我正在使用 ifaddrsioctl 等标准功能从内核中提取我需要提取的内容。我对大多数 ifaddrsioctl 函数都不熟悉,所以我正在写行、编译、检查输出、评论事情搞砸了,只是把事情搞得一团糟。最终我偶然发现了这个错误:

 NixNet.h:36:15: error: expected ‘;’ at end of member declaration
   36 |  struct ifreq ifr_addr;
      |               ^~~~~~~~
NixNet.h:36:15: error: expected unqualified-id before ‘.’ token
   36 |  struct ifreq ifr_addr;
      |               ^~~~~~~~
RawSock.cpp: In constructor ‘RawSock::RawSock()’:
RawSock.cpp:7:16: error: ‘struct ifreq’ has no member named ‘ifru_addr’
    7 |  memset(&this->ifr_addr, 0, sizeof(struct ifreq));
      |                ^~~~~~~~
RawSock.cpp:33:15: error: ‘struct ifreq’ has no member named ‘ifru_addr’
   33 |  strcpy(this->ifr_addr.ifr_name, this->ifname);
      |               ^~~~~~~~
RawSock.cpp:37:28: error: ‘struct ifreq’ has no member named ‘ifru_addr’
   37 |  ioctl(sock, SIOCGIFADDR, &ifr_addr);
      |                            ^~~~~~~~
RawSock.cpp:44:50: error: ‘struct ifreq’ has no member named ‘ifru_addr’
   44 |  printf("%s\n", inet_ntoa(((struct sockaddr_in*)&ifr_addr.ifr_addr)->sin_addr));
      | 

咦...?

起初我认为这是我的老鼠窝 header 设法以某种奇怪的方式使编译器出错:

#include <iostream>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <pcap/pcap.h>
#include <regex>
#include <netdb.h>
#include <ifaddrs.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
//#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netdb.h>
__attribute__((constructor)) void RawSock_init();
__attribute__((destructor)) void RawSock_clnp();
void pcap_listalldevs(void);
char* iptos(u_long);
char* ip6tos(struct sockaddr*,char*,int);
void ifprint(pcap_if_t*);
uint32_t getMyIP();
bool getMyMAC(unsigned char*);
extern struct ifaddrs* ifaddr_g;
class RawSock {
private:
        uint32_t ifaddr;
        uint8_t hwaddr[6];
        char ifname[17];
        struct ifreq ifr_idx;
        struct ifreq ifr_mac;
        struct ifreq ifr_addr;
        struct sockaddr_ll sock_ll;
        int sock;
public:
        RawSock();
        ~RawSock();
        int send(void* buf, size_t len);
};

所以我将有问题的声明放入了一个虚拟程序中:

#include <iostream>
#include <cstring>
#include <linux/if.h>
struct ifreq ifr_addr;
int main(){
        memset(&ifr_addr, 0 , sizeof(struct ifreq));
        return 0;
}

g++ dummy.cpp -g -o dummy --std=c++2a

Test.cpp:4:14: error: expected initializer before ‘.’ token
    4 | struct ifreq ifr_addr;
      |              ^~~~~~~~
Test.cpp: In function ‘int main()’:
Test.cpp:6:9: error: ‘ifr_ifru’ was not declared in this scope
    6 | memset(&ifr_addr, 0, sizeof(struct ifreq));
      |         ^~~~~~~~
 

我把它扔进了gcc,同样的结果。此时唯一要做的就是为变量选择一个不同的名称。这导致了一个干净的编译,所以我在我的存储库中应用了名称更改。 问题已解决!

也许没什么,但我觉得这真的很奇怪,我很想知道这种行为背后的原因。

ifr_addr是glibc中定义的宏https://github.com/lattera/glibc/blob/master/sysdeps/gnu/net/if.h#L153

# define ifr_addr   ifr_ifru.ifru_addr  /* address      */

因此您的代码变为:

class RawSock {
private:
      //struct ifreq ifr_addr;
      struct ifreq ifr_ifru.ifru_addr;
};

无效。


您可能会问“为什么?”。为什么 ifr_addr 是一个宏?

手册页 https://man7.org/linux/man-pages/man7/netdevice.7.html 描述了以下结构:

struct ifreq {
     char ifr_name[IFNAMSIZ]; /* Interface name */
     union {
         struct sockaddr ifr_addr;
         struct sockaddr ifr_dstaddr;
         // ... other fields
     }; 
 //  ^^ anonymous union - no name
 };

此结构使用 anonymous union,但该功能可从 C11 获得。因此,为了让代码在 C11 之前的不支持匿名联合的编译器下工作,你给联合成员一个名字并做宏魔术:

struct ifreq {
     char ifr_name[IFNAMSIZ]; /* Interface name */
     union {
         struct sockaddr ifr_addr;
         struct sockaddr ifr_dstaddr;
         // ... other fields
     } some_hidden_name;
 };

 #define ifr_addr     some_hidden_name.ifr_addr
 #define ifr_dstaddr  some_hidden_name.ifr_dstaddr

 int main() {
    struct ifreq variable;
    variable.ifr_addr = stuff;
    // becomes:
    // variable.some_hidden_name.ifr_addr = stuff;
 }

这个技巧就用到了——http://www.qnx.com/developers/docs/6.5.0SP1/neutrino/lib_ref/s/sigevent.html https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/bits/types/sigevent_t.h.html#_M/sigev_notify_function。标准库中有很多宏。大多数结构成员都有前缀 - ifr_*sigeventsigev_*siginfosi_*timevaltv_*。这是因为 old C 编译器对所有结构成员名称和变量使用相同的命名空间,而且还因为在极端情况下标准库可能希望使用此类技巧。

总的来说,选择唯一的名称,通常是默默地kind-of假设标准库中的结构成员名称可以是宏。