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

由Jeza Chen 发表于 October 2, 2024

书接上回,我们已经知道如何调试大部分的冻结模块,然而,由于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()

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

  • 我们先删掉sys.modules中相关的模块,避免后续的导入流程直接使用缓存

  • 其次,我们通过importlib.import_module('importlib._bootstrap')的方式,尝试通过源码导入importlib._bootstrap模块。为什么这里就能够导入非冻结版本呢?我们在上面已经提到,importlib._bootstrap实际上是importlib属性,其在importlib的初始化过程中指向的一个不同名的冻结模块_frozen_importlib。注意到该函数中,我们并没有删掉importlib的模块缓存,也就是说,importlib._bootstrap不会重新触发importlib.__init__.py里的代码。此时importlib._bootstrap判断父模块importlib已经在缓存中,从而importlib所在路径作为搜索入口,检索名为_bootstrap的模块,因此就定位到文件_bootstrap.py,从而加载的是源码版本。

    注:如果我们删除了importlib的模块缓存(即sys.modules.pop('importlib', None)),调用importlib.import_module('importlib._bootstrap')将会先导入importlib,此时如上文所述,importlib的初始化过程会导入冻结模块_frozen_importlib作为其_bootstrap属性,并加入到sys.modules中,而后,importlib.import_module发现sys.modules已经存在importlib._boostrap,于是不会继续后面的导入操作!这个是Python导入机制的一个副作用,具体可以看先前的这篇文章

  • 最后,我们还需要将非冻结版本的importlib._bootstrap模块加入到缓存sys.modules中,并将builtins.__import__importlib.__import__ 均指向非冻结版本的importlib._bootstrap模块里的__import__方法。否则,后续的import语句仍使用之前的冻结版本。

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

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

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

总结

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