【问题标题】:What is the best way to read a structure from a binary file containing IP header fragments?从包含 IP 标头片段的二进制文件中读取结构的最佳方法是什么?
【发布时间】:2019-09-20 01:25:33
【问题描述】:

在我的计算机网络实验室期间,我不得不阅读许多包含 IPv4 格式数据包的二进制文件。 Here 是 IPv4 头文件格式。

以下结构封装了 IP 标头的所有基本部分。

struct ip_header {
    uint8_t version;
    uint8_t header_length;
    uint8_t service_type;
    uint16_t total_length;
    uint16_t identification;
    uint8_t flags;
    uint16_t fragment_offset;
    uint8_t ttl;
    uint8_t protocol;
    uint16_t checksum;
    uint32_t src;
    uint32_t dest;
    /* other fields for options if needed */
};

读取二进制文件以获取结构化格式数据的一种方法是逐字节读取文件,然后将每个字节字段专门转换为上述结构的相应字段。读取文件不是问题。

我想知道这是否是唯一的方法,或者有没有其他好的和神奇的方法来实现同样的目的。另外,最近我知道字节序在读取这些具有不同大小数据类型的文件时也会产生一些问题。

【问题讨论】:

  • c 还是 c++ ?他们不一样。如果是c++,那是什么标准?标记您正在使用的那个(如果您使用当前的,则不标记)
  • 另外,你的字段大小是错误的。第一个:uint8_t version:4; 使用位域。
  • 既然您已经澄清您知道通过读取原始字节并转换为正确的类型是可能的,您是否尝试过其他任何方法?
  • 如果您以与实际 IP 标头二进制兼容的方式创建结构(减去字节序),这将有所帮助。这样您就可以直接从文件中读取到属于struct ip_header 变量的内存。在/usr/include/netinet/ip.h 中查看struct iphdr
  • 感谢@TedLyngmo 提供的信息。

标签: c++ c


【解决方案1】:

通常的方法是使用fread之类的东西

bool readIpHeader(ip& buffer, const std::string& filename)
{
    auto pFile= fopen(filename.data(), "rb");
    if (!pFile) {
        return false;
    }
    auto ok= fread(&buffer, sizeof(buffer), 1, pFile) == 1;
    fclose(pFile);
    return ok;
}

这会将sizeof(buffer)读入地址&buffer:它将用文件的内容填充缓冲区; fread 将返回 1 成功。

就像 Ted 指出的那样,你的结构很糟糕。您可以将https://unix.superglobalmegacorp.com/Net2/newsrc/netinet/ip.h.html 作为来源(如果您在 Linux 上,您很可能可以include 文件):

struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN 
    u_char    ip_hl:4,           /* header length */
              ip_v:4;            /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN 
    u_char    ip_v:4,            /* version */
        ip_hl:4;        /* header length */
#endif
    u_char    ip_tos;            /* type of service */
    short     ip_len;            /* total length */
    u_short   ip_id;             /* identification */
    short     ip_off;            /* fragment offset field */
#define    IP_DF 0x4000          /* dont fragment flag */
#define    IP_MF 0x2000          /* more fragments flag */
    u_char    ip_ttl;            /* time to live */
    u_char    ip_p;              /* protocol */
    u_short   ip_sum;            /* checksum */
    struct    in_addr ip_src,ip_dst;    /* source and dest address */
};

【讨论】:

  • AFAIK,位字段对删除填充无效。
  • 感谢@Mirko 的 ip 标头结构。但问题不在于读取文件。我的问题是:有什么方法可以直接从二进制文件中读取结构,在这种情况下,我不必显式分配 ip 标头结构中的所有字段。
  • 按照我的介绍方式,您可以在一行代码中一次读取所有结构。您没有明确地读取和分配:它是由编译器以编程方式为您完成的。
  • @yumetodo 我不明白你在说什么。谁在谈论填充?
  • 我担心二进制能力问题。
【解决方案2】:

如果您关心可移植性(特别是对于强制 16 位和 32 位变量自然对齐的大端架构),您不能只将 struct 的内存布局写入磁盘。编译器的下一个版本可能会以不同的方式打包数据并破坏与所有数据文件的兼容性。不止一家大公司发现,他们通过在另一个 CPU 上编译而不进行规范化,意外创建了两种数据格式,大端和小端。通常,没有任何简单的方法可以判断旧文件保存在哪个文件中。请记住,数据比代码更有效!

这假设你想在你的程序中使用ip_header结构,它应该被填充以便高效访问,并且它的目的不仅仅是隐藏文件布局。

当不同大小的字段散布在一起时,单独设置它们不是一个好方法。您不能假设实现可以使用任意未对齐的地址作为指针。在这种情况下,我也没有假设该文件与您的 CPU 具有相同的字节序;我将字节顺序定义为大端。 (如果您希望此代码在 x86 这样的小端 CPU 上运行,您可以将顺序定义为小端,但仍然使用诸如 glib 之类的库或操作系统。)

您可以将磁盘上的布局移植到内存中的结构,如下所示:

#include <arpa/inet.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct ip_header {
    uint8_t version;
    uint8_t header_length;
    uint8_t service_type;
    uint16_t total_length;
    uint16_t identification;
    uint8_t flags;
    uint16_t fragment_offset;
    uint8_t ttl;
    uint8_t protocol;
    uint16_t checksum;
    uint32_t src;
    uint32_t dest;
    /* other fields for options if needed */
} ip_header;

#define IP_HEADER_DISK_LEN 22U

