JZX轻语:简

[Python导入系统] 如何调试importlib_bootstrap模块

发表于2024年10月02日

#Python #导入系统 #Python进阶 #调试

书接上回,我们已经知道如何调试大部分的冻结模块,然而,由于importlib_bootstrap属性是通过导入_frozen_importlib这个不同名的冻结模块实现的,前文的方法仍无法做到对importlib._bootstrap的源码级调试,需要进一步研究如何通过patch的方式,诱拐解释器载入源码实现调试。

importlib._bootstrap的导入实现

上文说到,importlib._bootstrapimportlib的一个属性,本质上指向的是一个名为_frozen_importlib的冻结模块。与此同时,我们发现Python提供的标准库(目录Lib)中,importlib包里面有一个_bootstrap.py文件,其就是_frozen_importlib的源码。

`importlib`包含有`_bootstrap.py`文件

importlib包体包含_bootstrap.py文件

因此问题在于,对于importlib._bootstrap,我们怎样引导Python解释器载入importlib包的_bootstrap.py源码文件,而非_frozen_importlib冻结模块

我们来看看importlib的实现,观察importlib._bootstrap是如何导入的:

# importlib.__init__.py的部分源码

import _imp  # Just the builtin component, NOT the full Python module
import sys

try:
    import _frozen_importlib as _bootstrap
except ImportError:
    from . import _bootstrap
    _bootstrap._setup(sys, _imp)
else:
    # importlib._bootstrap is the built-in import, ensure we don't create
    # a second copy of the module.
    _bootstrap.__name__ = 'importlib._bootstrap'
    _bootstrap.__package__ = 'importlib'
    try:
        _bootstrap.__file__ = __file__.replace('__init__.py', '_bootstrap.py')
    except NameError:
        # __file__ is not guaranteed to be defined, e.g. if this code gets
        # frozen by a tool like cx_Freeze.
        pass
    sys.modules['importlib._bootstrap'] = _bootstrap
    
# ...

可以看到,importlib先尝试直接通过import _frozen_importlib的方式导入冻结模块,如果导入成功,则执行else后面的语句块,使得_frozen_importlib模块能冒充importlib._bootstrap子模块。如果导入_frozen_importlib失败,则importlib会回退到导入_bootstrap.py源码,这也是我们所希望的方式,导入源码就意味着调试器也能对其进行源码级的调试!

诱导Python重新导入importlib._bootstrapimportlib._bootstrap_external 的源码版本

在本节中,我们通过importlib.import_module方法导入importlib._bootstrap源码,并改变import语句的入口点,指向importlib._bootstrap非冻结版本的__import__实现。此外,我们还顺便导入importlib._bootstrap_external的源码版本

def _patch_importlib_bootstrap():
    import sys
    import builtins
    import importlib
    import _imp

    # 首先删除掉sys.modules里相关的模块,以便重新加载
    del sys.modules['importlib._bootstrap']
    del sys.modules['_frozen_importlib']
    del sys.modules['_frozen_importlib_external']

    # 加载importlib._bootstrap的源码
    boostrap_py_module = importlib.import_module('importlib._bootstrap')
    # 模仿importlib的行为, 调用_setup方法初始化
    boostrap_py_module._setup(sys, _imp)
    # 重新设置sys.modules, 使得importlib._bootstrap指向新的、源码加载的模块
    sys.modules['importlib._bootstrap'] = importlib._bootstrap = boostrap_py_module
    # !!! 注意这里, 我们也需要将import语句的入口设置为boostrap_py_module.__import__
    builtins.__import__ = importlib.__import__ = boostrap_py_module.__import__

    # 同上, 加载_frozen_importlib_external的源码
    boostrap_external_module = importlib.import_module('_frozen_importlib_external')
    boostrap_external_module._set_bootstrap_module(boostrap_py_module)
    sys.modules['_frozen_importlib_external'] = _frozen_importlib_external = boostrap_external_module

_patch_importlib_bootstrap()

上述代码主要有以下关键点:

最后,我们尝试下调试import语句,看看能不能进去到导入系统里面。从下面的动图可以发现,我们先往import PyQt5语句打上断点,命中后并成功Step Into里面的源码,且成功触发了_bootstrap.py源码里的断点,这说明我们的补丁方法成功做到了对importlib._bootstrap的源码级调试。

补丁后,成功对`importlib._bootstrap`进行源码级的调试

补丁后,成功对importlib._bootstrap进行源码级的调试

总结

在本文中,我们基于上一篇文章,进一步研究了如何通过补丁的方式实现对importlib_bootstrap模块的源码级调试,为我们后续对导入系统的研究打下了良好的基础。Let’s enjoy Python!

上一篇

[Python进阶] 如何解冻和调试冻结模块(Frozen Modules)

下一篇

本站极简风格分支已发布