第38章 网络地址计算实例

众所周知,IPv4下的TCP/IP地址由4个数字组成,每个数字都在0~255(十进制)之间。所以IPv4的地址可以表示为4字节数据。4字节数据就是一个32位数据。因此IPv4的主机地址、子网掩码和网络地址都可以表示为一个32位的整数。

从使用者的角度来看,子网掩码由4位数字组成,写出来大致就是255.255.255.0这类形式的数字。但是网络工程师或者系统管理员更喜欢使用更为紧凑的表示方法,也就是CIDR[1]规范的“/8”“/16”一类的表示方法。CIDR格式的子网掩码从子网掩码的MSB(最高数权位)开始计数,统计子网掩码里面有多少个1并将统计数字转换为10进制数。

CIDK规范的掩码

数字空间

可用地址(个)[2]

十进制子网掩码

十六进制子网掩码

/30

4

2

255.255.255.252

fffffffc

/29

8

6

255.255.255.248

fffffff8

/28

16

14

255.255.255.240

fffffff0

/27

32

30

255.255.255.224

ffffffe0

/26

64

62

255.255.255.192

ffffffc0

/24

256

254

255.255.255.0

ffffff00

C类网段

/23

512

510

255.255.254.0

fffffe00

/22

1024

1022

255.255.252.0

fffffc00

/21

2048

2046

255.255.248.0

fffff800

/20

4096

4094

255.255.240.0

fffff000

/19

8192

8190

255.255.224.0

ffffe000

/18

16384

16382

255.255.192.0

ffffc000

/17

32768

32766

255.255.128.0

ffff8000

/16

65536

65534

255.255.0.0

ffff0000

B类网段

/8

16777216

16777214

255.0.0.0

ff000000

A类网段

这里举一个简单的例子:将子网掩码应用到主机地址,从而计算的网络地址。

#include <stdio.h>
#include <stdint.h>

uint32_t form_IP (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t ip4)
{
          return (ip1<<24) | (ip2<<16) | (ip3<<8) | ip4;
};

void print_as_IP (uint32_t a)
{
          printf ("%d.%d.%d.%d\n",
                   (a>>24)&0xFF,
                   (a>>16)&0xFF,
                   (a>>8)&0xFF,
                   (a)&0xFF);
};

// bit=31..0
uint32_t set_bit (uint32_t input, int bit)
{
          return input=input|(1<<bit);
};

uint32_t form_netmask (uint8_t netmask_bits)
{
          uint32_t netmask=0;
          uint8_t i;

          for (i=0; i<netmask_bits; i++)
                    netmask=set_bit(netmask, 31-i);

          return netmask;
};

void calc_network_address (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t  ip4, uint8_t netmask_bits)
{
          uint32_t netmask=form_netmask(netmask_bits);
          uint32_t ip=form_IP(ip1, ip2, ip3, ip4);
          uint32_t netw_adr;

          printf ("netmask=");
          print_as_IP (netmask);

          netw_adr=ip&netmask;

          printf ("network address=");
          print_as_IP (netw_adr);
};

int main()
{
          calc_network_address (10, 1, 2, 4, 24);       // 10.1.2.4, /24
          calc_network_address (10, 1, 2, 4, 8);       // 10.1.2.4, /8
          calc_network_address (10, 1, 2, 4, 25);      // 10.1.2.4, /25
          calc_network_address (10, 1, 2, 64, 26);     // 10.1.2.4, /26
};

38.1 计算网络地址函数calc_network_address()

计算网络地址函数calc_network_address()实现起来非常简单:它将主机地址和网络子网掩码进行AND与运算,得到的结果就是网络的实际地址。

指令清单38.1 MSVC 2012采用参数/Ob0优化

 1  _ip1$ = 8              ; size = 1
 2  _ip2$ = 12             ; size = 1
 3  _ip3$ = 16             ; size = 1
 4  _ip4$ = 20             ; size = 1
 5  _netmask_bits$ = 24    ; size = 1
 6  _calc_network_address PROC
 7         push     edi
 8         push     DWORD PTR _netmask_bits$[esp]
 9         call     _form_netmask
