这篇文章上次修改于 1614 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

0x1 介绍

LEB128(little endian base 128)是一种变长的整数压缩编码形式,它是出自于DWARF debug file format。在Android的Dalvik Executable format中使用该编码用于表示32位整数。由于32位整数占用固定的4个字节,可能大多数整数并不需要4个字节,最高几个字节可能为0(正数)或者为1(负数),该编码就是不保存最高位的这些字节。

0x2 原理

LEB128的表现形式都是一样的,如下面表格所示,由于是little endian,因此是从低字节到高字节。每个字节中的最高bit是标识信息,1表示还有后续字节,0表示结束,后面7bits是有效数据。将多个字节的该7bits从低到高组合起来就是所表示的整数。
LEB128分成有符号数和无符号数两种分别进行处理,不过,只是在编码和解码过程有些不同。

0x3 前置知识点

进制间转换

在线工具 https://tool.oschina.net/hexconvert

2、8、16进制->10进制

  • 二进制 → 十进制

  方法:二进制数从低位到高位(即从右往左)计算,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

  例:将二进制的(101011)B转换为十进制的步骤如下:

  1. 第0位 1 x 2^0 = 1;
  2. 第1位 1 x 2^1 = 2;
  3. 第2位 0 x 2^2 = 0;
  4. 第3位 1 x 2^3 = 8;
  5. 第4位 0 x 2^4 = 0;
  6. 第5位 1 x 2^5 = 32;
  7. 读数,把结果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。
  • 八进制 → 十进制

  方法:八进制数从低位到高位(即从右往左)计算,第0位的权值是8的0次方,第1位的权值是8的1次方,第2位的权值是8的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

  八进制就是逢8进1,八进制数采用 0~7这八数来表达一个数。

  例:将八进制的(53)O转换为十进制的步骤如下:

  1. 第0位 3 x 8^0 = 3;
  2. 第1位 5 x 8^1 = 40;
  3. 读数,把结果值相加,3+40=43,即(53)O=(43)D。
  • 十六进制 → 十进制

  方法:十六进制数从低位到高位(即从右往左)计算,第0位的权值是16的0次方,第1位的权值是16的1次方,第2位的权值是16的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

  十六进制就是逢16进1,十六进制的16个数为0123456789ABCDEF。

  例:将十六进制的(2B)H转换为十进制的步骤如下:

  1. 第0位 B x 16^0 = 11;
  2. 第1位 2 x 16^1 = 32;
  3. 读数,把结果值相加,11+32=43,即(2B)H=(43)D。

10进制->2、8、16进制

  • 十进制 → 二进制

  方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。

  例:将十进制的(43)D转换为二进制的步骤如下:

  1. 将商43除以2,商21余数为1;
  2. 将商21除以2,商10余数为1;
  3. 将商10除以2,商5余数为0;
  4. 将商5除以2,商2余数为1;
  5. 将商2除以2,商1余数为0;
  6. 将商1除以2,商0余数为1;
  7. 读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,101011,即(43)D=(101011)B。
  • 十进制 → 八进制

  方法1:除8取余法,即每次将整数部分除以8,余数为该位权上的数,而商继续除以8,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。

  例:将十进制的(796)D转换为八进制的步骤如下:

  1. 将商796除以8,商99余数为4;
  2. 将商99除以8,商12余数为3;
  3. 将商12除以8,商1余数为4;
  4. 将商1除以8,商0余数为1;
  5. 读数,因为最后一位是经过多次除以8才得到的,因此它是最高位,读数字从最后的余数向前读,1434,即(796)D=(1434)O。
  • 十进制 → 十六进制

  方法1:除16取余法,即每次将整数部分除以16,余数为该位权上的数,而商继续除以16,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。

  例:将十进制的(796)D转换为十六进制的步骤如下:

  1. 将商796除以16,商49余数为12,对应十六进制的C;
  2. 将商49除以16,商3余数为1;
  3. 将商3除以16,商0余数为3;
  4. 读数,因为最后一位是经过多次除以16才得到的,因此它是最高位,读数字从最后的余数向前读,31C,即(796)D=(31C)H。

逻辑与(&)计算

逻辑与计算

5&3

5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011
----------------------------------------------------
1转换为二进制:0000 0000 0000 0000 0000 0000 0000 0001

从低位到高位,从右往左,开始比较。

1&1=1

0&1=0

1&0=0

...

逻辑或(|)计算

5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011
----------------------------------------------------
7转换为二进制:0000 0000 0000 0000 0000 0000 0000 0111

