PyCG: Practical Call Graph Generation in Python
摘要
调用图在不同的上下文中起着重要的作用,比如配置分析和漏洞传播分析。当涉及到模块化的、包含动态特性和高阶函数的高级语言时,以一种高效的方式生成调用图可能是一项具有挑战性的任务。
尽管这种语言很受欢迎,但针对Python程序生成调用图的工具却很少。更糟糕的是,这些工具存在几个有效性问题,这限制了它们在现实程序中的实用性。我们提出了一种实用的、静态的方法来在Python中生成调用图。我们通过过程间的分析来计算函数、变量、类和模块的程序标识符之间的所有分配关系。基于这些赋值关系,我们通过解析对潜在调用函数的所有调用来生成生成的调用图。值得注意的是,底层分析被设计为高效和可伸缩的,可以处理一些Python特性,如模块、生成器、函数闭包和多个继承。
我们已经评估了我们的原型实现,我们称之为PyCG,使用了两个基准测试:一个包含小Python程序的微基准测试套件和一组包含几个流行的现实世界Python包的宏基准测试。我们的结果表明,PyCG可以在不到一秒的时间内有效地处理数千行代码(平均1kLoC为0.38秒)。此外,它在精度和查全率方面都优于最先进的Python:PyCG实现了高比率的精度99.2%,和足够的召回率~69.9%。最后,我们通过展示GitHub的“安全咨询”通知服务的潜在增强,来演示PyCG如何帮助依赖影响分析。
优势
- 新:可以处理一些Python特性,如模块、生成器、函数闭包和多个继承。
- 快:PyCG可以在不到一秒的时间内有效地处理数千行代码(平均1kLoC为0.38秒)
- 准:PyCG实现了高比率的精度99.2%
劣势
- 召回率还不够高(比起Pyan和Dependence高),会缺失很多边
贡献
一、我们提出了一种Python中实用调用图的静态方法。我们的方法对一种中间语言执行程序间分析,该语言记录了程序标识符之间的分配关系,即函数、变量、类和模块之间的分配关系。然后,它检查记录的关联以提取调用图。
二、我们开发了一个微基准套件,可以用作评估Python中调用图生成方法的标准。我们的套件是模块化的,易于扩展,并且涵盖了与类、生成器、字典等相关的Python的大部分功能。
三、我们通过微基准和一组宏观基准,包括几个中型Python项目,来评估我们的方法的有效性。在所有情况下,我们的方法都实现了很高的精度和召回率,优于其他可用的分析方法。
四、我们演示了我们的方法如何通过潜在的增强GitHub的“安全咨询”通知服务来帮助依赖性影响分析。
论文中提到的挑战
Higher-order Functions
变量可以指向函数
>>> abs(-10)
10
>>> abs
<built-in function abs>
可见,abs(-10)是函数调用,而abs是函数本身。
要获得函数调用结果,我们可以把结果赋值给变量
>>> x = abs(-10)
>>> x
10
函数本身也可以赋值给变量
>>> f = abs
>>> f(-10)
10
Nested Definitions
函数嵌套函数
def max_value(list_data):
value = list_data[0]
def compare(x,y):
return x if x > y else y
for x in range(1,len(list_data)):
value = compare(list_data[x],value)
return value
Classes
继承解析顺序
Modules
跟踪在应用程序中导入的不同模块以及这些导入的解析顺序可能是一项具有挑战性的任务。
Dynamic Features
Python是动态类型的,允许变量在执行期间获取不同类型的值。此外,它还允许在运行时动态地修改类。此外,eval函数允许将一个动态构造的字符串作为代码执行。
Duck Typing
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。
# I love FishC.com!
class Duck:
def quack(self):
print("呱呱呱!")
def feathers(self):
print("这个鸭子拥有灰白灰白的羽毛。")
class Person:
def quack(self):
print("你才是鸭子你们全家人是鸭子!")
def feathers(self):
print("这个人穿着一件鸭绒大衣。")
def in_the_forest(duck):
duck.quack()
duck.feathers()
def game():
donald = Duck()
john = Person()
in_the_forest(donald)
in_the_forest(john)
game()
in_the_forest() 函数对参数 duck 只有一个要求:就是可以实现 quack() 和 feathers() 方法。然而 Duck 类和 Person 类都实现了 quack() 和 feathers() 方法,因此它们的实例对象 donald 和 john 都可以用作 in_the_forest() 的参数。这就是鸭子类型。
论文中用来对比的Pyan和Depends
这个Python文件引入了一个cryptops模块,并定义了一个函数Crypto,里面实现了__init__和apply方法,其中apply方法的方法有一个参数需要一个函数传入。
然后我们创建了一个类Crypto,并用cryptops中的函数传入Crypto.apply方法
这是三个工具生成的调用图:
Pyan的结果不太精确,没有包含所有的函数调用,没有将cryptops中的encrypt和decrypt方法检测到。Pyan还会引入多余的调用边,在模块导入的情况下,Pyan会生成一个从导入名称空间到模块名称的调用边。
Depends 结果图不包含源程序中包含的大部分调用。依赖项不会跟踪导致参数函数缺失边的函数的过程间流。
调用图生成步骤
1、使用不动点迭代算法,并逐步构建赋值图,这是一个显示程序标识符之间分配关系的结构(第三-a节)
2、下一步中,我们使用赋值图来构建原始程序的调用图。
上下文语法
Identifier表示程序标识符
IdentType表示标识符类型,有Func(函数) var(变量) Cls(类) mod(模块)
Definition表示定义,例如(int a)
Namespace表示命名空间,为了区分不同变量的作用范围第一个var的命名空间是[(main,mod)], 第二个var的命名空间 [(main,mod), (A,cls)].
Obj表示对象,我们的方法将每个对象都视为由该语言所支持的表达式的求值所给出的值。
Expr表示表达式。我们的表示法包含捕获过程间流、分配语句、类和函数定义、模块导入和迭代器/生成器的表达式(请参阅Expr规则)。该语言能够抽象出不同的特性,包括lambda表达式、关键字参数、构造函数、多重继承等等。
E表示属性赋值图