10         push     OFFSET $SG3045   ; 'netmask='
11         mov      edi, eax
12         call     DWORD PTR __imp__printf
13         push     edi
14         call     _print_as_IP
15         push     OFFSET $SG3046   ; 'network address='
16         call     DWORD PTR __imp__printf
17         push     DWORD PTR _ip4$[esp+16]
18         push     DWORD PTR _ip3$[esp+20]
19         push     DWORD PTR _ip2$[esp+24]
20         push     DWORD PTR _ip1$[esp+28]
21         call     _form_IP
22         and      eax, edi         ; network address = host address & netmask
23         push     eax
24         call     _print_as_IP
25         add      esp, 36
26         pop      edi
27         ret      0
28  _calc_network_address ENDP

在第22行,我们可以看到最为重要的运算指令AND。就是它计算出了网络地址,实现了核心功能。

38.2 函数form_IP()

form_IP()函数将IP的4个字节转换成一个32位数值。

它的运算流程如下:

经MSVC 2012进行非优化编译,可得到下述指令:

指令清单38.2 非优化的MSVC2012的实现

; denote ip1 as "dd", ip2 as "cc", ip3 as "bb", ip4 as "aa".
_ip1$ = 8        ; size = 1
_ip2$ = 12       ; size = 1
_ip3$ = 16       ; size = 1
_ip4$ = 20       ; size = 1
_form_IP PROC
         push    ebp
         mov     ebp, esp
         movzx   eax, BYTE PTR _ip1$[ebp]
         ; EAX=000000dd
         shl     eax, 24
         ; EAX=dd000000
         movzx   ecx, BYTE PTR _ip2$[ebp]
         ; ECX=000000cc
         shl     ecx, 16
         ; ECX=00cc0000
         or      eax, ecx
         ; EAX=ddcc0000
         movzx   edx, BYTE PTR _ip3$[ebp]
         ; EDX=000000bb
         shl     edx, 8
         ; EDX=0000bb00
         or      eax, edx
         ; EAX=ddccbb00
         movzx   ecx, BYTE PTR _ip4$[ebp]
         ; ECX=000000aa
         or      eax, ecx
         ; EAX=ddccbbaa
         pop     ebp
         ret     0
_form_IP ENDP

这里操作的顺序不同。当然这不会影响最后的运算结果。

在启用优化选项之后,MSVC 2012会生成另一种算法的应用程序:

指令清单38.3 优化的MSVC2012带参数/Ob0的实现

; denote ip1 as "dd", ip2 as "cc", ip3 as "bb", ip4 as "aa".
_ip1$ = 8        ; size = 1
_ip2$ = 12       ; size = 1
_ip3$ = 16       ; size = 1
_ip4$ = 20       ; size = 1
_form_IP PROC
        movzx   eax, BYTE PTR _ip1$[esp-4]
        ; EAX=000000dd
        movzx   ecx, BYTE PTR _ip2$[esp-4]
        ; ECX=000000cc
        shl     eax, 8
        ; EAX=0000dd00
        or      eax, ecx
        ; EAX=0000ddcc
        movzx   ecx, BYTE PTR _ip3$[esp-4]
        ; ECX=000000bb
        shl     eax, 8
        ; EAX=00ddcc00
        or      eax, ecx
        ; EAX=00ddccbb
        movzx   ecx, BYTE PTR _ip4$[esp-4]
        ; ECX=000000aa
        shl     eax, 8
        ; EAX=ddccbb00
        or      eax, ecx
        ; EAX=ddccbbaa
        ret     0
_form_IP ENDP

这个实现过程还可以描述为:每个字节都写入到其返回值的最低8个比特位,并且每次左移一个字节,并将其与返回值做或操作。重复四次,就能完成函数功能。

就这样了。然而遗憾的是,可能没其他的办法来实现以上的逻辑了。据笔者所知,目前的CPU及其ISA还不能把既定比特位或者字节直接复制到其他类型数据里。所以一般都是通过位移和OR或运算才能把IP地址转换为32位数据。

38.3 函数print_as_IP()

函数print_as_IP()实现的功能与上面函数完全相反,它将一个32位的数值切分成4个字节。切分过程比较简单:只需要将输入的数值分别位移24位、16位、8位或者0位,取最低字节的0到7位即可。

指令清单38.4 非优化MSVC 2012

