SNMP++ 02-SNMP中INTEGER的BER编码与解码

阅读完本文你可以学到:

(1)BER 中 INTEGER 的编码规则(其中1、2、3主要引自《ASN.1编码规则详解.doc》(作者不详,该文档可在CSDN资源中搜索到))。

(2)SNMP 中 INTEGER 的编码及解码实现(主要参考 net-snmp源码和 snmp++源码)。本文仅对编码相关函数进行了详细的解释。理解它,或许是我们走向自己实现 SNMP 协议的第一步。

特别声明:

(1)感谢《ASN.1编码规则详解.doc》作者所做的工作。

(2)感谢所有为 net-snmp 及 snmp++ 作出贡献的 coders 及相关人士。

(3)文中的所有观点仅供参考。


一、BER 中 INTEGER 的编码规则

1、BER是什么

BER(Basic Encoding Rules)是 ASN.1 中最早定义的编码规则,在讨论编码规则时,我们是基于正确的抽象描述上。BER 传输语法的格式一直是 TLV 三元组<Type, Length, Value>。TLV 每个域都是一系列八位组,对于组合结构,其中V还可以是 TLV 三元组。BER 传输语法 是基于八位组(为了避免不同系统上的混淆,没有采用 Byte 为单位)的自定界的 编码,因为其中 L 明确界定了八位组的长度。BER 是大端编码的,其八位组的高位比特在左手边。(注:术语“大端”和“小端”表示多字节值的哪一端(小端或大端))存储在该值的起始地址。引自《UNP卷一》)。

2、Tag

INTEGER 对应的 Tag 为 0x02。

3、Length

Length有三种形式:定长短格式、定长长格式和变长格式。

定长短格式:采用定长方式,当长度不大于127个八位组时,Length 只在一个八位组中编码。

定长长格式:采用定长方式,当长度大于127个八位组时,Length 在多个八位组中编码,此时第一个八位组低七位(bit0~bit6)表示的是 Length 所占的长度,第一个八位组的最高位 bit7 为1。

变长格式:采用变长方式,Length 所在八位组固定编码为 0x80,但在 Value 编码结束后以两个 0x00 结尾。这种方式使得可以在编码没有完全结束的情况下,可以先发送部分消息个对方。

4、INTEGER 类型的编码

按照编码规则,如果一个数值编码后大于等于两个八位组,则要求编码后的结果从 MSB(Most Significant Bit 最高有效位)开始的9个位不能是连续的0或1。

该规则是为了尽最大的可能减少 INTEGER 编码后所占的八位组个数。

可以这样理解(仅供参考)(以32位 Windows 系统上的 int 类型举例):

在 32位的 Windows 上,一个 int 类型的值占 4个字节,即占 32位。那么它对应的八位组个数就是 32 / 8 = 4。现在我想对 1进行 BER 编码,那么应该这样做:1 的十六进制为 0x00000001,如果按照通常的编码,那它编码后的结果就是 0x00000001(对应 4个八位组),但是 BER 则要求编码后的结果从 MSB开始的9个位不能是连续的0或1,这就要求我们去除一些东西。如果按照 BER 的编码规则,那它编码后的结果就是 0x01(对应 1个八位组)。

相应地,在解码时,如果待解码数值的 MSB 为 0,则如果有需要的话,它的前面应填充 0;如果待解码数值的 MSB 为 1,则如果有需要的话,它的前面应填充 1。

二、SNMP中INTEGER的编码与解码实现(在 VS2013 下演示)

1. BER(Basic Encoding Rules)所处的位置,如图1-1:

SNMP++ 02-SNMP中INTEGER的BER编码与解码_第1张图片

图1-1

2.  INTEGER 的 BER 编码与解码代码,并给出测试程序。

/************************************************************************/
/* asn1.h                                                                   */
/************************************************************************/
#pragma once

typedef unsigned char u_char;
typedef unsigned long u_long;

#define ASN_LONG_LEN 0x80

u_char* asn_build_int(u_char *data, size_t *datalength, const u_char type, long integer);
u_char* _asn_build_header(u_char *data, size_t *datalength, const u_char type, size_t length);
u_char* _asn_build_length(u_char *data, size_t *datalength, size_t length);

u_char* asn_parse_int(u_char* const data, size_t *datalength, u_char *type, long *intp);
u_char* _asn_parse_length(u_char *data, u_long *length);


/************************************************************************/
/* asn1.cpp                                                                     */
/************************************************************************/
#include "stdafx.h"
#include "asn1.h"

