• [Win32]IP数据报的首部如何定义


    在进行网络编程时,可能需要直接操作原始的IP数据报,例如编写网络嗅探器。此时要定义一个表示IP数据报首部的结构体来获取首部中的各个信息,问题也随之而来:平时我们使用的数据都是BYTE、WORD或者DWORD,但IP数据报首部的有些字段并不按照字节、字或双字对齐,字段的长度也不是一字节、两字节或四字节,这种不一致的现象使得结构体的定义很有难度。我见过几种IP数据报首部结构体的定义,虽然方法各异,但都是大量使用union,将几个字段塞进一个BYTE或WORD中,在代码中还要通过移位、按位与等操作获取实际的字段。其实,只要理解数据在内存中是如何摆放的,以及利用好结构体定义的特性,可以最大限度地使结构体的字段直接对应首部中的字段,避免在代码中使用大量的位操作。

     

    位域

    在使用C/C++编程时我们都会大量定义和使用结构体,但可能有不少人还不知道定义结构体时可以使用“位域”这个特性。位域是指不需要占据整个字段长度,而只需要占据一个或多个位的字段。定义方法如下所示:

    1 struct Byte {
    2 BYTE Low : 4;
    3 BYTE High : 4;
    4 };

    上面的代码定义了一个名为Byte的结构体,它有两个字段Low和High,这两个字段的类型都是BYTE,而且都指定了只占用四个位,所以它们共用一个BYTE的空间。又因为Low定义在High之前,所以Low使用这个BYTE的低四位,High使用高四位。

     

    位域的类型除了BYTE之外,还可以使用WORD和DWORD。要注意的是,在使用位域时,对于那些希望共用同一个BYTE、WORD或DWORD的字段,要始终使用相同的类型来定义,只有当BYTE、WORD或DWORD的位分配完毕时,才使用另一种类型。例如,不要出现下面的定义:

    1 struct BitFields {
    2 WORD Field1 : 8;
    3 BYTE Field2 : 8;
    4 };
    5
    6  struct BitFields {
    7 BYTE Field1 : 3;
    8 WORD Field2 : 5;
    9 };

    上面的定义令人难以理解(至少我是根本无法理解),而且也不知道编译器会如何处理这些定义。所以,为了程序的可理解性和正确性,应该中规中矩地使用位域:

    1 struct BitFields {
    2 BYTE Field1 : 5;
    3 BYTE Field2 : 3;
    4 WORD Field3 : 7;
    5 WORD Field4 : 3;
    6 WORD Field5 : 6;
    7 };

    有了位域,在访问这些字段时,编译器会自动进行位操作,还会自动进行截断,所以在代码中再也不需操心这些繁琐的操作了。

     

    位序

    字节序对大家来说都不会陌生,它表示字节在内存中的存放顺序。而位序与字节序的意义相近,它表示的是字节内每个位的存放顺序,也有大端和小端格式。我们通常只在字节级别上访问数据,几乎不会在位级别上访问数据,所以忽略了位序这个问题。现在我们用到了位域,已经深入到了位级别,因此很有必要了解一下Intel处理器的位序。遗憾的是,我没找到任何资料解析Intel处理器使用哪种位序,因此只能靠自己动手进行实验。

     

    实验很简单,下面是实验的代码:

    1 void wmain() {
    2
    3 struct Byte {
    4 BYTE Zero : 1;
    5 BYTE One : 1;
    6 BYTE Two : 1;
    7 BYTE Three : 1;
    8 BYTE Four : 1;
    9 BYTE Five : 1;
    10 BYTE Six : 1;
    11 BYTE Seven : 1;
    12 };
    13
    14 Byte b = { 0 };
    15 b.Zero = 1;
    16 b.Five = 1;
    17 }

    该代码将一个字节的第0位和第5位设置为1,其它位都是0。由于Visual Studio不能以二进制方式查看数据,因此我使用WinDbg进行查看:

     

    左上角第一个字节就是变量b的数据,可以看到第0位和第5位已经设置为1。更重要的是,看到了位序是大端格式,即低地址存放高位,高地址存放低位。

     

    了解了位域和位序之后,下面就开始定义IP数据报的首部了。

     

    IP数据报首部的定义

    首先来看一下IP数据报首部的格式:

     

    对于那些占据整个BYTE、WORD或DWORD的字段,可以直接按照它们的先后顺序来定义而不会出现任何问题,而其它“不规则”的字段则需要进行特殊的设计。“不规则”的字段包括:版本、首部长度、片偏移以及三个标识位。

     

    首先来看一下版本和首部长度,它们都是四字节长度,一开始我们会很自然地按照它们的先后顺序来定义:

    1 struct IPHeader {
    2 BYTE Version : 4;
    3 BYTE HeaderLength : 4;
    4 };

    然而这样是错误的,你使用Version字段得到的是首部长度,使用HeaderLength得到的是版本,两者调转过来了!之所以会出现这样的错误,恰恰是位序的问题。上文说过,Intel处理器使用的是大端格式的位序,所以IPHeader结构的第一个字节是这样的:

     

    由于Version定义在前,所以Version使用0~3位,HeaderLength使用4~7位。很明显,这跟IP数据报首部的格式正好相反。所以,正确的定义应该是:

    1 struct IPHeader {
    2 BYTE HeaderLength : 4;
    3 BYTE Version : 4;
    4 };

     

    接下来的区分服务、总长度和标识都占据了整个BYTE和WORD的长度,所以按顺序定义它们就行了:

    1 struct IPHeader {
    2 BYTE HeaderLength : 4;
    3 BYTE Version : 4;
    4 BYTE DS;
    5 WORD TotalLength;
    6 WORD ID;
    7 };

     

    接下来的三个标识位和片偏移则有点复杂。首先来看一下一个WORD中的位是如何放置的(注意是小端字节序):

     

    对照IP数据报首部的格式图,可以看到三个标识位分别占用了WORD的7、6和5位,剩下的位都是片偏移的。虽然从图中看上去片偏移的位都连接在一起,可是如果以位编号的顺序来看的话,片偏移实际上被标识位分割成了两部分,分别在两个BYTE中:第一部分是0~4位,第二部分是8~F位。所以,无论如何也不能仅仅使用位域把这两部分组合在一起,必须在代码中使用位操作来组合。这确实是一个遗憾。

     

    将这两部分组合起来有多种方法,我使用的方法是:将这个WORD分成两个BYTE,第一个BYTE 的最后三个位作为标致位,前面5个位作为片偏移的第一部分,第二个BYTE全部作为片偏移的第二部分,如下所示:

    1 struct IPHeader {
    2 BYTE HeaderLength : 4;
    3 BYTE Version : 4;
    4 BYTE DS ;
    5 WORD TotalLength;
    6 WORD ID;
    7 BYTE FragmentOffset0 : 5;
    8 BYTE MF : 1;
    9 BYTE DF : 1;
    10 BYTE Reserved : 1;
    11 BYTE FragmentOffset1;
    12 };

    注意第一个BYTE中的字段都按反顺序来定义,这也是位序的缘故,就不再解释了。为了得到完整的片偏移,要将第一部分左移8位,再加上第二部分:

    FragmentOffset = (FragmentOffset0 << 8) + FragmentOffset1

      

    好了,IP数据报首部中比较难搞的字段都已经解决了,剩下的都很容易解决,下面是完整的定义:

    1 struct IPHeader {
    2 BYTE HeaderLength : 4; //首部长度
    3   BYTE Version : 4; //版本
    4   BYTE DS; //区分服务
    5   WORD TotalLength; //总长度
    6   WORD ID; //标识
    7   BYTE FragmentOffset0 : 5; //片偏移
    8   BYTE MF : 1; //MF标识
    9   BYTE DF : 1; //DF标识
    10   BYTE Reserved : 1; //保留标识
    11   BYTE FragmentOffset1; //片偏移
    12   BYTE TTL; //生存时间
    13   BYTE Protocol; //协议
    14   WORD Checksum; //检验和
    15   DWORD SourceAddress; //源地址
    16   DWORD DestinationAddress; //目的地址
    17  };

     

    TCP报文段首部的定义

    既然说到了IP数据报首部,就不得不说一下TCP报文段的首部。下面是TCP报文段的首部格式:

     

    上图中比较复杂的是数据偏移、保留字段以及一些标致位,不过有了上文的讲解,相信这不再是问题。下面直接给出完整的定义,不再详细解释了:

    1 struct TCPHeader {
    2 WORD SourcePort; //源端口
    3   WORD DestinationPort; //目的端口
    4 DWORD SequenceNumber; //序号
    5 DWORD AcknowledgmentNumber; //确认号
    6 BYTE Reserved0 : 4; //保留字段第一部分
    7 BYTE DataOffset : 4; //数据偏移
    8 BYTE FIN : 1; //FIN标识
    9 BYTE SYN : 1; //SYN标识
    10 BYTE RST : 1; //RST标识
    11 BYTE PSH : 1; //PSH标识
    12 BYTE ACK : 1; //ACK标识
    13 BYTE URG : 1; //URG标识
    14 BYTE Reserved1 : 2; //保留字段第二部分
    15 WORD Window; //窗口
    16 WORD Checksum; //检验和
    17 WORD UrgentPointer; //紧急指针
    18 };

     

    UDP报文段首部的定义

    为了本文的完整性,这里也给出UDP报文段首部的格式以及定义。

     

    1 struct UDPHeader {
    2 WORD SourcePort; //源端口
    3 WORD DestinationPort; //目的端口
    4 WORD Length; //长度
    5 WORD Checksum; //检验和
    6 };

     


    作者:Zplutor
    出处:http://www.cnblogs.com/zplutor/
    本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    浏览器驱动器下载地址
    7.pytest测试完成后通过allure生成测试报告
    6.pytest参数化操作
    5.pytest中fixture的使用(params参数)
    5.pytest中fixture的使用(scope参数)
    4.pytest中固定装置setup和teardown
    3.pytest函数级测试用例和类级测试用例
    2.pytest用例设计及运行
    1.pytest环境搭建
    Pycharm 常用快捷键
  • 原文地址:https://www.cnblogs.com/zplutor/p/2098641.html
Copyright © 2020-2023  润新知