_a$ = 8                   ; size = 4
_print_as_IP PROC
         push    ebp
         mov     ebp, esp
         mov     eax, DWORD PTR _a$[ebp]
         ; EAX=ddccbbaa
         and     eax, 255
         ; EAX=000000aa
         push    eax
         mov     ecx, DWORD PTR _a$[ebp]
         ; ECX=ddccbbaa
         shr     ecx, 8
         ; ECX=00ddccbb
         and     ecx, 255
         ; ECX=000000bb
         push    ecx
         mov     edx, DWORD PTR _a$[ebp]
         ; EDX=ddccbbaa
         shr     edx, 16
         ; EDX=0000ddcc
         and     edx, 255
         ; EDX=000000cc
         push    edx
         mov     eax, DWORD PTR _a$[ebp]
         ; EAX=ddccbbaa
         shr     eax, 24
         ; EAX=000000dd
         and     eax, 255 ; probably redundant instruction
         ; EAX=000000dd
         push    eax
         push    OFFSET $SG2973 ; '%d.%d.%d.%d'
         call    DWORD PTR __imp__printf
         add     esp, 20
         pop     ebp
         ret     0
_print_as_IP ENDP

优化MSVC 2012程序做的和上面的一样,但是它不会重新加载输入值。

指令清单38.5 优化MSVC 2012 /Ob0

_a$ = 8          ; size = 4
_print_as_IP PROC
         mov     ecx, DWORD PTR _a$[esp-4]
         ; ECX=ddccbbaa
         movzx   eax, cl
         ; EAX=000000aa
         push    eax
         mov     eax, ecx
         ; EAX=ddccbbaa
         shr     eax, 8
         ; EAX=00ddccbb
         and     eax, 255
         ; EAX=000000bb
         push    eax
         mov     eax, ecx
         ; EAX=ddccbbaa
         shr     eax, 16
         ; EAX=0000ddcc
         and     eax, 255
         ; EAX=000000cc
         push    eax
         ; ECX=ddccbbaa
         shr     ecx, 24
         ; ECX=000000dd
         push    ecx
         push    OFFSET $SG3020 ; '%d.%d.%d.%d'
         call    DWORD PTR __imp__printf
         add     esp, 20
         ret     0
_print_as_IP ENDP

38.4 form_netmask()函数和set_bit()函数

form_netmask()函数从网络短地址CIDR中获取网络子网掩码。当然,或许事先计算出一个查询表、转换的时候进行表查询的速度可能会更快。但是为了演示位移运算的特征,本节特意采用了这种现场计算的转换方法。我们这里还编写了一个函数set_bit()。虽说格式转换这种底层运算本来不应当调用其他函数了,但是笔者相信set_bit()函数可以提高代码的可读性。

指令清单38.6 优化MSVC 2012 /Ob0

_input$ = 8          ; size = 4
_bit$ = 12           ; size = 4
_set_bit PROC
        mov     ecx, DWORD PTR _bit$[esp-4]
        mov     eax, 1
        shl     eax, cl
        or      eax, DWORD PTR _input$[esp-4]
        ret     0
_set_bit ENDP

_netmask_bits$ = 8                ; size = 1
_form_netmask PROC
        push    ebx
        push    esi
        movzx   esi, BYTE PTR _netmask_bits$[esp+4]
        xor     ecx, ecx
        xor     bl, bl
        test    esi, esi
        jle     SHORT $LN9@form_netma
        xor     edx, edx
$LL3@form_netma:
        mov     eax, 31
        sub     eax, edx
        push    eax
        push    ecx
        call    _set_bit
        inc     bl
        movzx   edx, bl
        add     esp, 8
        mov     ecx, eax
        cmp     edx, esi
        jl      SHORT $LL3@form_netma
$LN9@form_netma:
        pop     esi
        mov     eax, ecx
        pop     ebx
        ret     0
_form_netmask ENDP

set_bit()函数的功能十分单一。它将输入值左移既定的比特位,接着将位移运算的结果与输入值进行或OR运算。而后form_mask()函数通过循环语句重复调用set_bit()函数,借助循环控制变量netmask_bits设置子网掩码里数值为1的各个比特位。

38.5 总结

上述程序的结果如下所示。

netmask=255.255.255.0
network address=10.1.2.0
netmask=255.0.0.0
network address=10.0.0.0
netmask=255.255.255.128
network address=10.1.2.0
netmask=255.255.255.192
network address=10.1.2.64

[1] CIDR是Classless Inter-Domain Routing的缩写,即无类域间路由。

[2] 可用地址二数字空间-2。