Python字节码浅析
一、前言
字节码是一种由 Python 解释器执行的中间代码形式。当你运行 Python 程序时,解释器首先会将源代码转换为字节码,然后逐条执行字节码指令来完成程序的运行。字节码类似于机器码,但与特定的硬件平台无关,因此可以在不同的平台上运行相同的字节码。
与直接解释源代码相比,字节码具有一些优势:
- 更快的执行速度:由于字节码是一种中间形式,它比解释源代码更接近机器指令。因此,解释器可以更高效地执行字节码,提高程序的执行速度。
- 跨平台可移植性:由于字节码与特定的硬件平台无关,你可以将字节码文件在不同的计算机上运行,而无需重新编译源代码。
- 保护源代码:字节码文件不包含完整的源代码,因此可以用作代码保护的一种手段。其他人无法直接查看和修改源代码,只能执行字节码。
二、生成字节码
首先,我们先编写一个简单的python代码
print("hello world")
使用以下命令生成字节码文件
# python3
$ python3 -m py_compile hello.py
或者
$ python3 -m compileall hello.py
# python2 也可以直接这样生成
$ python2 -m hello.py
然后在同目录下会生成一个“__pycache__”文件夹,里面包含一个pyc文件,如果是python2就在当前目录生成一个pyc文件
同一python版本的,只能运行同一版本的pyc文件
三、文件结构
我们使用hexdump分别以16进制的格式打印python3、python2的字节码
$ hexdump -C __pycache__/hello.cpython-38.pyc
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 55 0d 0d 0a 00 00 00 00 4c 60 1c 67 15 00 00 00 |U.......L`.g....|
00000010 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 00 02 00 00 00 40 00 00 00 73 0c 00 00 00 65 00 |.....@...s....e.|
00000030 64 00 83 01 01 00 64 01 53 00 29 02 7a 0b 68 65 |d.....d.S.).z.he|
00000040 6c 6c 6f 20 77 6f 72 6c 64 4e 29 01 da 05 70 72 |llo worldN)...pr|
00000050 69 6e 74 a9 00 72 02 00 00 00 72 02 00 00 00 fa |int..r....r.....|
00000060 08 68 65 6c 6c 6f 2e 70 79 da 08 3c 6d 6f 64 75 |.hello.py..<modu|
00000070 6c 65 3e 01 00 00 00 f3 00 00 00 00 |le>.........|
0000007c
$ hexdump -C hello.pyc
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 03 f3 0d 0a 4c 60 1c 67 63 00 00 00 00 00 00 00 |....L`.gc.......|
00000010 00 01 00 00 00 40 00 00 00 73 09 00 00 00 64 00 |.....@...s....d.|
00000020 00 47 48 64 01 00 53 28 02 00 00 00 73 0b 00 00 |.GHd..S(....s...|
00000030 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 4e 28 00 00 |.hello worldN(..|
00000040 00 00 28 00 00 00 00 28 00 00 00 00 28 00 00 00 |..(....(....(...|
00000050 00 73 08 00 00 00 68 65 6c 6c 6f 2e 70 79 74 08 |.s....hello.pyt.|
00000060 00 00 00 3c 6d 6f 64 75 6c 65 3e 01 00 00 00 74 |...<module>....t|
00000070 00 00 00 00 |....|
00000074
我们可以清楚的看到不同版本的pyc结构是有些许不一样的,这里以python3为例
偏移 | 值 | 说明 |
0x00-0x03 | 0x0A0D0D55 | 魔数(magic number),0x0D55=3411 |
0x04-0x07 | 0 | 填充为空 |
0x08-0x0B | 0x671C604C=1729912908 | 源代码最后更新时间戳 |
0x0C-0x0F | 0x15 | 源代码文件的大小 |
0x10-末尾 | 字节码主体 |
使用dis、marsha1模块解析字节码
import dis
import marshal
import struct
import time
import sys
from rich import inspect
fp = open(sys.argv[1], 'rb')
# 魔数
print("magic code: 0x%X"%(struct.unpack('<l', fp.read(4))[0]))
# 填充
print(f"padding: {struct.unpack('<l', fp.read(4))[0]}")
# 源代码最后更新时间
t = struct.unpack('<l', fp.read(4))[0]
print(f"last modified time: {time.asctime(time.localtime(t))}")
# 源码文件大小
print(f"file size: {struct.unpack('<l', fp.read(4))[0]} Bytes")
# 构造字节码code对象
code_obj = marshal.load(fp)
# 查看字节码对象类型
print(f"type: {type(code_obj)}")
# 通过前面介绍的rich的inspect()进行code对象的检视:
inspect(code_obj)
# 查看字节码指令序列
dis.dis(code_obj)
解析的结果
最后的是指令序列,类似汇编语言,具体的功能可以参考官方文档,以上大概解释如下
行数 | 索引 | 操作码 | 参数 | 参数实际值 | 说明 |
1 | 0 | LOAD_NAME | 0 | 将与co_consts[0]="print" 推入栈顶 | |
2 | LOAD_CONST | 0 | 'hello world' | 将co_consts[0]="hello world" 推入栈顶 | |
4 | CALL_FUNCTION | 1 | 调用函数 | ||
6 | POP_TOP | 删除堆栈顶部(TOS)项 | |||
8 | LOAD_CONST | 1 | None | 将co_consts[1]=None 推入栈顶 | |
10 | RETURN_VALUE | 返回 TOS 到函数的调用者 |
如果要想查看当前python3支持的所有操作码,可以这样,这里的版本为3.8.10
import opcode
for op in range(len(opcode.opname)):
if opcode.opname[op][0] != '<':
print('0x%.2X(%.3d): %s' % (op, op, opcode.opname[op]))
内容如下:
0x01(001): POP_TOP
0x02(002): ROT_TWO
0x03(003): ROT_THREE
0x04(004): DUP_TOP
0x05(005): DUP_TOP_TWO
0x06(006): ROT_FOUR
0x09(009): NOP
0x0A(010): UNARY_POSITIVE
0x0B(011): UNARY_NEGATIVE
0x0C(012): UNARY_NOT
0x0F(015): UNARY_INVERT
0x10(016): BINARY_MATRIX_MULTIPLY
0x11(017): INPLACE_MATRIX_MULTIPLY
0x13(019): BINARY_POWER
0x14(020): BINARY_MULTIPLY
0x16(022): BINARY_MODULO
0x17(023): BINARY_ADD
0x18(024): BINARY_SUBTRACT
0x19(025): BINARY_SUBSCR
0x1A(026): BINARY_FLOOR_DIVIDE
0x1B(027): BINARY_TRUE_DIVIDE
0x1C(028): INPLACE_FLOOR_DIVIDE
0x1D(029): INPLACE_TRUE_DIVIDE
0x32(050): GET_AITER
0x33(051): GET_ANEXT
0x34(052): BEFORE_ASYNC_WITH
0x35(053): BEGIN_FINALLY
0x36(054): END_ASYNC_FOR
0x37(055): INPLACE_ADD
0x38(056): INPLACE_SUBTRACT
0x39(057): INPLACE_MULTIPLY
0x3B(059): INPLACE_MODULO
0x3C(060): STORE_SUBSCR
0x3D(061): DELETE_SUBSCR
0x3E(062): BINARY_LSHIFT
0x3F(063): BINARY_RSHIFT
0x40(064): BINARY_AND
0x41(065): BINARY_XOR
0x42(066): BINARY_OR
0x43(067): INPLACE_POWER
0x44(068): GET_ITER
0x45(069): GET_YIELD_FROM_ITER
0x46(070): PRINT_EXPR
0x47(071): LOAD_BUILD_CLASS
0x48(072): YIELD_FROM
0x49(073): GET_AWAITABLE
0x4B(075): INPLACE_LSHIFT
0x4C(076): INPLACE_RSHIFT
0x4D(077): INPLACE_AND
0x4E(078): INPLACE_XOR
0x4F(079): INPLACE_OR
0x51(081): WITH_CLEANUP_START
0x52(082): WITH_CLEANUP_FINISH
0x53(083): RETURN_VALUE
0x54(084): IMPORT_STAR
0x55(085): SETUP_ANNOTATIONS
0x56(086): YIELD_VALUE
0x57(087): POP_BLOCK
0x58(088): END_FINALLY
0x59(089): POP_EXCEPT
0x5A(090): STORE_NAME
0x5B(091): DELETE_NAME
0x5C(092): UNPACK_SEQUENCE
0x5D(093): FOR_ITER
0x5E(094): UNPACK_EX
0x5F(095): STORE_ATTR
0x60(096): DELETE_ATTR
0x61(097): STORE_GLOBAL
0x62(098): DELETE_GLOBAL
0x64(100): LOAD_CONST
0x65(101): LOAD_NAME
0x66(102): BUILD_TUPLE
0x67(103): BUILD_LIST
0x68(104): BUILD_SET
0x69(105): BUILD_MAP
0x6A(106): LOAD_ATTR
0x6B(107): COMPARE_OP
0x6C(108): IMPORT_NAME
0x6D(109): IMPORT_FROM
0x6E(110): JUMP_FORWARD
0x6F(111): JUMP_IF_FALSE_OR_POP
0x70(112): JUMP_IF_TRUE_OR_POP
0x71(113): JUMP_ABSOLUTE
0x72(114): POP_JUMP_IF_FALSE
0x73(115): POP_JUMP_IF_TRUE
0x74(116): LOAD_GLOBAL
0x7A(122): SETUP_FINALLY
0x7C(124): LOAD_FAST
0x7D(125): STORE_FAST
0x7E(126): DELETE_FAST
0x82(130): RAISE_VARARGS
0x83(131): CALL_FUNCTION
0x84(132): MAKE_FUNCTION
0x85(133): BUILD_SLICE
0x87(135): LOAD_CLOSURE
0x88(136): LOAD_DEREF
0x89(137): STORE_DEREF
0x8A(138): DELETE_DEREF
0x8D(141): CALL_FUNCTION_KW
0x8E(142): CALL_FUNCTION_EX
0x8F(143): SETUP_WITH
0x90(144): EXTENDED_ARG
0x91(145): LIST_APPEND
0x92(146): SET_ADD
0x93(147): MAP_ADD
0x94(148): LOAD_CLASSDEREF
0x95(149): BUILD_LIST_UNPACK
0x96(150): BUILD_MAP_UNPACK
0x97(151): BUILD_MAP_UNPACK_WITH_CALL
0x98(152): BUILD_TUPLE_UNPACK
0x99(153): BUILD_SET_UNPACK
0x9A(154): SETUP_ASYNC_WITH
0x9B(155): FORMAT_VALUE
0x9C(156): BUILD_CONST_KEY_MAP
0x9D(157): BUILD_STRING
0x9E(158): BUILD_TUPLE_UNPACK_WITH_CALL
0xA0(160): LOAD_METHOD
0xA1(161): CALL_METHOD
0xA2(162): CALL_FINALLY
0xA3(163): POP_FINALLY
四、代码还原
还原代码可以使用uncompyle6、pycdc,但是如果python解释器加以修改字节码就可能无法解析了,所以还是要学会看指令。
五、参考文档
https://baijiahao.baidu.com/s?id=1803643939985376783
https://baijiahao.baidu.com/s?id=1771566595672540367
http://www.360doc.com/content/22/1208/11/81250822_1059437643.shtml
https://docs.python.org/zh-cn/3.9/library/dis.html#dis.Bytecode
https://docs.python.org/zh-cn/3.9/library/marshal.html