u_char* asn_build_int(u_char *data, size_t *datalength, const u_char type, long integer)
{
	if (nullptr == data || nullptr == datalength || *datalength < 1)
		return nullptr;

	size_t intsize = sizeof(integer);
	u_long mask = ((u_long)0x1FF) << (8 * (sizeof(long) - 1) - 1);
	/* mask is OxFF800000 on a 32 bit big-endian machine */
	while (intsize > 1
		&& (((integer & mask) == 0) || ((integer & mask) == mask)))
	{
		integer <<= 8;
		--intsize;
	}
	data = _asn_build_header(data, datalength, type, intsize);
	if (nullptr == data)
		return nullptr;

	if (*datalength < intsize)
		return nullptr;
	(*datalength) -= intsize;
	for (size_t i = sizeof(integer)-1; intsize--; --i)
	{
		*data++ = *((u_char*)&integer + i);
	}
	return data;
}

static u_char* _asn_build_header(u_char *data, size_t *datalength, const u_char type, size_t length)
{
	if (nullptr == data || nullptr == datalength || *datalength < 1)
		return nullptr;

	*data++ = type;
	--(*datalength);
	return _asn_build_length(data, datalength, length);
}

static u_char* _asn_build_length(u_char *data, size_t *datalength, size_t length)
{
	if (nullptr == data || nullptr == datalength)
		return nullptr;

	const u_char *startdata = data;

	if (length < 0x80)
	{
		if (*datalength < 1)
			return nullptr;
		*data++ = (u_char)length;
	}
	else if (length <= 0xFF)
	{
		if (*datalength < 2)
			return nullptr;
		*data++ = (u_char)(0x01 | ASN_LONG_LEN);
		*data++ = (u_char)length;
	}
	else if (length <= 0xFFFF)
	{			/* 0xFF < length <= 0xFFFF */
		if (*datalength < 3)
			return nullptr;
		*data++ = (u_char)(0x02 | ASN_LONG_LEN);
		*data++ = (u_char)((length >> 8) & 0xFF);
		*data++ = (u_char)(length & 0xFF);
	}
	else
	{
		return nullptr;		/* "length" too big */
	}

	*datalength -= (data - startdata);
	return data;
}


u_char* asn_parse_int(u_char* const data, size_t *datalength, u_char *type, long *intp)
{
	if (nullptr == data || nullptr == datalength || *datalength < 1 
		|| nullptr == type || nullptr == intp)
		return nullptr;

	u_char *bufp = data;
	u_long asn_length;

	if (*bufp != 0x02)
		return nullptr;
	*type = *bufp++;

	bufp = _asn_parse_length(bufp, &asn_length);
	if (nullptr == bufp)
		return nullptr;
	if (asn_length > sizeof(*intp))
		return nullptr;
	
	long value = 0;
	if (*bufp & 0x80)
		value = -1;			/* integer is negative */
	while (asn_length--)
	{
		value = (value << 8) | *bufp++;
	}
	*intp = value;
	
	(*datalength) -= (bufp - data);
	return bufp;
}

static u_char* _asn_parse_length(u_char *data, u_long *length)
{
	if (nullptr == data || nullptr == length)
		return nullptr;

	u_char lengthbyte = *data;

	if (lengthbyte < 0x80)
	{					/* short asnlength */
		*length = *data++;
	}
	else if (lengthbyte == 0x80)
	{
		return nullptr;			/* not support */
	}
	else
	{					/* long asnlength */
		lengthbyte &= ~ASN_LONG_LEN;
		if (lengthbyte > 0x2)
			return nullptr;		/* asnlength too big */
		*length = 0;
		while (lengthbyte--)
		{
			*length <<= 8;
			*length = *++data;
		}
		++data;
		if ((long)*length < 0)
			return nullptr;		/* negative data length */
	}
	
	return data;
}

// snmp_get.cpp : 测试程序
//

#include "stdafx.h"
#include "asn1.h"
#include <stdio.h>

void print(const u_char *data, size_t datalength);

int _tmain(int argc, _TCHAR* argv[])
{
	u_char buf[100];
	size_t validlen = sizeof(buf);

	asn_build_int(buf, &validlen, 0x02, 65539);
	print(buf, sizeof(buf) - validlen);

	validlen = sizeof(buf);
	u_char type;
	long integer;
	asn_parse_int(buf, &validlen, &type, &integer);
	printf("0x%.2X %d \n", type, integer);
	
	return 0;
}

void print(const u_char *data, size_t datalength)
{
	if (nullptr == data || datalength < 1)
		return;

	for (size_t i = 0; i < datalength; ++i)
	{
		printf("0x%.2X ", data[i]);
	}
	printf("\n");
}

3. 实现细节(这部分是以解释代码的方式展开的)

3.1 INTEGER 的 BER 编码

(1)涉及的函数:

u_char* asn_build_int(u_char *data, size_t *datalength, const u_char type, long integer);
u_char* _asn_build_header(u_char *data, size_t *datalength, const u_char type, size_t length);
u_char* _asn_build_length(u_char *data, size_t *datalength, size_t length);

