pickle源码详解

首先很重要的就是字节码,其实pickle经过了多次迭代已经加了很多内容了,我们直接到源码里去看看


# Pickle opcodes.  See pickletools.py for extensive docs.  The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.
# Pickle的opcodes,可在pickletools.py中查看大范围的文档,在此处列出的内容是按字母顺序排列的单字符pickle code
# 在pickleltools中将这些内容按功能分组了
# 等会再去pickletools里看
# 部分偷的自己师傅的(自己人不算偷)
MARK           = b'('   #向栈中压入一个Mark标记
STOP           = b'.'   #相当于停止当前的反序列化过程
POP            = b'0'   #从栈中pop出一个元素,就是删除栈顶元素
POP_MARK       = b'1'   #从栈中不断pop元素直到遇到Mark标记
DUP            = b'2'   #向栈中再压入一个当前的栈顶元素,就是复制一份当前栈顶元素然后进行压栈
FLOAT          = b'F'   #读取当前行到行末尾,然后转为float类型,向栈中压入一个float浮点数
INT            = b'I'   #向栈中压入一个int整数,整数就是当前行的最后一个字节,不过如果整数为01的时候压入的是True,为00的时候压入的是False
BININT         = b'J'   #从后面的输入中读取4个字节并且使用unpack通过'<i'的格式将4字节的buffer数据解包转为int类型,后面不能换行,直接家下一步的操作b"(S'a'\nK\x01\x01\x01\x01."
BININT1        = b'K'   #和上面BININT一样,不过K操作只读取一个字节的数据b"(S'a'\nK\x01."
LONG           = b'L'   #读取当前行到行末尾,然后转为int类型,但如果后面是字符L的话会先去掉最后一个字符L再转int
BININT2        = b'M'   #从后面的输入中读取2个字节并且使用unpack通过'<H'的格式将2字节的buffer作为一个2进制数解包为int,后面不能换行,直接加下一步的操作b"(S'a'\nM\x01\x01."
NONE           = b'N'   #向栈中压入一个None元素,后面不能换行,直接加下一步的操作b"(S'a'\nN."
PERSID         = b'P'   #读取当前行到行末尾,将读取到的数据作为id,通过persistent_load函数获得obj对象返回后将obj对象压栈,默认情况没用,要重写persistent_load函数才能生效
BINPERSID      = b'Q'   #和上面作用一样,从当前栈中弹出一个元素作为id,通过persistent_load...
REDUCE         = b'R'   #从当前栈中弹出两次元素,第一次是函数参数args,第二次是函数func,执行func(args)
STRING         = b'S'   #向栈中压入一个string字符串,内容就是后面的数据,后面的字符串第一个和最后一个必须是单引号b"(S'a'\nS''a''\n."
BINSTRING      = b'T'   #从后面数据读取4字节数据,通过unpack使用<i格式将数据解压后变为int类型, 然后将其作为一个长度, 后面读取这个指定长度的数据作为字符串进行压栈b"(S'a'\nT\x10\x00\x00\x000123456789abcdef."
# _struct.unpack('<i', b"\x10\x00\x00\x00") => (16,)
SHORT_BINSTRING= b'U'   #先读取一个字节数据作为长度,然后按照这个长度读取字符串,读出的字符串压栈
UNICODE        = b'V'   #读出当前行后面的全部数据,然后进行Unicode解码,将解码内容压栈b'V\\u0061\n.'
BINUNICODE     = b'X'   #读出4字节数据通过unpack使用<I格式解压,将解压得到的数据作为长度,然后进行数据读取b'X\x10\x00\x00\x00abcdef0123456789.'
APPEND         = b'a'   #先pop出栈一个变量var1,然后获取当前栈顶元素var2,执行栈顶元素的append函数,就是将一开始的栈顶元素弹出,然后又加到下一个栈顶数组中b"]S'h0cksr'\na." => 得到['h0cksr']
BUILD          = b'b'   #这个操作就是设置元素属性的操作
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding

TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO          = b'\x80'  # identify pickle protocol
NEWOBJ         = b'\x81'  # build object by applying cls.__new__ to argtuple
EXT1           = b'\x82'  # push object from extension registry; 1-byte index
EXT2           = b'\x83'  # ditto, but 2-byte index
EXT4           = b'\x84'  # ditto, but 4-byte index
TUPLE1         = b'\x85'  # build 1-tuple from stack top
TUPLE2         = b'\x86'  # build 2-tuple from two topmost stack items
TUPLE3         = b'\x87'  # build 3-tuple from three topmost stack items
NEWTRUE        = b'\x88'  # push True
NEWFALSE       = b'\x89'  # push False
LONG1          = b'\x8a'  # push long from < 256 bytes
LONG4          = b'\x8b'  # push really big long

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES       = b'B'   # push bytes; counted binary string argument
SHORT_BINBYTES = b'C'   #  "     "   ;    "      "       "      " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c'  # push short string; UTF-8 length < 256 bytes
BINUNICODE8      = b'\x8d'  # push very long string
BINBYTES8        = b'\x8e'  # push very long bytes string
EMPTY_SET        = b'\x8f'  # push empty set on the stack
ADDITEMS         = b'\x90'  # modify set by adding topmost stack items
FROZENSET        = b'\x91'  # build frozenset from topmost stack items
NEWOBJ_EX        = b'\x92'  # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL     = b'\x93'  # same as GLOBAL but using names on the stacks
MEMOIZE          = b'\x94'  # store top of the stack in memo
FRAME            = b'\x95'  # indicate the beginning of a new frame

# Protocol 5

BYTEARRAY8       = b'\x96'  # push bytearray
NEXT_BUFFER      = b'\x97'  # push next out-of-band buffer
READONLY_BUFFER  = b'\x98'  # make top of stack readonly

上面的内容就是pickle中定义的所有字节码了,我们先略过这部分不谈,我们先来看后面的反序列化中对于不同字节码的实现来更好的理解每个字节码的注释内容

关于pickle详细执行过程

起因

孩子比较呆,对于什么栈之类的描述没法很好理解,所以还是自己跑一下吧,这篇文会用几个不同的例子来详细说明

基础内容分析

首先我们先看看调用load时的pickle类的定义内容,这有利于我们对后面的操作进行理解

self._unframer = _Unframer(self._file_read, self._file_readline)
self.read = self._unframer.read #字节读取,没啥好说的,下面也是
self.readinto = self._unframer.readinto
self.readline = self._unframer.readline
self.metastack = [] #存储栈(?,我们目前可以这么叫他,这里存放的是与目前这步无关的内容
self.stack = [] #操作栈(?,这里存储的就是我们当前这一步操作所需要的内容
self.append = self.stack.append # 对self的append操作等同于对栈的append操作
self.proto = 0
read = self.read #读一位
dispatch = self.dispatch #pickle预置的字节码和函数的对应关系
try: #反序列化整体逻辑,首先通过read读入一位字节码,判断是否是字节类型,取出首位字节码对应的函数进行执行
		while True:
				key = read(1)
				if not key:
		        raise EOFError
        assert isinstance(key, bytes_types)
        dispatch[key[0]](self) #进入对应读出字节码的函数中
except _Stop as stopinst:
		return stopinst.value

https://jlan-blog.oss-cn-beijing.aliyuncs.com/202212041745342.png

实例分析

正常类反序列化

第一个是一个正常的类的对象的序列化和反序列化,先使用了pickletools.optimize对字节码进行了精简,方便后续分析

import pickle
import pickletools
class User():
    def __init__(self):
        self.username="Jlan"
        self.password="pass"
