ArkAnalyzer源码初步分析I:https://blog.csdn.net/2302_80118884/article/details/151627341?spm=1001.2014.3001.5501
首先,我们必须明确 PTA 的核心工作:它不再关心变量的“声明类型”,而是为程序中的每一个变量和每一个对象字段维护一个**“指向集”(Points-to Set)。这个集合里包含了该变量可能指向的所有对象实例**。分析过程就是通过代码语句,不断更新和传递这些“指向集”。
我们还是使用 Animal
的例子,但这次会引入更复杂的结构。
// --- 我们的基础类 ---
abstract class Animal { abstract sound(): void; }
class Dog extends Animal { sound() { /* 汪汪 */ } }
class Cat extends Animal { sound() { /* 喵喵 */ } }
class Pig extends Animal { sound() { /* 哼哼 */ } } // Pig 还是存在,但我们看看 PTA 能否排除它// --- 更复杂的业务逻辑 ---
class PetStore {bestSeller: Animal | null = null;inventory: Animal[] = [];setBestSeller(pet: Animal) {this.bestSeller = pet;}stockInventory(pets: Animal[]) {this.inventory = pets;}promoteBestSeller() {if (this.bestSeller) {this.bestSeller.sound();}}
}function choosePet(isMorning: boolean): Animal {let chosenPet: Animal;if (isMorning) {chosenPet = new Dog();} else {chosenPet = new Cat();}return chosenPet;
}
现在,让我们分析一个使用这些复杂结构的 main
函数。
场景 1: 通过对象字段(Field Assignments)传递
代码:
function main1() {const store = new PetStore();const myDog = new Dog();// 1. 将 Dog 实例设置到 store 的字段中store.setBestSeller(myDog);// 2. 调用一个方法,该方法会使用这个字段store.promoteBestSeller();
}
PTA 分析过程:
-
const store = new PetStore();
- PTA 创建一个
PetStore
的实例,我们称之为PetStore_obj1
。 - 它的“指向集账本”记录:
store
->{ PetStore_obj1 }
。
- PTA 创建一个
-
const myDog = new Dog();
- PTA 创建一个
Dog
的实例,称之为Dog_obj1
。 - 账本记录:
myDog
->{ Dog_obj1 }
。
- PTA 创建一个
-
store.setBestSeller(myDog);
- 这是一个方法调用。PTA 会分析
setBestSeller
内部。 - 内部执行
this.bestSeller = pet;
。 - PTA 知道,
this
指向store
(即PetStore_obj1
),参数pet
指向myDog
(即Dog_obj1
)。 - 于是,它执行了一次数据流传递:将
pet
的指向集{ Dog_obj1 }
赋给了this.bestSeller
字段。 - 账本更新:
PetStore_obj1.bestSeller
->{ Dog_obj1 }
。
- 这是一个方法调用。PTA 会分析
-
store.promoteBestSeller();
- PTA 进入
promoteBestSeller
方法,this
仍然指向PetStore_obj1
。 - 分析到
this.bestSeller.sound();
。 - PTA 查询
this.bestSeller
的指向集。账本上写着:PetStore_obj1.bestSeller
->{ Dog_obj1 }
。 - 结论: 指向集里只有一个
Dog
对象。因此,这里的sound()
调用唯一的目标就是Dog.sound()
。
- PTA 进入
对比 RTA: 如果 RTA 看到代码里别处有 new Cat()
,它可能会错误地认为 this.bestSeller.sound()
也可能调用 Cat.sound()
。PTA 通过追踪数据流,精确地排除了这种可能。
场景 2: 通过数组/集合(Collections)传递
代码:
function main2() {const store = new PetStore();const todayStock = [new Dog(), new Cat()];// 1. 将一个包含 Dog 和 Cat 的数组设置到 store 的库存中store.stockInventory(todayStock);// 2. 从库存中取出一个宠物(静态分析时无法知道索引是几)const randomPet = store.inventory[0]; randomPet.sound();
}
PTA 分析过程:
这是一个非常关键且具有挑战性的场景。大多数为了性能而设计的 PTA 算法是**“数组不敏感”(Array-insensitive)或“集合不敏感”(Collection-insensitive)**的。这意味着它们会将整个数组或集合视为一个“大容器”,而不会区分里面的每个元素。
-
const todayStock = [new Dog(), new Cat()];
- PTA 创建
Dog_obj2
和Cat_obj1
。 - PTA 创建一个数组实例
Array_obj1
。 - 由于“数组不敏感”,它会将
Dog_obj2
和Cat_obj1
的信息合并到这个数组容器的指向集中。 - 账本记录:
todayStock
->{ Array_obj1 }
。 - 同时记录:
Array_obj1[*]
->{ Dog_obj2, Cat_obj1 }
。([*]
表示数组的任意元素)
- PTA 创建
-
store.stockInventory(todayStock);
- 类似于场景1,数据流发生传递。
- 账本更新:
PetStore_obj2.inventory
->{ Array_obj1 }
。
-
const randomPet = store.inventory[0];
- PTA 需要确定
randomPet
的指向集。 - 它首先找到
store.inventory
,发现它指向Array_obj1
。 - 然后它需要从
Array_obj1
中取元素。因为分析是静态的,且是“数组不敏感”的,它无法确定索引[0]
到底会取出谁。 - 因此,它必须做出一个保守的假设:
randomPet
可能指向Array_obj1
容器里的任何一个对象。 - 它将整个容器的指向集
{ Dog_obj2, Cat_obj1 }
赋给了randomPet
。 - 账本更新:
randomPet
->{ Dog_obj2, Cat_obj1 }
。
- PTA 需要确定
-
randomPet.sound();
- PTA 查询
randomPet
的指向集,发现是{ Dog_obj2, Cat_obj1 }
。 - 结论: 指向集里既有
Dog
也有Cat
。因此,这里的sound()
调用可能的目标是Dog.sound()
和Cat.sound()
。
- PTA 查询
关键点: 在处理集合时,为了保证分析的可行性(在有限时间内完成)和稳健性(Soundness,即不错报任何可能的调用),PTA 会牺牲一定的精度(Precision),通过合并指向集来处理不确定性。
场景 3: 通过条件逻辑和函数返回值(Control Flow & Return Values)
代码:
function main3() {// 1. 调用一个根据条件返回不同类型对象的函数const petOfTheDay = choosePet(new Date().getHours() < 12);// 2. 调用返回对象的方法petOfTheDay.sound();
}
PTA 分析过程:
-
const petOfTheDay = choosePet(...)
- PTA 需要分析
choosePet
函数来确定其返回值的指向集。 - 静态分析器无法知道
new Date().getHours() < 12
的结果是true
还是false
。 - 因此,它必须分析所有可能的执行路径。
- 路径 A (
if
分支):chosenPet = new Dog();
->chosenPet
指向{ Dog_obj3 }
。函数返回chosenPet
。 - 路径 B (
else
分支):chosenPet = new Cat();
->chosenPet
指向{ Cat_obj2 }
。函数返回chosenPet
。
- 路径 A (
- 在函数结束,控制流重新汇合时,PTA 必须合并所有路径的结果。
- 因此,PTA 为
choosePet
函数计算出一个调用摘要(Summary):choosePet
的返回值指向集是{ Dog_obj3, Cat_obj2 }
。 - 当分析
main3
中的调用时,它直接应用这个摘要。 - 账本更新:
petOfTheDay
->{ Dog_obj3, Cat_obj2 }
。
- PTA 需要分析
-
petOfTheDay.sound();
- PTA 查询
petOfTheDay
的指向集,发现是{ Dog_obj3, Cat_obj2 }
。 - 结论: 这里的
sound()
调用可能的目标是Dog.sound()
和Cat.sound()
。
- PTA 查询
总结
PTA 通过以下方式处理复杂情况:
- 数据流跟踪: 它能跟踪对象实例如何通过变量赋值、参数传递、字段存储和函数返回在程序中“流动”。
- 路径合并: 当遇到
if/else
或循环等控制流分支时,它会分析所有可能路径,然后在路径汇合点合并结果(指向集)。这保证了不会遗漏任何可能性。 - 保守抽象: 在遇到它无法精确分析的情况时(如数组索引、复杂的反射调用等),它会做出保守的假设(例如,把整个数组看成一个整体),这会牺牲精度,但能保证结果的“稳健性”(即结果集一定包含了所有真实运行时的可能调用)。
所以,PTA 的强大之处在于,即使代码逻辑错综复杂,它也能给出一个包含所有真实调用且尽可能精确的调用图。它是进行更高级程序分析(如安全漏洞扫描、资源泄露检测)的基石,因为这些分析都依赖于一个准确的调用关系和数据流动图。