bool read_ip_header( FILE* const input, ip_header* const d )
{
  char buffer[IP_HEADER_DISK_LEN];

  if ( IP_HEADER_DISK_LEN !=
       fread( buffer, 1, IP_HEADER_DISK_LEN, input ) ) {
    return false;
  }

  memset( d, 0, sizeof(*d) );

  memcpy( &d->version,         &buffer[0],  sizeof(d->version) );
  memcpy( &d->header_length,   &buffer[1],  sizeof(d->header_length) );
  memcpy( &d->service_type,    &buffer[2],  sizeof(d->service_type) );
  memcpy( &d->total_length,    &buffer[3],  sizeof(d->total_length) );
  d->total_length = ntohs(d->total_length);
  memcpy( &d->identification,  &buffer[5],  sizeof(d->identification) );
  d->identification = ntohs(d->identification);
  memcpy( &d->flags,           &buffer[7],  sizeof(d->flags) );
  memcpy( &d->fragment_offset, &buffer[8],  sizeof(d->fragment_offset) );
  d->fragment_offset = ntohs(d->fragment_offset);
  memcpy( &d->ttl,             &buffer[10], sizeof(d->ttl) );
  memcpy( &d->protocol,        &buffer[11], sizeof(d->protocol) );
  memcpy( &d->checksum,        &buffer[12], sizeof(d->checksum) );
  d->checksum = ntohs(d->checksum);
  memcpy( &d->src,             &buffer[14], sizeof(d->src) );
  d->src = ntohl(d->src);
  memcpy( &d->dest,            &buffer[18], sizeof(d->dest) );
  d->dest = ntohl(d->dest);

  return true;
}

这对整个标头进行单次读取,但您可能会进行单独的 I/O 调用,甚至将文件映射到内存中。大多数现代编译器都足够聪明,可以将连续的memcpy() 调用组合到连续的位置,将不需要的字节交换编译为无操作,并且只编译为不会立即被覆盖的memset() 字节,所以,如果你能得到只需复制字节,这种方式应该同样有效。 (出于您的目的,您甚至可以跳过将填充字节清零并进行字节序转换。)

请记住,读取操作将比处理对齐字节顺序或填充的任何位旋转操作花费的时间要长得多。试图优化这些并不能很好地利用你的时间。特别是如果它在另一个编译器上编译成不兼容的程序!

【讨论】:

    【解决方案3】:

    如果您的 IPv4 标头以与“它们进来”相同的格式存储(这是存储它们的常用方式) - 源地址和目标地址是标头中的最后一个字段,应该这样做:

    #include <fstream>
    #include <iostream>
    
    #include <netinet/ip.h> // a common place to find a "iphdr" definition
    
    // add a streaming operator for reading an iphdr
    std::istream& operator>>(std::istream& is, iphdr& ip) {
        return is.read(reinterpret_cast<char*>(&ip), sizeof(iphdr));
    }
    
    // add a streaming operator for writing an iphdr
    std::ostream& operator<<(std::ostream& os, const iphdr& ip) {
        return os.write(reinterpret_cast<const char*>(&ip), sizeof(iphdr));
    }
    
    int main() {
        std::ifstream ips("ipheaders");
        if(ips) {
            iphdr h;
            while(ips >> h) {
                std::cout << h.version << "\n"
                          << h.ihl << "\n"
                          << h.tos << "\n"
                          << h.tot_len << "\n"
                          << h.id << "\n"
                          << h.frag_off << "\n"
                          << h.ttl << "\n"
                          << h.protocol << "\n"
                          << h.check << "\n"
                          << h.saddr << "\n"
                          << h.daddr << "\n";
            }
        }
    }
    
    

    物理标头中的前 4 位始终是version,但正如@Mirco 所示,当您摆弄位字段时,您编译程序的计算机的字节序很重要。通过网络传来并存储在文件中的前 4 位仍然是 version - 如果您也使用添加的 operator&lt;&lt;iphdr 写入磁盘,则会如此。如果您想要可移植性,请按照自 IPv4 发明以来的外观来读取和写入 IP 标头。

    幸运的是,ip 标头的布局与大多数系统上所需的基本数据类型的对齐方式相匹配。如果您发现无法创建与原始数据匹配的 IP 标头结构的系统,您很可能找不到 netinet/ip.h - 但如果您仍然担心它,您可以添加编译时检查:

        static_assert(alignof(uint8_t) == 1);
        static_assert(alignof(uint16_t) == 2);
        static_assert(alignof(uint32_t) == 4);
    

    【讨论】:

      【解决方案4】:

      我认为是这样的:

          #include <stdint.h>
          #include <arpa/inet.h>
          #include <netinet/ip.h>
          ....
          #define IPSIZ  20
          static void ntoip(uint8_t *buf, struct ip *i) {
              i->ip_vhl = buf[0];
              i->ip_tos = buf[1];
              i->ip_len = ntohs(buf+2);
              i->ip_id  = ntohs(buf+4);
              i->ip_off = ntohs(buf+6);
              i->ip_ttl = buf[8];
              i->ip_p   = buf[9];
              i->ip_sum = ntohs(buf+10);
              i->ip_src = ntohl(buf+12);
              i->ip_dst = ntohl(buf+16);           
          }
          int fget_ip(FILE *fp, struct ip *i) {
              uint8_t buf[IPSIZ];
              if (fread(buf, sizeof buf, 1, fp) == 1) {
                  ntoip(buf, i);
                  return 1;
              }
              return 0;
          }
      ...
          void iptoh(struct ip *i, uint8_t *buf) {
      ...
          }
          int fput_ip(struct ip *i, FILE *fp) {
      ....
          }
      

      是你最好的选择。简单,清晰,易于理解,便携。您可以确保它始终按网络顺序读取和存储,这样无论是从文件还是实际设备中都可以正常工作。

      如果它以某种方式成为性能问题,则将其封装起来,您可以在一个地方用通常的一堆恶作剧替换它。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-12
        相关资源
        最近更新 更多