python逆向之原理浅析

一、py文件运行原理浅析:

1.Python生成的各文件:

py: python 脚本文件(source code)
pyc: 脚本文件编译得到的字节码, 二进制文件,python文件经过编译器编译之后的文件。可以提高文件加载速度
pyo: 脚本文件开启优化编译选项(-O)编译得到的字节码,二进制文件,优化编译后的文件。可以通过python -O file.py生成。
pyd: 基本的Windows DLL文件, python的动态链接库。

2.python执行py文件的流程

  1. 首先将文件里面的内容读取出来, 所以从这个角度上讲, 文件名不一定非要是.py结尾, .txt也是可以的, 只要文件里面的内容符合Python代码规范即可.
  2. 读取文件里面的内容之后会对其进行分词, 将源代码切分成一个一个的token.
  3. 然后Python编译器会对token进行语法解析, 建立抽象语法树(AST, abstract syntax tree)
  4. 编译器再将得到AST编译成字节码
  5. 最终由Python虚拟机来执行字节码

3.啥是Python解释器、编译器、虚拟机

  • Python解释器=Python编译器+Python虚拟机
  • Python编译器:负责将Python源代码编译成字节码(包括文件读取、分词、建立AST、编译成字节码)
  • Python虚拟机负责执行这些字节码

python.exe可以看到其实不到100k,不可能容纳一个解释器加一个虚拟机,所以下面还有一个python38.dll,其实编译器、虚拟机都藏身于python38.dll当中。

4.PyCodeObject对象与pyc文件的关系

Python编译器的编译结果→PyCodeObject对象。

而pyc的文件结构为magic numberTimestamp + PyCodeObject。

  • magic number:这是Python定义的一个整数值。.pyc文件的Magic Number是为了确保Python解释器加载与其版本匹配的.pyc文件。当Python编译器编译.py文件生成.pyc文件时,它会在.pyc文件的开头写入一个特定的Magic Number,这个值是特定于Python版本的。

    当Python解释器尝试加载一个.pyc文件时,它首先会检查这个Magic Number。如果Magic Number与解释器期望的值不匹配,解释器会拒绝加载这个.pyc文件。这确保了Python解释器不会尝试执行为其他版本的Python编译的字节码,因为不同版本的Python可能有不同的字节码指令集。

  • Timestamp:这是.py源文件的最后修改时间。当Python尝试运行.pyc文件时,它会检查关联的.py文件的最后修改时间。如果.py文件比.pyc文件更新,Python会重新编译它。
  •  PyCodeObject:这是编译后的Python代码,存储为字节码。这是在Python解释器中实际执行的部分。下面会对这个对象的主要成员进行介绍。

tips1:关于魔数各版本的对应值可以在Python 的源代码中的 importlib/_bootstrap_external.py找到:

# Known values:
#  Python 1.5:   20121
#  Python 1.5.1: 20121
#     Python 1.5.2: 20121
#     Python 1.6:   50428
#     Python 2.0:   50823
#     Python 2.0.1: 50823
#     Python 2.1:   60202
#     Python 2.1.1: 60202
#     Python 2.1.2: 60202
#     Python 2.2:   60717
#     Python 2.3a0: 62011
#     Python 2.3a0: 62021
#     Python 2.3a0: 62011 (!)
#     Python 2.4a0: 62041
#     Python 2.4a3: 62051
#     Python 2.4b1: 62061
#     Python 2.5a0: 62071
#     Python 2.5a0: 62081 (ast-branch)
#     Python 2.5a0: 62091 (with)
#     Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
#     Python 2.5b3: 62101 (fix wrong code: for x, in ...)
#     Python 2.5b3: 62111 (fix wrong code: x += yield)
#     Python 2.5c1: 62121 (fix wrong lnotab with for loops and
#                          storing constants that should have been removed)
#     Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
#     Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
#     Python 2.6a1: 62161 (WITH_CLEANUP optimization)
#     Python 2.7a0: 62171 (optimize list comprehensions/change LIST_APPEND)
#     Python 2.7a0: 62181 (optimize conditional branches:
#                          introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
#     Python 2.7a0  62191 (introduce SETUP_WITH)
#     Python 2.7a0  62201 (introduce BUILD_SET)
#     Python 2.7a0  62211 (introduce MAP_ADD and SET_ADD)
#     Python 3000:   3000
#                    3010 (removed UNARY_CONVERT)
#                    3020 (added BUILD_SET)
#                    3030 (added keyword-only parameters)
#                    3040 (added signature annotations)
#                    3050 (print becomes a function)
#                    3060 (PEP 3115 metaclass syntax)
#                    3061 (string literals become unicode)
#                    3071 (PEP 3109 raise changes)
#                    3081 (PEP 3137 make __file__ and __name__ unicode)
#                    3091 (kill str8 interning)
#                    3101 (merge from 2.6a0, see 62151)
#                    3103 (__file__ points to source file)
#     Python 3.0a4: 3111 (WITH_CLEANUP optimization).
#     Python 3.0b1: 3131 (lexical exception stacking, including POP_EXCEPT
                          #3021)