从低位到高位,从右往左,开始比较。

1&1=1

0&1=1

1&0=1

0&0=0

...

移位运算(<< or >>)

5<<2 5左移 2 位
首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位):

0000 0000 0000 0000 0000 0000 0000 0101           然后左移2位后,低位补0:
0000 0000 0000 0000 0000 0000 0001 0100           换算成10进制为20
5>>2 5右移 2 位
还是先将5转为2进制表示形式:
0000 0000 0000 0000 0000 0000 0000 0101 然后右移2位,高位补0:
0000 0000 0000 0000 0000 0000 0000 0001

Python 运算符优先级

运算符描述
**指数 (最高优先级)
~ + -按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@)
* / % //乘,除,取模和取整除
+ -加法减法
>> <<右移,左移运算符
&位 'AND'
^ \ 位运算符
<= < > >=比较运算符
<> == !=等于运算符
= %= /= //= -= += = *=赋值运算符
is is not身份运算符
in not in成员运算符
not and or逻辑运算符

0x4 LEB128编码过程

无符号整数

无符号整数编码步骤如下:

  1. 将无符号整数写成二进制形式,
  2. 从低位到高位7个bits为一个整体组合成一个字节,
  3. 在该字节最高位填入上述所说的标识信息。
  4. 按照小端规则存放

下面以10000为例,

编码过程:

二进制形式为10 0111 0001 0000
以7bits为整体1001110 0010000
添加标识组合成新的字节(从后往前,即低bits到高bits)01001110(0x4E) 10010000(0x90) (最高位标识设置为0,表示没有后续字节)
LEB128 则为0x90 0x4F (小端存放)

解码过程:

LEB1280x90 0x4E
二进制形式10010000 01001110
去掉标识信息0010000(低7bits) 1001110(高7bits)
组合的结果为10011100010000 (10000)

Python 代码实现

class _U:
    @staticmethod
    def encode(i: int):
        assert i >= 0
        r = []
        while True:
            byte = i & 0x7f
            i = i >> 7
            if i == 0:
                r.append(byte)
                return r
            r.append(0x80 | byte)

    @staticmethod
    def decode(b: list) -> int:
        r = 0
        for i, v in enumerate(b):
            print(f'i={i} e={v}')
            r = r + ((v & 0x7f) << (i * 7))
        return r

有符号数

有符号数分成了正数和负数,在计算机的存储中都是以补码存储,正数和上述无符号数一样的处理,负数的处理会有些区别,以-10000为例说明,

编码过程:

二进制补码11111111 11111111 11111100 00011000(可以看出最高两字节都是符号扩展的1)
以7bits为整体1111 1111111 1111111 1111000 0011000
添加标识信息组合新的字节(从后往前,即低bits到高bits)01111000 10011000(此处结束条件不像上面那么明显,若前面和该7bits的最高位都为1时停止)
LEB128则为0x98 0x78

解码过程:

LEB1280x98 0x78
二进制形式10011000 01111000
去掉标识信息0011000 1111000 (若最后一个字节中7bits的最高位为1,则前面需要符号扩展都添加1)
组合结果11111111 11111111 1111100 00011000 (-10000)

Python 代码实现

class _I:
    @staticmethod
    def encode(i: int):
        r = []
        while True:
            byte = i & 0x7f
            i = i >> 7
            if (i == 0 and byte & 0x40 == 0) or (i == -1 and byte & 0x40 != 0):
                r.append(byte)
                return r
            r.append(0x80 | byte)

    @staticmethod
    def decode(b: list) -> int:
        r = 0
        for i, e in enumerate(b):
            r = r + ((e & 0x7f) << (i * 7))
        if e & 0x40 != 0:
            r |= - (1 << (i * 7) + 7)
        return r

0x05 参考

https://blog.csdn.net/liwugang43210/article/details/50475928

https://www.cnblogs.com/liwugang/p/7594093.html

https://berryjam.github.io/2019/09/LEB128(Little-Endian-Base-128)%E6%A0%BC%E5%BC%8F%E4%BB%8B%E7%BB%8D

https://blog.csdn.net/xiaochunyong/article/details/7748713

http://androidxref.com/9.0.0_r3/xref/dalvik/libdex/Leb128.h

https://files.pythonhosted.org/packages/03/d7/91ad62b5c68ed1182e1d4b225ce918c7c0a90b7eb8f044ba8566ee948e6c/leb128-1.0.2.tar.gz