(注:(1)u_char 是 unsigned char 的别名,只为代码看起来更清爽,别无其他意图。在任何出现 u_char 的地方你完全可以用 unsigned char 代替。(2)如果函数名是以‘_’开头的,那意味着我们不想让该函数被外部调用,然而这只是我们的一厢情愿。实际上,我们通过将该函数声明为 static,才能真正达到限制该函数不能被外部调用的目的。)

(2)函数 asn_build_int

SNMP++ 02-SNMP中INTEGER的BER编码与解码_第2张图片

函数的功能、参数的意义及返回值的解析(引自 net-snmp 的 asn1.c,略有修改):

/**

 * @internal 

 * asn_build_int - builds an ASN object containing an integer.

 *

 *  On entry, datalength is input as the number of valid bytes following

 *   "data".  On exit, it is returned as the number of valid bytes

 *   following the end of this object.

 *

 *  Returns a pointer to the first byte past the end

 *   of this object (i.e. the start of the next object).

 *  Returns NULL on any error.

 * 

 * 

 * @param data         IN - pointer to start of output buffer

 * @param datalength   IN/OUT - number of valid bytes left in buffer

 * @param type         IN  - asn type of objec

 * @param integer     IN  - long integer                 //@param intp         IN - pointer to start of long integer

 // * @param intsize      IN - size of input buffer(我们并没有用到)

 * 

 * @return  Returns a pointer to the first byte past the end

 *          of this object (i.e. the start of the next object).

 *          Returns NULL on any error.

 */

参数解析补充:

1)type 使用 const 限定符修饰,是基于我们认为不应该在该函数中做任何“意外地”修改。当然我们也可以不使用 const,在这种情况下,如果 type 在编码前做了任何修改,我们都将处于不知情的状况,并且代码仍能正常执行,问题可能在我们解码时才会显现出来。

2)最后一个参数是 long interger,而在 net-snmp 实现中使用的是 long *intp。对于 long 这样的内置类型,我想不出什么好理由使用指针去引用它的内容(这一点亦可参考《Effective C++》中的有关介绍)。如果你想使用 long *intp,尽管使用吧!但在引用它的内容时,最好先判断该指针是否为 NULL。

3)在 net-snmp 中,函数 asn_build_int 的最后一个参数为 size_t intsize,而在 snmp++ 中没有使用该参数。是的,我想不出任何说服我使用该参数的理由。

函数体解析:

3~4行,用于对输入的参数值进行最基本的有效性检查,这些检查项显而易见。值得关注的是,*datalength < 1 必须置于 nullptr == datalength 之后。很可惜该函数在 net-snmp 中的实现并未进行这些检查。不过,从代码健壮性的角度开说,我建议在使用指针之前,最好先检查它是否为 NULL。

第 6行,intsize 的初始值(由第6行确定)表示即将进行编码的整型值所占的八位组个数,后期的值(由9~14行确定)表示去除从 MSB 开始的9个位连续的0或1后,占用的八位组个数。它的最小值是1(对应一个八位组),此时已经满足了 BER 中对 INTEGER 的编码规则。另外,在 net-snmp 及 snmp++ 中是使用 sizeof(long) 初始化 intsize 的。我改用 sizeof(integer) 初始化 intsize 是基于这样的考虑:有一天,我想把 integet 的类型由 long 改为诸如 long long 或 short 之类的类型时,在我对参数类型进行修改后,函数体中的代码可能引起相应地修改。

第 7行,mask 的作用是用于检查一个数值的高9位是否为连续的0或1,所以 mask 的高9位必须全为1。在 32位大端系统上,mask 的值为 0xFF800000。我们不能将 0xFF800000 直接赋值给 mask,如果这样做的话, 0xFF800000 在64位机器上的实际值是 0x00000000FF800000。显然 mask 的高9位已不是全1了。

9~14行,意在得到 INTEGER 的 Length 域大小(由 intsize 记录这个值)。此后,Value 域的值就是 integer 从最高位开始的 intsize 个八位组(这会在22~25行中用到)。

15~17行,构建 INTEGER TLV三元组中 Tag 域和 Length 域。如果构建成功(返回值不为 nullptr),则返回值意味着指向 Value 域的第一个八位组,此时  Value 域尚待构建。datalength 则指示了目前 data 中还剩多少个八位组可用。

19~20行,检查 data 中可用的八位组是否可以容纳下 INTEGER 编码后的结果INTEGER 编码后的结果所占的八位组个数由 intsize 指示

第 21行,在装载 Value 域后,data 中还剩的可以使用的八位组个数。

第 22~25行,装载 Value 域。这里的 Value 域是由9~14行得到的。这里的装载方式与 net-snmp 和 snmp++ 中的都不同。

(3)函数 _asn_build_header

SNMP++ 02-SNMP中INTEGER的BER编码与解码_第3张图片

