众所周知,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
};
计算网络地址函数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。就是它计算出了网络地址,实现了核心功能。
form_IP()函数将IP的4个字节转换成一个32位数值。
它的运算流程如下:
给返回值分配一个变量,并赋值为0。
取数权最低的第4个字节,与返回值0进行OR/或操作,即可得到含有第4字节信息的32位值。
取第3个字节,左移8位,以生成0x0000bb00(其中bb就是这步读取的第三字节)这种形式的数值。此后与返回值进行OR/或运算。如果上一步的值如果是0x000000aa的话,在执行OR或操作后,就会得到0x0000bbaa这样的返回值。
依此类推。取第2个字节,左移16位,生成0x00cc0000这样一个含有第2字节的32位值,再进行OR/或运算。由于以上一步的返回值应当是0x0000bbaa,因此本次运算的结果会是0x00ccbbaa。
同理。取最高位,左移24位,以生成0xdd000000这样一个含有第一字节信息的32位值,再进行OR/或运算。由于上一步的返回值是0x00ccbbaa,因此最终的结果的值就是0xddccbbaa这样的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位数据。
函数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
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的各个比特位。
上述程序的结果如下所示。
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。