在构造获取权限的UDP包的过程中遇到了两个麻烦的问题,就是在计算数据包的校验和的时候,需要将校验和作为int32整形来计算,需要截断数据,但是Lua是用double来存储数值的,所以需要简单写一个workaround方法来处理;并且校验和还需要进行一些bit操作,但是Lua的数值运算没有bit运算,所以这里需要导入nixio库的bit运算库。(当然,也可以写C,然后用Lua调用,但是这样不符合该应用的现状)
我们先假设有这样一个计算校验和算法的伪代码:
function getchecksum(bytes)
checksum = 666666
for b in bytes
if res > 0
checksum ^= checksum >> 2 + checksum << 5 + b
else
checksum ^= (checksum >> 2) | 0xC0000000 + checksum << 5 + b
return checksum & 0x7FFFFFFF
其中输入的参数是需要计算校验和的二进制字符串,checksum是int32整形。
bit运算
使用C语言编写的nixio的bit库。查看源码后发现:
/* 52 bit maximum precision */
#ifdef NIXIO_DOUBLE
#define NIXIO_BIT_BMAX 52
#define NIXIO_BIT_NMAX 0xfffffffffffff
#else
#define NIXIO_BIT_BMAX 32
#define NIXIO_BIT_NMAX 0xffffffff
#endif
如果Lua编译时使用了默认的double来保存数值的话,bit运算的最大数值为0xfffffffffffff(double的尾数位为52)。nixio在bit运算过程中,每个函数(例如bit.band(target, a1, a2 …))都将各参数从堆栈中取出来保存在类型为uint64_t 局部变量中,这样就可以通过for循环对这些整型局部变量进行bit运算,运算完毕后将结果压入堆栈作为函数返回。
源码中以一个宏定义来定义bit运算,BIT_XOP是C语言的bit运算操作符,NIXIO_BIT_XOP其实就是luaL_checknumber(见nixio.h),从堆栈中获取第i个参数,这个参数是数值参数,lua_gettop获取堆栈元素的个数(参数个数),nixio__pushnumber其实就是lua_pushnumber(见nixio.h),将oper作为函数返回值。
#define NIXIO_BIT_XOP(BIT_XOP) \
uint64_t oper = nixio__checknumber(L, 1); \
const int args = lua_gettop(L); \
\
for (int i = 2; i <= args; i++) { \
uint64_t oper2 = nixio__checknumber(L, i); \
oper BIT_XOP oper2; \
} \
\
nixio__pushnumber(L, oper); \
return 1;
然后任意的bit运算函数都以以下方式来定义(bit and为例):
static int nixio_bit_and(lua_State *L) {
NIXIO_BIT_XOP(&=);
}
这样,在Lua中只需要导入nixio的bit库后就可以进行bit运算了:
bit = require "nixio".bit
--[[以下operX是数值操作数,shift是移位的个数
bit.arshift (oper, shift) --对oper算数右移shift位
bit.band (oper1, oper2, ...) --对多个操作数进行逻辑与
bit.bnot (oper) --对oper取反
bit.bor (oper1, oper2, ...) --对多个操作数进行逻辑或
bit.bxor (oper1, oper2, ...) --对多个操作数进行逻辑异或
bit.cast (oper) --将oper转换为可以bit运算的数值
bit.check (bitfield, flag1, ...) --检查数值bitfield中某一位是否置1,例如bit.check(16, 0x10) 这将返回true
bit.div (oper1, oper2, ...) --地板除法,oper1/oper2/...
bit.lshift (oper, shift) --oper向左移shift位
bit.rshift (oper, shift) --oper向右移shift位
bit.set (bitfield, flag1, ...) --将某些位置为1,等价于bor
bit.unset (bitfield, flag1, ...) --将某些位置为0
--]]
处理int32运算
由于checksum是int32类型,所以在进行上述算法时有以下几点需要注意:
1. 每次bit运算后,由于Lua的bit有效位一般都是52位,所以需要对结果与0xffffffff进行逻辑与,防止32位以后的位数影响数值,这样每次bit运算后的结果都是真正的int32,只是Lua计算时需要将其转化罢了。
2. 数值计算时,需要将数值转化为int32。由于经过bit运算后,数值是32位的,但是Lua返回的是由uint64(见源码)转化而来的Lua数值类型,直接使用会等效的使用为uint32,这时可以使用bit向右移位运算获取其符号值,将其转化为正确的int32数值。
3. 在进行数值运算(加减乘除)后需要判断数值是否溢出(-2,147,483,648 <= int32 <= 2,147,483,647),溢出则进行截断,使用加上2的32次方或减去2的32次方即可。
于是,综上所述,这一个计算校验和的算法可以用Lua这样编写:
--转换无符号32位整数为int32
function u2i (uint)
local rs=uint
--获取符号位
local signed = bit.rshift(uint,31)
if signed > 0 then --负数
rs = bit.band(bit.bnot(uint), 0x7fffffff) + 1
rs = -1 * rs
end
return rs
end
--lua用double来保存数值,故进行数值运算时需要将checksum转化为int32
--不参与数值运算的数值不需要u2i,因为我们在bit运算时已经将其视为int32
function calchecksum(bytes)
local checksum = 0x666666
for k, v in ipairs(bytes) do
local rb = u2i(bit.band(bit.rshift(checksum, 2), 0xffffffff))
local lb = u2i(bit.band(bit.lshift(checksum, 5), 0xffffffff))
if u2i(checksum) < 0 then
rb = u2i(bit.band(bit.bor(rb, 0xc0000000), 0xffffffff))
end
local temp = rb + lb + v
--溢出,钟摆原理
if temp < 0x80000000 then
temp = temp + 0x100000000
elseif temp > 0x7fffffff then
temp = temp - 0x100000000
end
checksum = bit.band(bit.bxor(checksum, temp), 0xffffffff)
--print(checksum)
end
return bit.band(checksum, 0x7fffffff)
end
此时,校验和算法完成,可以进行udp包的构造了,这里我用table来构造udp包,每个索引保存一个字节数值(byte),根据udp包的结构构造出一个有56个元素的table,将其送入calchecksum函数计算出校验和后,使用string.char函数和字符串连接操作符.. 将table中的数据结合成为二进制字符串data,最终再将校验和以小端模式pack进data中,获取拨号权限的udp包构造完毕。