Slipcover的拓展过程

文章讲述了作者在使用slipcover进行Python函数插桩时遇到的问题,发现由于只处理了顶层代码而忽略了内联函数,导致覆盖率为零。通过分析字节码,作者找到了问题所在并提出了解决方案,即在函数对象的内联代码中进行插桩操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、目标

最开始,我的目标需求是在slipcover的基础上,增加对函数的插桩,也就是在每个def函数之后插入一条语句。

那slipcover又是什么东西呢?它是一个python的动态插桩覆盖率统计工具,它会在加载python模块的时候,自定义一个loader,然后在load的过程中,对python字节码进行修改。
当程序运行的时候,如果是已加载的模块它会直接使用插桩后的模块,对于需要新加载的模块,就进行修改插桩并返回。

同时我们的测试过程会设计到多进程,其实在python中,使用mutilprocessing的process创建的子进程会继承父进程已加载的模块,因为我们都是在linux系统上进行的试验。

当你使用Python的multiprocessing.Process类来创建子进程,是否会继承父进程中加载的模块取决于操作系统及其'fork模式
在 Unix-like 系统上
在 Unix-like 系统(如Linux和macOS)上,Python的’multiprocessing’默认使用’fork()系统调用来创建子进程。在·fork()调用之后,子进程会继承父进程的整个内存空间的副本,包括所有已加载的模块。这意味着,使用你提供的代码片段创建的子进程会“看到”所有在'Process对象创建之前由父进程加载的模块。这种行为使得在子进程启动时无需重新加载这些模块。

所以,创建子进程之后,并不需要重新设置类加载器,直接在父进程中创建子进程运行测试用例即可。

在meta_path中添加一个模块加载器,这个加载器在加载模块时会进行字节码插桩
sys.meta_path.insert(0, SlipcoverMetaPathFinder(CoveragePython.sci, CoveragePython.file_matcher, False))

2、思路

我的初步想法也是参考slipcover本身来进行的;

  1. 首先,就是通过修改py文件本身的ast树,在每个def函数开始的位置,插入一个赋值语句,为了后续查找它的字节偏移来保证插桩;
  2. 然后,通过dis.dis获取字节码偏移,找到对应的赋值语句的偏移,然后再在对应位置进行插桩,至此结束

3、问题

经过上面的实现,我发现覆盖率为0。

具体来说,在import的时候,这里是可以统计到覆盖率的。经过调研,发现import其实是未加载的字节码,也是会执行代码的,那它具体会执行哪些代码呢?实际上是那些顶层代码,就是一般写在py文件函数外部的代码,比如说一些初始变量设置。

也就是说我的插桩逻辑只能统计这部分的覆盖,而在实际运行的过程中,一个覆盖都没有。

这里就不禁思考是不是插桩的位置出问题了呢?

经过一系列的对比(插桩前后的字节码对比,以及slipcover本来的插桩逻辑和我的逻辑)

ast树修改这块是没问题的;
问题出在第二步,插桩里面。

终于发现了问题所在:代码(types.CodeType)其实包含了两部分,一部分是顶层代码(也就是我前面提到的,在类加载会执行的代码),一部分是内部的其他函数对象(types.CodeType.co_consts)

那我原来为什么插桩插错了呢?

因为我原来就仅仅对顶层代码进行了处理,并没有考虑内部的其他函数对象,导致我插桩的内容都是在函数外层,且slipcover本身是行覆盖(它会在每一行的前面插入自定义的c代码),我基于它的实现就也插在了def的前面。那这就导致了后续执行的时候根本没有覆盖率。

总结:就是我插桩的代码只会执行一次,因为在外部,属于类加载会运行的代码,那么后续执行api的时候,100%不会执行到自定义的插桩代码

4、解决

 # handle functions-within-functions
        for i, c in enumerate(co.co_consts):
            if isinstance(c, types.CodeType):
                ed.set_const(i, self.instrument(c, co))

上面的就是原本slipcover处理py文件内部的函数对象的代码,就是需要对co.co_consts进行处理,这里才是我们真正需要插桩的位置。

所以就在这一块去处理,使用原来的逻辑即可。

def lfl_instrument(self, co: types.CodeType, modify_ast: ast.AST) -> types.CodeType:
        # 实际上,我是需要在code对象中进行插桩,也就是co的co_consts
        ed = bc.Editor(co)
        for i, c in enumerate(co.co_consts):
            if isinstance(c, types.CodeType):
                ed.set_const(i, self.lfl_instrument_pro(c, modify_ast))
        new_code = ed.finish()
        return new_code

5、例子分析

这里给出py字节码的一个例子,表示我的分析过程。

  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (os)
              6 STORE_NAME               0 (os)

  3           8 LOAD_CONST               2 (1)
             10 STORE_NAME               1 (a)

  4          12 LOAD_CONST               2 (1)
             14 STORE_NAME               2 (b)

  7          16 LOAD_CONST               3 (<code object add at 0x7fdb4ab0c450, file "/root/src/test.py", line 7>)
             18 LOAD_CONST               4 ('add')
             20 MAKE_FUNCTION            0
             22 STORE_NAME               3 (add)

 11          24 LOAD_CONST               5 (<code object subs at 0x7fdb4ab0c500, file "/root/src/test.py", line 11>)
             26 LOAD_CONST               6 ('subs')
             28 MAKE_FUNCTION            0
             30 STORE_NAME               4 (subs)
             32 LOAD_CONST               1 (None)
             34 RETURN_VALUE

Disassembly of <code object add at 0x7fdb4ab0c450, file "/root/src/test.py", line 7>:
  7           0 LOAD_CONST               1 ('add')
              2 STORE_FAST               2 (_LFL_FUNC_NAME)

  8           4 LOAD_GLOBAL              0 (print)
              6 LOAD_CONST               2 ('yes done')
              8 CALL_FUNCTION            1
             10 POP_TOP

  9          12 LOAD_FAST                0 (a)
             14 LOAD_FAST                1 (b)
             16 BINARY_ADD
             18 RETURN_VALUE

Disassembly of <code object subs at 0x7fdb4ab0c500, file "/root/src/test.py", line 11>:
 11           0 LOAD_CONST               1 ('subs')
              2 STORE_FAST               2 (_LFL_FUNC_NAME)

 12           4 LOAD_GLOBAL              0 (print)
              6 LOAD_CONST               1 ('subs')
              8 CALL_FUNCTION            1
             10 POP_TOP

 13          12 LOAD_FAST                0 (a)
             14 LOAD_FAST                1 (b)
             16 BINARY_SUBTRACT
             18 RETURN_VALUE

从上面的字节码可以看到,每个函数都有自己单独的字节偏移,我们其实就是需要在这个函数对象内部进行插桩。
当遍历到函数对象的时候才进行处理即可。

而且每个函数对象内部的字节码偏移都是从0开始的,也就是说每个函数对象和外部代码对象都是互相不影响的。(字节偏移为每个函数或者代码内部的局部设计,不会影响其他函数或者全局代码,这样每个函数的编译、优化和执行就可以独立进行)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

meilidekcl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值