Lua 第13部分 位和字节
13.1 位运算
Lua 语言从 5.3 版本开始提供了针对数值类型的一组标准位运算符。与算术运算符不同的是,位运算符只能用于整型数。位运算符包括 &( 按位与)、|(按位或)、~(按位异或)、>>(逻辑右移)、<<(逻辑左移)和一元运算符 ~(按位取反)。(请注意,在其他一些语言中, 异或运算符为 ^ , 而在 Lua 语言中 ^ 代表幂运算。)
string.format("%x", 0xff & 0xabcd)
> cd
string.format("%x", 0xff | 0xabcd)
> abff
string.format("%x", 0xaaaa ~ -1)
> ffffffffffff5555
string.format("%x", ~ 0)
> ffffffffffffffff
所有的位运算都针对构成一个整型数的所有位。在标准 Lua 中,也就是 64 位。 这对于使用 32 位整型数的算法可能会成为问题(例如,SHA-2 密码散列算法)。不过,要操作 32位整型数也不难。除了右移操作外,只要忽略高 32 位,那么所有针对 64 位整型数的操作与针对 32 位整型数的操作都一样。这对于加法、减法和乘法都有效。因此,在操作 32 位整型数时,只需要在进行右移前抹去高 32 位即可(对于这类计算很少会做除法)。
两个移位操作都会用 0 填充空出的位,这种行为通常被称为逻辑移位。Lua 语言没有提供算术右移,即使用符号位填充空出的位。 我们可以通过向下取整除法,除以合适的 2 的整数次幕来实现算术右移( 例如, x//16 与算术右移 4 位等价)。
移位数是负数表示向相反的方向移位,即 a >> n 与 a << -n 等价 :
string.format("%x", 0xff << 12)
> ff000
string.format("%x", 0xff >> -12)
> ff000
如果移位数等于或大于整型表示的位数(标准 Lua 为 64 位, 精简 Lua 为 32 位),由于所有的位都被从结果中移出了,所以结果是 0:
string.format("%x", -1 << 80)
> 0
13.2 无符号整型数
整型表示中使用一个比特来存储符号位。因此,64 位整型数最大可以表示 而不是
。 通常,这点区别是无关紧要的 ,因为
已经相当大了 。 不过,由于我们可能需要处理使用无符号整型表示的外部数据或实现一些需要 64 位整型数的算法,因而有时也不能浪费这个符号位。此外,在精简 Lua 中,这种区别可能会很重要。例如,如果用一个32 位有符号整型数表示文件中的位置,那么能够操作的最大文件大小就是 2GB ;而一个无符号整型数能操作的最大文件大小则是有符号整型数的 2 倍,即 4GB 。
Lua 语言不显式支持无符号整型数。不过尽管如此,只要稍加注意,在 Lua 语言中处理无符号整型数并不难,我们后续就会看到 。
虽然看上去不太友好,但可以直接写出比大的常量 :
x = 13835058055282163712 -- 3 << 62
x --> -4611686018427387904
这里的问题并不在于常量本身,而在于 Lua 语言输出常量的方式 :默认情况下,打印数值时是将其作为有符号整型数进行处理的 。我们可以使用选项%u或%x在函数 string.format 中指定以无符号整型数进行输出:
string.format("%u", x)
> 13835058055282163712
string.format("%0x%X", x)
> 0xC000000000000000
根据有符号整型数的表示方式( 2 的补码),加法 、减法和乘法操作对于有符号整型数和无符号整型数是一样的:
string.format("%u", x)
> 13835058055282163712
string.format("%u", x + 1)
> 13835058055282163713
string.format("%u", x - 1)
> 13835058055282163711
关系运算对于有符号整型数和无符号整型数是不一样的,当比较具有不同符号位的整型数时就会出现问题。对于有符号整型数而言,符号位被置位的整数更小,因为它代表的是负数 :
> 0x7fffffffffffffff < 0x8000000000000000 --> fals
如果把两个整型数都当作无符号的,那么结果显然是不正确的 。因此,我们需要使用一种不同的操作来比较无符号整型数。 Lua5.3 提供了函数 math.ult 来完成这个需求 :
> math.ult ( 0x7fffffffffffffff, 0x8000000000000000) --> true
另一种方法是在进行有符号比较前先用掩码掩去两个操作数的符号位:
> mask = 0x8000000000000000
> (0x7fffffffffffffff ~ mask) < (0x8000000000000000 ~ mask)
--> true
无符号除法和有符号除法也不一样,示例 13.1 给出了一种无符号除法的算法。
function udiv (n, d)if d < 0 thenif math.ult(n, d) then return 0else return 1endendlocal q = ((n >> 1) // d) << 1local r = n - q * dif not math.ult(r, d) then q = q + 1 endreturn q
end
第一个比较( d<0 )等价于比较 d 是否大于。 如果大于,那么商只能是 1 (如果 n 等于或大于 d )或 0 。 否则,我们使被除数除以 2 ,然后除以除数,再把结果乘以 2 。 右移 1 位等价于除以 2 的无符号除法,其结果是一个非负有符号整型数。后续的左移则纠正了商,还原了之前的除法。
总体上说, floor (floor(n/2)/d)*2(算法进行的计算)与floor (((floor(n/2)/d)*2)(正确的结果)并不等价。不过,要证明它们之间最多相差 1 并不困难。因此,算法计算了余数(变量r),然后判断余数是否比除数大,如果余数比除数大则纠正商(加 1 )即可 。
无符号整型数和浮点型数之间的转换需要进行一些调整。 要把一个无符号整型数转换为浮点型数,可以先将其转换成有符号整型数,然后通过取模运算纠正结果:
u = 11529215046068469760 -- 一个示例f = (u + 0.0) % 2^64string.format("%.0f", f) --> 11529215046068469760
由于标准转换把 u 当作有符号整型数,因此表达式 u+0.0 的值是-6917529027641081856 ,而之后的取模操作会把这个值限制在有符号整型数的表示范围内(在实际的代码中,由于涉及浮点型数的取模运算肯定会进行类型转换,所以并不需要进行这次加法运算)。
要把一个浮点型数转换为无符号整型数,可以使用如下的代码 :
f = 0xA000000000000000.0 -- 一个示例u = math.tointeger(((f + 2^63) % 2^64) - 2^63)string.format("%x", u) --> a000000000000000
加法把一个大于 的数转换为一个大于
的数,取模运算把这个数限制到[0,
)范围内,然后通过减法把结果变成一个“负”值(即最高位置位的值)。对于小于
的值,加法结果小于
,所以取模运算没有任何效果,之后的减法则把它既复到了之前的值。
13.3 打包和解包二进制数据
Lua 5.3 还引入了一个在二进制数和基本类型值(数值和字符串类型)之间进行转换的函数。函数 string.pack 会把值“打包( pack )”为二进制字符串, 而函数 string.unpack 则从字符串中提取这些值。
函数 string.pack 和 函 string.unpack 的第 1 个参数是格式化字符串,用于描述如何打包数据。 格式化字符串中的每个字母都描述了如何打包/解包一个值,例如:
> s = string.pack("iii", 3, -27, 450)
> #s
12
> string.unpack("iii", s)
3 -27 450 13
调用函数 string.pack 将创建一个字符串,其中为 3 个整型数的二进制代码(根据"iii"),每一个"i"编码对与之对应的参数进行了编码, 而字符串的长度则是一个整型数本身大小的3 倍(本人机器上是 3 ×4 字节)。调用函数 string.unpack 对给定字符串中的 3 个整型数进行了解码(还是根据"iii")并返回解码后的结果。
为了便于迭代,函数 string.unpack 还会返回最后一个读取的元素在字符串中的位置(这解释了上例中的 13 )。相应地,该函数还有一个可选的第 3 个参数 ,这个参数用于指定开始读取的位置。 例如,下例输出了一个指定字符串中所有被打包的字符串 :
s = "hello\0Lua\0world\0"
local i = 1
while i <= #s dolocal resres, i = string.unpack("z", s, i)print(res)
end
> hello
> Lua
> world
正如我们马上要看到的,选项 z 意味着一个以 \0 结尾的字符串。因此 ,调用函数 unpack 会从 s 中提取位于 i 的字符串,并返回该字符串外加循环迭代的下一个位置。
对于编码一个整型数而言有几种选项,每一种对应了一种整型大小:b(char)、h(short)、i(int)、l(long)和 j(代表 Lua 语言中整型数的大小)。 要是使用固定的、与机器元关的大小 ,可以在选项 i 后加上一个1~16的数。例如,i7 会产生 7 字节的整型数。所有的大小都会被检查是否存在溢出的情况 :
> x = string.pack("i7", 1 << 54)
> string.unpack("i7", x)
18014398509481984 8
> x = string.pack("i7", -(1 << 54))
> string.unpack("i7", x)
-18014398509481984 8
> x = string.pack("i7", 1 << 55)
stdin:1: bad argument #2 to 'pack' (integer overflow)
我们可以打包和解包比 Lua 语言原生整型数更大的整型数,但是在解包的时候它们的实际值必须能够被 Lua 语言的整型数容纳 :
> x = string.pack("i12", 2^61)
> string.unpack("i12", x)
2305843009213693952 13
>
> x = "aaaaaaaaaaaa"
> string.unpack("i12", x)
stdin:1: 12-byte integer does not fit into Lua Integer
每一个针对整型数的选项都有一个对应的大写版本,对应相应大小的无符号整型数:
> s = "\xFF"
> string.unpack("b", s)
-1 2
> string.unpack("B", s)
255 2
同时,无符号整型数对于 size_t 而言还有一个额外的选项 T ( size_t 类型在 ISO C 中是一个足够容纳任意对象大小的无符号整型数)。
我们可以用 3 种表示形式打包字符串:\0结尾的字符串、定长字符串和使用显式长度的字符串。\0 结尾的字符串使用选项 z ;定长字符串使用选项 cn,其中 n 是被打包字符串的字节数。显式长度的字符串在存储时会在字符串前加上该字符串的长度。在这种情况下,选项的格式形如sn,其中 n 是用于保存字符串长度的无符号整型数的大小。例如,选项 s1 表示把字符串长度保存在一个字节中:
> s = string.pack("s1", "hello")
> for i = 1, #s do print((string.unpack("B", s, i))) end
5
104
101
108
108
111
如果用于保存长度的字节容纳不了字符串长度,那么 Lua 语言会抛出异常。我们也可以单纯使用选项 s ,在这种情况下,字符串长度会被以足够容纳任何字符串长度的size_t 类型保存(在 64 位机器中,size_t 通常是 8 字节的无符号整型数,对于较短的字符串来说可能会浪费空间)。
对于浮点型数,有 3 种选项 :f 用于单精度浮点数 、d 用于双精度浮点数、 n 用于 Lua 语
言浮点数。
格式字符串也有用来控制大小端模式和二进制数据对齐的选项。在默认情况下,格式使用的是机器原生的大小端模式。选项 > 把所有后续的编码转换改为大端模式或网络字节序:
> s = string.pack(">i4", 1000000)
> for i = 1, #s do print((string.unpack("B", s, i))) end
0
15
66
64
选项 < 则改为小端模式 :
> s = string.pack("<i2 i2", 500, 24)
> for i = 1, #s do print((string.unpack("B", s, i))) end
244
1
24
0
最后,选项 = 改回机器默认的原生大小端模式。
对于对齐而言,选项 !n 强制数据对齐到以 n 为倍数的索引上。更准确地说,如果数据比 n 小,那么对齐到其自身大小上;否则,对齐到 n 上。 例如,假设格式化字符串为 !4,那么1 字节整型数会被写入以 1 为倍数的索引位置上(也就是任意索引位置上),2 字节的整型数会被写入以 2 为倍数的索引位置上,而 4 字节或更大的整型数则会被写入以 4 为倍数的索引位置上,而选项 !(不带数字) 则把对齐设为机器默认的对齐方式。
函数 string.pack 通过在结果字符串到达合适索引值前增加 0 的方式实现对齐,函数
string.unpack 在读取字符串时会简单地跳过这些补位。对齐只对 2 的整数次幕有效,如果把对齐设为 4 但试图操作 3 字节的整型数,那么 Lua 语言会抛出异常。
所有的格式化字符串默认带有前缀"=!1",即表示使用默认的大小端模式且不对齐(因为每个索引都是 1 的倍数)。我们可以在程序执行过程中的任意时点改变大小端模式和对齐方式。
如果需要,可以手工添加补位。选项 x 代表 1 字节的补位,函数 string.pack 会在结果字符串中增加一个 0 字节,而函数 string.unpack 则从目标字符串中跳过 1 字节。
13.4 二进制文件
函数 io.input 和 io.output 总是以文本方式打开文件。 在 POSIX 操作系统中,二进制文件和文本文件是没有差别的。然而,在其他一些像 Windows 之类的操作系统中,必须用特殊方式来打开二进制文件,即在 io.open 的模式字符串中使用字母 b 。
通常,在读取二进制数据时,要么使用模式"a"来读取整个文件,要么使用模式 n 来读取 n 字节(在二进制文件中,“行”是没有意义的)。下面是一个简单的示例,它会把 Windows 格式的文本文件转换为 POSIX 格式,即把\r\n 转换为\n:
local inp = assert(io.open(arg[1], "rb"))
local out = assert(io.open(arg[2], "wb"))local data = inp:read("a")
data = string.gsub(data, "\r\n", "\n")
out:write(data)assert(out:close())
由于标准 I/O 流( stdin/stdout )是以文本模式打开的,所以上例不能使用标准 I/O 流。相反,该程序假设输入和输出文件的名称是由程序的参数指定的。可以使用如下的命令调用该程序 :
> lua prog.lua file.dos file.unix
再举一个例子,以下的程序输出了一个二进制文件中的所有字符串:
local f = assert(io.open(arg[1], "rb"))
local data = f:read("a")
local validchars ="[%g%s]"
lcoal pattern = "(" .. string.rep(validchars, 6) .. "+)0"
for w in string.gmatch(data, pattern) doprint(w)
end
这个程序假定字符串是一个以 \0 结尾的、包含 6 个或 6 个以上有效字符的序列,其中有效字符是指能与模式 validchars 匹配的任意字符。在这个示例中,这个模式由可打印字符组成。我们使用函数 string.rep 和字符串连接创建用于捕获以 \0 结尾的、包含 6 个或 6 个以上有效字符 validchars 的模式,这个模式中的括号用于捕获不带 \0 的字符串 。
最后一个示例用于以十六进制内容输出二进制文件的 Dump,以下示例展示了在 POSIX操作系统下将这个程序用于其自身时的结果:
local f = assert(io.open(arg[1], "rb"))
local blocksize = 16
for bytes in f:lines(blocksize) dofor i = 1, #bytes dolocal b = string.unpack("B", bytes, i)io.write(string.format("%02X ", b))endio.write(string.rep(" ", blocksize - #bytes))bytes = string.gsub(bytes, "%c", ".")io.write(" ", bytes, "\n")
end
同样,程序的第一个参数是输入文件名,结果则是被输出到标准输出中的普通文本。 这个程序以 16 字节为一个块读取文件,对于每个块先输出每个字节的十六进制表示,然后将控制字符替换为点,最后把整个块作为文本输出 。 函数 string.rep 用于填充最后一行中的空白(因为最后一行往往不到 16 字节)以保持对齐。