#     Python 3.1a1: 3141 (optimize list, set and dict comprehensions:
#                         change LIST_APPEND and SET_ADD, add MAP_ADD #2183)
#     Python 3.1a1: 3151 (optimize conditional branches:
#                         introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE
                          #4715)
#     Python 3.2a1: 3160 (add SETUP_WITH #6101)
#                   tag: cpython-32
#     Python 3.2a2: 3170 (add DUP_TOP_TWO, remove DUP_TOPX and ROT_FOUR #9225)
#                   tag: cpython-32
#     Python 3.2a3  3180 (add DELETE_DEREF #4617)
#     Python 3.3a1  3190 (__class__ super closure changed)
#     Python 3.3a1  3200 (PEP 3155 __qualname__ added #13448)
#     Python 3.3a1  3210 (added size modulo 2**32 to the pyc header #13645)
#     Python 3.3a2  3220 (changed PEP 380 implementation #14230)
#     Python 3.3a4  3230 (revert changes to implicit __class__ closure #14857)
#     Python 3.4a1  3250 (evaluate positional default arguments before
#                        keyword-only defaults #16967)
#     Python 3.4a1  3260 (add LOAD_CLASSDEREF; allow locals of class to override
#                        free vars #17853)
#     Python 3.4a1  3270 (various tweaks to the __class__ closure #12370)
#     Python 3.4a1  3280 (remove implicit class argument)
#     Python 3.4a4  3290 (changes to __qualname__ computation #19301)
#     Python 3.4a4  3300 (more changes to __qualname__ computation #19301)
#     Python 3.4rc2 3310 (alter __qualname__ computation #20625)
#     Python 3.5a1  3320 (PEP 465: Matrix multiplication operator #21176)
#     Python 3.5b1  3330 (PEP 448: Additional Unpacking Generalizations #2292)
#     Python 3.5b2  3340 (fix dictionary display evaluation order #11205)
#     Python 3.5b3  3350 (add GET_YIELD_FROM_ITER opcode #24400)
#     Python 3.5.2  3351 (fix BUILD_MAP_UNPACK_WITH_CALL opcode #27286)
#     Python 3.6a0  3360 (add FORMAT_VALUE opcode #25483)
#     Python 3.6a1  3361 (lineno delta of code.co_lnotab becomes signed #26107)
#     Python 3.6a2  3370 (16 bit wordcode #26647)
#     Python 3.6a2  3371 (add BUILD_CONST_KEY_MAP opcode #27140)
#     Python 3.6a2  3372 (MAKE_FUNCTION simplification, remove MAKE_CLOSURE
#                         #27095)
#     Python 3.6b1  3373 (add BUILD_STRING opcode #27078)
#     Python 3.6b1  3375 (add SETUP_ANNOTATIONS and STORE_ANNOTATION opcodes
#                         #27985)
#     Python 3.6b1  3376 (simplify CALL_FUNCTIONs & BUILD_MAP_UNPACK_WITH_CALL
                          #27213)