函数的功能、参数的意义及返回值的解析(引自 net-snmp 的 asn1.c):

/**

 * @internal

 * asn_build_header - builds an ASN header for an object with the ID and

 * length specified.

 *

 *  On entry, datalength is input as the number of valid bytes following

 *   "data".  On exit, it is returned as the number of valid bytes

 *   in this object following the id and length.

 *

 *  This only works on data types < 30, i.e. no extension octets.

 *  The maximum length is 0xFFFF;

 *

 *  Returns a pointer to the first byte of the contents of this object.

 *  Returns NULL on any error.

 *

 * @param data         IN - pointer to start of object

 * @param datalength   IN/OUT - number of valid bytes left in buffer

 * @param type         IN - asn type of object

 * @param length       IN - length of object

 * @return Returns a pointer to the first byte of the contents of this object.

 *          Returns NULL on any error.

 */

函数体解析:

3~4行,用于对输入的参数值进行最基本的有效性检查。在 net-snmp 的函数 asn_build_header 实现中,它检查了 *datalength 是否小于1的情况,但依然没有判断指针是否有效。我想他检查 *datalength 是否小于1的情况是可能考虑了因素:*datalength 的类型为 size_t,如果 *datalength 为0时,做自减操作将导致无定义的结果。

第6 行,填充 Tag 域。INTEGER 对应的 Tag 为  0x02。并让 data 指向 Length 域的第一个八位组。

第7 行,在填充 Tag 域后,data 中还剩的可以使用的八位组个数。

第8 行,构建 Length 域。如果构建成功(返回值不为 nullptr),则返回值意味着指向 Value 域的第一个八位组。*datalength 则指示了目前 data 中还剩多少个八位组可用。

(4)函数 _asn_build_header

SNMP++ 02-SNMP中INTEGER的BER编码与解码_第4张图片

函数的功能、参数的意义及返回值的解析(引自 net-snmp 的 asn1.c):

/**

 * @internal

 * asn_build_length - builds an ASN header for a length with

 * length specified.

 *

 *  On entry, datalength is input as the number of valid bytes following

 *   "data".  On exit, it is returned as the number of valid bytes

 *   in this object following the length.

 *

 *

 *  Returns a pointer to the first byte of the contents of this object.

 *  Returns NULL on any error.

 *

 * @param data         IN - pointer to start of object

 * @param datalength   IN/OUT - number of valid bytes left in buffer

 * @param length       IN - length of object

 *

 * @return Returns a pointer to the first byte of the contents of this object.

 *         Returns NULL on any error.

 */

函数体解析:

3~4行,用于对输入的参数值进行最基本的有效性检查。多么显而易见!

第6 行,用变量 startdata 保存 data 指针的初始值,startdata 将在函数结束前计算 *dataength 时用到。另外,我用 const 限定符修饰 startdata,以确保该值一直保持着它的初值。这是有必要的!因为当我们再次使用该变量时,已经达到了函数体的尾声,在此期间很容易发生被改变的情况,尽管我们不会这么做,但我们不能保证后期的开发人员不会这么做。另外,变量 startdata 并不是必须存在的,但它的存在给我们带来诸多好处。如果不使用 startdata 变量,我们必须在不同的情况下,采用不同的计算 *datalenght 的规则。如果我们再增加新的情况,我们还得增加在新情况中计算 *datalenght 的新规则。正是该变量让我们免去了这些不必要的麻烦,这是最实在的好处!另外,我们的代码可以看起来跟简洁易懂一些。

8~13行,这部分对应 Length 域采用定长短格式。此时,Length 域占用一个八位组。所以它进行了 *datalength < 1 的检查以确保 data 中至少有一个八位组可用。

14~20行,这部分对应 Length 域采用定长长格式。此时, 0x80 <= Length  <= 0xFF,Length 域占用两个八位组,第一个八位组用以指定该八位组后面的1个八位组是表示长度的。第二个八位组表示 Value 域的长度。

21~28行,这部分对应 Length 域采用定长长格式。此时, 0xFF <= Length  <= 0xFFFF,Length 域占用三个八位组,第一个八位组用以指定该八位组后面的2个八位组是表示长度的。第2个和第3个八位组组合起来表示 Value 域的长度。

29~32行,由于本函数旨在支持的最大数据长度为 0xFFFF。所以,当程序运行到此处时,就返回错误。当然,你可以支持 0xFFFFFF甚至更大,就像 snmp++ 中的实现那样。

第34 行,构建 Length 域后,*datalength 指示目前 data 中还剩多少个八位组可用。


至此,INTEGER 的 BER 编码过程就分析完了。理解了编码过程,至于解码过程,在代码的参考下,我想也不在话下了。

你可能感兴趣的:(编码,Integer,snmp,解码,BER)