阅读完本文你可以学到:
(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:
图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
函数的功能、参数的意义及返回值的解析(引自 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
函数的功能、参数的意义及返回值的解析(引自 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
函数的功能、参数的意义及返回值的解析(引自 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.
*/
函数体解析:
第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 编码过程就分析完了。理解了编码过程,至于解码过程,在代码的参考下,我想也不在话下了。