a=User()
b=pickle.dumps(a,1)
b=pickletools.optimize(b)
print(b)
pickletools.dis(b)
x=pickle.loads(b)
#b'ccopy_reg\n_reconstructor\n(c__main__\nUser\nc__builtin__\nobject\nNtR}(X\x08\x00\x00\x00usernameX\x04\x00\x00\x00JlanX\x08\x00\x00\x00passwordX\x04\x00\x00\x00passub.'
#     0: c    GLOBAL     'copy_reg _reconstructor'
#    25: (    MARK
#    26: c        GLOBAL     '__main__ User'
#    41: c        GLOBAL     '__builtin__ object'
#    61: N        NONE
#    62: t        TUPLE      (MARK at 25)
#    63: R    REDUCE
#    64: }    EMPTY_DICT
#    65: (    MARK
#    66: X        BINUNICODE 'username'
#    79: X        BINUNICODE 'Jlan'
#    88: X        BINUNICODE 'password'
#   101: X        BINUNICODE 'pass'
#   110: u        SETITEMS   (MARK at 65)
#   111: b    BUILD
#   112: .    STOP
# highest protocol among opcodes = 1
# 详细过程可以看上面啦,pickletools官方进行的解析

下面我们按每个操作来进行说明

  • ccopy_reg\n_reconstructor\n

    首先取出的是c操作符,对应的是GLOBAL操作,进入load_global函数

    def load_global(self):
    		module = self.readline()[:-1].decode("utf-8")
    		#读一行,存入module,也就是模块名
        name = self.readline()[:-1].decode("utf-8")
    		#读一行,存入name,也就是模块中的方法或属性
        klass = self.find_class(module, name)
    		#通过find_class方法找到对应的方法
        self.append(klass)
    		#将找到的内容压入栈中
    def find_class(self, module, name):
        # Subclasses may override this.
        sys.audit('pickle.find_class', module, name)
        if self.proto < 3 and self.fix_imports:
            if (module, name) in _compat_pickle.NAME_MAPPING:
                module, name = _compat_pickle.NAME_MAPPING[(module, name)]
            elif module in _compat_pickle.IMPORT_MAPPING:
                module = _compat_pickle.IMPORT_MAPPING[module]
        __import__(module, level=0)
    		#通过import方法导入模块
        if self.proto >= 4:
            return _getattribute(sys.modules[module], name)[0]
        else:
            return getattr(sys.modules[module], name)
    				#取出对应属性

    c:GLOBAL:load_global:GLOBAL操作做的事就是取出模块.属性名并压入栈

  • (c__main__\nUser\nc__builtin__\nobject\n
    首先取出的是(操作符,对应的是MARK操作,进入load_mark函数

    def load_mark(self):
        self.metastack.append(self.stack)
    		#将操作栈的内容整个压入存储栈
        self.stack = []
    		#清空操作栈
        self.append = self.stack.append

    (:MARK:load_mark:MARK操作将操作栈中所有内容压入存储栈,并清空操作栈

    然后就是两次GLOBAL操作加一次NONE操作

  • N
    取出N操作符,对应NONE操作,进入load_none函数

    def load_none(self):
        self.append(None)

    (:MARK:load_mark:NONE操作将一个None对象压入操作栈

    经过这些操作后操作栈和存储栈的情况如下

    self.stack = [<class '__main__.User'>, <class 'object'>, None]
    self.metastack = [[<function _reconstructor at 0x1006735e0>]]
  • t
    取出t操作符,对应TUPLE操作,进入load_tuple函数

    def load_tuple(self):
        items = self.pop_mark()
    		#进入pop_mark方法,取得之前操作栈的数据
        self.append(tuple(items))
    		#将之前操作栈的数据整体压入当前操作栈
    def pop_mark(self):
        items = self.stack
    		#将目前操作栈中的所有内容存入到items中
        self.stack = self.metastack.pop()
    		#弹出存储栈中的一个元素,并将其赋给操作栈
        self.append = self.stack.append
        return items
    		#返回原始操作栈中的内容

    t:TUPLE:load_tuple:TUPLE操作将最后一个mark标记的栈和现在的操作栈(转为元组)压入操作栈

    还是看一下操作栈和存储栈的状态吧

    self.stack = [<function _reconstructor at 0x1006735e0>, (<class '__main__.User'>, <class 'object'>, None)]
    self.metastack = []
  • R
    取出R操作符,对应REDUCE操作,进入load_reduce函数

    def load_reduce(self):
        stack = self.stack
    		#将目前栈中内容放入函数内变量中
        args = stack.pop()
    		#弹出栈中最后一个内容做函数参数
        func = stack[-1]
    		#取出栈中最后一个元素做函数方法
        stack[-1] = func(*args)
    		#将函数执行结果存入栈中覆盖函数方法

    R:REDUCE:load_reduce:REDUCE操作将操作栈的最后一个元素作为函数参数,倒数第二个元素作为函数方法,将函数执行结果放到操作栈末尾

  • }
    取出}操作符,对应EMPTY_DICT,进入load_empty_dictionary函数

    def load_empty_dictionary(self):
        self.append({})

    }:EMPTY_DICT:load_empty_dictionary:EMPTY_DICT操作将一个空字典压入操作栈

  • (X\x08\x00\x00\x00usernameX\x04\x00\x00\x00JlanX\x08\x00\x00\x00passwordX\x04\x00\x00\x00pass
    第一个压栈操作之前已经看过了,直接来看X操作符对应的内容,load_binunicode

    def load_binunicode(self):
        len, = unpack('<I', self.read(4))
    		#以小端头存储方式读取一个无符号int数(4位),读出后面需要的内容的长度
        if len > maxsize:
            raise UnpicklingError("BINUNICODE exceeds system's maximum size "
                                  "of %d bytes" % maxsize)
        self.append(str(self.read(len), 'utf-8', 'surrogatepass'))

    X:BINUNICODE:load_binunicodeBINUNICODE操作先读取字符串长度,然后按UTF-8编码读入内容并压入栈中

    操作完看栈

    metastack = [[<__main__.User object at 0x105d9e0a0>, {}]]
    #一开始的mark操作压入的
    stack = ['username', 'Jlan', 'password', 'pass']
  • u
    取出u操作符,对应SETITEMS,进入load_setitems函数(这个名字超明显)

    def load_setitems(self):
        items = self.pop_mark()
    		#把当前操作栈数据取出,存储栈的内容放入操作栈
        dict = self.stack[-1]
    		#把当前栈的最后一个属性取出作为字典
        for i in range(0, len(items), 2):
            dict[items[i]] = items[i + 1]
    		#按照单数键,双数值的方式把items中的内容转成字典

    u:SETITEMS:load_setitemsSETITEMS操作将存储栈的内容取出到操作栈中,然后将原本操作栈的数据转为字典并替换掉上一步(}操作符)中压入的空字典

  • b
    取出b操作符,对应BUILD,进入load_build函数

    # call __setstate__ or __dict__.update()
    def load_build(self):
        stack = self.stack
        state = stack.pop()
    		#把上一步生成的属性字典弹出
        inst = stack[-1]
    		#取出要进行操作的对象
        setstate = getattr(inst, "__setstate__", None)
    		#检查有没有__setstate__方法,有就调用
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        if state:
    		#属性转字典并且逐位赋值
            inst_dict = inst.__dict__
            intern = sys.intern
            for k, v in state.items():
                if type(k) is str:
                    inst_dict[intern(k)] = v
                else:
                    inst_dict[k] = v
        if slotstate:
            for k, v in slotstate.items():
                setattr(inst, k, v)

    b:BUILD:load_buildBUILD操作将操作栈中末尾字典弹出作为栈中末尾对象的属性字典进行赋值操作,并且如果对象有__setstate__方法就调用该方法进行赋值操作

  • .
    取出.操作符,对应STOP,进入load_stop函数

    # every pickle ends with STOP
    def load_stop(self):
        value = self.stack.pop()
        raise _Stop(value)

    .:STOP:load_stopSTOP操作将栈尾作为最终返回值弹出,并抛出_Stop

更高协议的不同之处