#     Python 3.6b1  3377 (set __class__ cell from type.__new__ #23722)
#     Python 3.6b2  3378 (add BUILD_TUPLE_UNPACK_WITH_CALL #28257)
#     Python 3.6rc1 3379 (more thorough __class__ validation #23722)
#     Python 3.7a1  3390 (add LOAD_METHOD and CALL_METHOD opcodes #26110)
#     Python 3.7a2  3391 (update GET_AITER #31709)
#     Python 3.7a4  3392 (PEP 552: Deterministic pycs #31650)
#     Python 3.7b1  3393 (remove STORE_ANNOTATION opcode #32550)
#     Python 3.7b5  3394 (restored docstring as the first stmt in the body;
#                         this might affected the first line number #32911)
#     Python 3.8a1  3400 (move frame block handling to compiler #17611)
#     Python 3.8a1  3401 (add END_ASYNC_FOR #33041)
#     Python 3.8a1  3410 (PEP570 Python Positional-Only Parameters #36540)
#     Python 3.8b2  3411 (Reverse evaluation order of key: value in dict
#                         comprehensions #35224)
#     Python 3.8b2  3412 (Swap the position of positional args and positional
#                         only args in ast.arguments #37593)
#     Python 3.8b4  3413 (Fix "break" and "continue" in "finally" #37830)
#     Python 3.9a0  3420 (add LOAD_ASSERTION_ERROR #34880)
#     Python 3.9a0  3421 (simplified bytecode for with blocks #32949)
#     Python 3.9a0  3422 (remove BEGIN_FINALLY, END_FINALLY, CALL_FINALLY, POP_FINALLY bytecodes #33387)
#     Python 3.9a2  3423 (add IS_OP, CONTAINS_OP and JUMP_IF_NOT_EXC_MATCH bytecodes #39156)
#     Python 3.9a2  3424 (simplify bytecodes for *value unpacking)
#     Python 3.9a2  3425 (simplify bytecodes for **value unpacking)

tips2:pyc的头部在 Python 2 和 Python 3 之间有所不同

  1. Python 2:

    • 魔法数字 (4 bytes): 一个标识 Python 版本的数字。不同的 Python 版本有不同的魔法数字。
    • 时间戳 (4 bytes): 最后一次修改源 .py 文件的时间。

    因此,Python 2 的 .pyc 文件头部总共有 8 bytes。

  2. Python 3:

    • 魔法数字 (4 bytes)
    • 时间戳 (4 bytes)
    • 源文件大小 (from Python 3.3 onwards, 4 bytes): 这表示对应的 .py 文件的大小。

    因此,对于 Python 3.3 及以后的版本,.pyc 文件的头部总共有 12 bytes。但在 Python 3.7 中,引入了一个新的 PEP 552,它提供了一个选项来使用一个 8 字节的哈希值替代时间戳和源文件大小,这会使头部增长到 16 字节。

pyc文件一般位于__pycache__目录中

 

Python中的字节码只是一个PyBytesObject对象、或者说一段字节序列,PyCodeObject对象中有一个成员co_code,它是一个指针,指向了这段字节序列。但是这个对象除了有co_code指向字节码之外,还有很多其它成员,负责保存代码涉及到的常量、变量(名字、符号)等等

所以我们知道了,pyc文件里面的内容是PyCodeObject对象。对于Python编译器来说,PyCodeObject对象才是其真正的编译结果,而pyc文件是这个对象在硬盘上表现形式。

在程序运行期间,编译结果存在于内存的PyCodeObject对象当中,而Python结束运行之后,编译结果又被保存到了pyc文件当中。当下一次运行的时候,Python会根据pyc文件中记录的编译结果直接建立内存中的PyCodeObject对象,而不需要再度重新编译了。

5.py文件与pyc文件的关系

加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。

通过上面的描述,python在整个运行过程中主要涉及源码***.py,编译好的文件xxx.pyc两类文件。其中xxx.pyc是可以由虚拟机直接执行的,是python将目标源码编译成字节码以后再磁盘上的文件形式。

二、PyCodeObject对象详解:

Python编译器会将Python源代码编译成字节码,虚拟机执行的也是字节码,所以要理解虚拟机的运行时的行为,就必须要先掌握字节码。而字节码是被底层结构体PyCodeObject的成员co_code指向,那么我们就必须来看看这个结构体了,它的定义位于 Include/code.h 中。

typedef struct {
    PyObject_HEAD
    int co_argcount;        /* 位置参数个数 */
    int co_nlocals;         /* 局部变量个数 */
    int co_stacksize;       /* 栈大小 */
    int co_flags;   
    PyObject *co_code;      /* 字节码指令序列 */
    PyObject *co_consts;    /* 所有常量集合 */
    PyObject *co_names;     /* 所有符号名称集合 */
    PyObject *co_varnames;  /* 局部变量名称集合 */
    PyObject *co_freevars;  /* 闭包用的的变量名集合 */
    PyObject *co_cellvars;  /* 内部嵌套函数引用的变量名集合 */
    /* The rest doesn’t count for hash/cmp */
    PyObject *co_filename;  /* 代码所在文件名 */
    PyObject *co_name;      /* 模块名|函数名|类名 */
    int co_firstlineno;     /* 代码块在文件中的起始行号 */
    PyObject *co_lnotab;    /* 字节码指令和行号的对应关系 */
    void *co_zombieframe;   /* for optimization only (see frameobject.c) */
} PyCodeObject;

tips 1:Python编译器在对Python源代码进行编译的时候,对于代码中的每一个block,都会创建一个PyCodeObject与之对应。但是多少代码才算得上是一个block呢?事实上,Python有一个简单而清晰的规则:当进入一个新的命名空间,或者说作用域时,我们就算是进入了一个新的block了。
tips 2:命名空间(name space)、也叫名字空间、名称空间,都是一个东西。名字空间是符号的上下文环境,符号的含义取决于名字空间。更具体的说,一个变量名对应的变量值什么,在Python中是不确定的,需要命名空间来决定。

For Example:

class A:
    a = 123
 
 
def foo():
    a = []

上面这个文件,它在编译完之后会有三个PyCodeObject对象,一个是对应整个py文件的,一个是对应class A的,一个是对应def foo的。因为这是三个不同的作用域,所以会有三个PyCodeObject对象。

 1.co_name:

代码块的名字,也就是说函数名(或模块名,类名)

def func(a,b,c=3):
    pass
print(func.__code__.co_name)

2.co_names:

是一个元组,保存了不在当前作用域的函数和变量

e="World"
def func(a,b,c=3):
    print("Hello"+e)
    d=("abc","cba",int,str)
print(func.__code__.co_names)

3.co_varnames:

是一个元组,保存了在当前作用域的变量,就是以字符串的形式保存了变量名

e="World"
def func(a,b,c=3):
    print("Hello"+e)
    d=("abc","cba",int,str)
print(func.__code__.co_varnames)

4.co_code:

这个成员记录的是PyCodeObject结构体的实际字节码

e="World"
def func(a,b,c=3):
    f=123
    print("Hello"+e)
    d=("abc","cba",int,str)
print(func.__code__.co_code)

5.co_lnotab:

字节码指令与Python源代码的行号之间的对应关系

Python不会直接记录这些信息,而是会记录增量值。比如说:
字节码在co_code中的偏移量            .py文件中源代码的行号
0                                  1  
6                                  2
50                                 7

那么co_lnotab就应该是: 0 1 6 1 44 5
0和1很好理解, 就是co_code和.py文件的起始位置
而6和1表示字节码的偏移量是6, .py文件的行号增加了1
而44和5表示字节码的偏移量是44, .py文件的行号增加了5
e="World"
def func(a,b,c=3):
    f=123
    print("Hello"+e)
    d=("abc","cba",int,str)
print(func.__code__.co_lnotab)

6.co_consts:

一个元组对象保存代码块中的所有常量

e="World"
def func(a,b,c=3):
    f=123
    print("Hello"+e)
    d=("abc","cba",int,str)
print(func.__code__.co_consts)

tips:这些底层对应的PyCodeObject对象,可以通__code__函数来获取

猜你喜欢

转载自blog.csdn.net/weixin_46175201/article/details/133350221