一种路径敏感的数据依赖分析算法

这篇工作发表于PLDI 24,提出了一种context- 以semi-path-sensitive的数据依赖分析算法,解决path-sensitive的内存模型中存在的aliasing-path-explosion问题。

1.方法

现有sparse flow-sensitive pointer analysis缺点:

  • 1.auxiliary pre-analysis的精度缺失会导致后续分析大量冗余错误pointer-information的传播。

  • 2.auxiliary pre-analysis只用到了一个point-to集合,而随后的flow-sensitive分析在处理path-sensitive问题时后point-to集合会出现路径爆炸。

为了解决这些问题作者提出了Falcon,Falcon处理的程序语法如下:

请添加图片描述

1.1.Basic Rule

作者定义了两个集合 E E E S S S,还定义了对应的查询方式 ∏ φ \prod_{\varphi} φ

  • E [ p → { . . . } ] E[p \rightarrow \{...\}] E[p{...}] 表示top-level variable p p p 的指向环境集合,每一个环境为 ( φ , o ) (\varphi, o) (φ,o) 表示一个address-taken object及其对应的路径条件。

  • S [ o → { . . . } ] S[o \rightarrow \{...\}] S[o{...}],每一个元素 ( π , l , q ) (\pi, l, q) (π,l,q) 表示top-level variable q q q 在语句 l l l 处路径条件为 π \pi π 时赋值给了 o o ostore 指令)。比如语句 l : ∗ x = q l: *x = q l:x=q,该路径条件 π \pi π x x x 只指向 o o o,那么有 S ( o ) = S ( o )    ∪    ( π , l , q ) S(o) = S(o) \; \cup \; (\pi, l, q) S(o)=S(o)(π,l,q)

  • ∏ φ ( S ( o ) ) = { ( π ∧ φ , l , v )    ∣    ( π , l , v ) ∈ S ( o ) } \prod_{\varphi}(S(o)) = \{(\pi \land \varphi, l, v) \; | \; (\pi, l, v) \in S(o)\} φ(S(o))={(πφ,l,v)(π,l,v)S(o)} 表示address-taken variable o o o 在路径条件 φ \varphi φ 下可能的值。这里值用top-level variable表示,也就是存在value-flow v → φ o v \stackrel{\varphi}{\rightarrow} o vφo

  • ∏ φ ( E ( v ) ) = { ( π ∧ φ , o )      ∣    ( π , o ) ∈ E ( v ) } \prod_{\varphi}(E(v)) = \{(\pi \land \varphi, o)\ \; | \; (\pi, o) \in E(v)\} φ(E(v))={(πφ,o) (π,o)E(v)} 表示top-level variable 在路径条件 π \pi π 下的指向集合。

  • E ( v ) ⨄ E ′ ( v ′ ) = { ( π    ∨    π ′ , o )    ∣    ∀ ( π , o ) ∈ E ( v ) , ∀ ( π ′ , o ) ∈ E ′ ( v ′ ) } E(v) \biguplus E^{'}(v^{'}) = \{(\pi \; \vee \; \pi^{'}, o) \; | \; \forall (\pi, o) \in E(v), \forall(\pi^{'}, o) \in E^{'}(v^{'})\} E(v)E(v)={(ππ,o)(π,o)E(v),(π,o)E(v)},表示两个 E E E 集合合并,一个应用的地方就是 Φ \Phi Φ 指令处合并不同的value时,不过关于具体操作paper里说的不是很明确,个人理解是如果 o o o 在两个 E E E 集合处都出现了,那么合并路径条件 π    ∨    π ′ \pi \; \vee \; \pi^{'} ππ,反之则相当于 π    ∨    f a l s e \pi \; \vee \; false πfalse 或者 f a l s e ∨    π ′ false \vee \; \pi^{'} falseπ

基础的transfer function如下, E , S ⊢ l , φ : s t m t : E ′ , S ′ E, S \vdash l, \varphi : stmt : E^{′}, S^{′} E,Sl,φ:stmt:E,S 表示 E , S E, S E,S 集合经过路径条件 φ \varphi φ 下的语句 l l l 后更新为 E ′ , S ′ E^{′}, S^{′} E,S,其中:

  • alloca 指令 p = & a p = \&a p=&a p p p E E E 集合添加 { φ , a l l o c a a } \{\varphi, alloca_a\} {φ,allocaa}

  • store 指令 ∗ x = q *x = q x=q 更新了 p t s ( x ) pts(x) pts(x) 每个address-taken variable的 S S S 集合。往已有的 S ( o ) S(o) S(o) 集合中添加 ( π , l , q ) (\pi, l, q) (π,l,q) 表示value-flow关系 q → π o q \stackrel{\pi}{\rightarrow} o qπo。同时只有 store 指令会更新 S S S 集合的值,其它只更新 E E E 集合。和SFS类似,如果 ∣ ∏ φ ( E ( x ) ) ∣ = 1 |\prod_{\varphi}(E(x))| = 1 φ(E(x))=1 也就是 x x x 只指向一个address-taken variable ( π , o ) (\pi, o) (π,o),那么对 o o o 进行strong update,这也是kill功能的实现。

  • SequencingBranching 规则表明Falcon可能通过遍历CFG实现(paper没有明说),由于CFG遍历很费时,为了跳过不必要的CFG node,作者后面提出了一个优化方案。

  • load, phi, copy 指令则负责传播指针集合 E E E 的值。其中 load 的最为复杂,个人理解是对于 l , φ : p = ∗ y l, \varphi: p = *y l,φ:p=y,首先 ∏ φ ( E ( y ) ) = { ( π , o ) , . . . } \prod_\varphi(E(y)) = \{(\pi, o),...\} φ(E(y))={(π,o),...} 查询出满足路径条件的所有指向address-taken variable ( π , o ) (\pi, o) (π,o)(此时存在value-flow o → π p o \stackrel{\pi}{\rightarrow} p oπp),由于 load 指令主要是更新 p p p 的指向集 E ( p ) E(p) E(p),因此需要先查询每个 ( π , o ) (\pi, o) (π,o) 的所有值 ∏ π ( S ( o ) ) \prod_\pi(S(o)) π(S(o))(找出所有的 v → φ o v \stackrel{\varphi}{\rightarrow} o vφo),这样找到了value-flow v → φ p v \stackrel{\varphi}{\rightarrow} p vφp,随后合并所有 v v v 的指向集合更新 E ( p ) E(p) E(p)

请添加图片描述

value flow的构建规则如下图所示(value-flow只存在于 store --> load),对于语句 l 1 l_1 l1 处路径条件为 φ 1 \varphi_1 φ1 store 指令 ∗ x = q *x = q x=q,以及语句 l 2 l_2 l2 处路径条件 φ 2 \varphi_2 φ2load 指令 p = ∗ y p = *y p=y,查询路径条件 φ 2 \varphi_2 φ2 y y y 指向的所有address-taken variable ( π i , o i ) (\pi_i, o_i) (πi,oi),通过 ∏ φ i ( S ( o i ) ) \prod_{\varphi_i}(S(o_i)) φi(S(oi)) 查找 φ i \varphi_i φi o i o_i oi 所有可能的值 ( ( φ i , l 1 , q ) (\varphi_i, l_1, q) (φi,l1,q)),也就是value-flow q → φ i o i q \stackrel{\varphi_i}{\rightarrow} o_i qφioi,最后合并所有同source value-flow的路径条件,也就是构造value-flow边 q → ∨ φ i p q \stackrel{\vee \varphi_i}{\rightarrow} p qφip

请添加图片描述
以下图为例,(b)为传统value-flow构建方法, (c)为Falcon,以处理从 store ∗ x = a *x = a x=aload d = ∗ x d = *x d=x 为例,存在 E ( x ) = { ( φ 1 , o 1 ) , ( ¬ φ 1 , o 2 ) } E(x) = \{(\varphi_1, o_1), (\lnot \varphi_1, o_2)\} E(x)={(φ1,o1),(¬φ1,o2)} S ( o 1 ) = { φ 1 , l 1 , a } S(o_1) = \{\varphi_1, l_1, a\} S(o1)={φ1,l1,a} S ( o 2 ) = { ¬ φ 1 , l 2 , a } S(o_2) = \{\lnot \varphi_1, l_2, a\} S(o2)={¬φ1,l2,a} l 1 , l 2 l_1, l_2 l1,l2是占位符)。处理 d = ∗ x d = *x d=x 时query ∏ ¬ φ 2 ( E ( x ) ) \prod_{\lnot \varphi_2}(E(x)) ¬φ2(E(x)),发现 φ 1 ∧ ¬ φ 2 \varphi_1 \land \lnot \varphi_2 φ1¬φ2 ¬ φ 1 ∧ ¬ φ 2 \lnot\varphi_1 \land \lnot \varphi_2 ¬φ1¬φ2 都能满足,因此得到 ∏ ¬ φ 2 ( E ( x ) ) = { ( φ 1 , o 1 ) , ( ¬ φ 1 , o 2 ) } \prod_{\lnot \varphi_2}(E(x)) = \{(\varphi_1, o_1), (\lnot \varphi_1, o_2)\} ¬φ2(E(x))={(φ1,o1),(¬φ1,o2)},接着分别query ∏ φ 1 ( S ( o 1 ) ) \prod_{\varphi_1}(S(o_1)) φ1(S(o1)) ∏ ¬ φ 1 ( S ( o 2 ) ) \prod_{\lnot \varphi_1}(S(o_2)) ¬φ1(S(o2)) 得到 { φ 1 ∧ ¬ φ 2 , l 1 , a } \{\varphi_1 \land \lnot \varphi_2, l_1, a\} {φ1¬φ2,l1,a} { ¬ φ 1 ∧ ¬ φ 2 , l 2 , a } \{\lnot \varphi_1 \land \lnot \varphi_2, l_2, a\} {¬φ1¬φ2,l2,a}。最后得到value-flow a ⟶ ¬ φ 2 d a \stackrel{\lnot \varphi_2}{\longrightarrow} d a¬φ2d(路径条件简化)。与之相比传统方法的value-flow边就多多了。

请添加图片描述

1.2.改进算法

不过目前上图的算法还有优化空间,主要原因包括:

  • 1.直接在CFG上按上面规则进行传播开销过大。而Sparse分析的pre-analysis构建的SVFG包括太多false def-use。因此也会降低性能。

  • 2.大量guard的路径条件需要更新,可能会引起路径爆炸问题,如果严格的对路径条件求解开销过大。

针对问题1作者提出了一个CFG优化方案,主要针对 storeload 的遍历。算法如下图所示(Algo1为 store,Algo2为 load)。优化的重点是 S S S 集合的访问,首先,store 会修改 S S S 的值而 load 会读取其值。

针对问题1,作者优化了 storeload 的遍历规则,其中将 S ( o ) S(o) S(o) 替换为一系列 S l ( o ) S_l(o) Sl(o),表示每个指令 l l l 处address-taken variable o o o 的值。改进后的算法如下图所示,红框为改变处,主要是处理 store 指令时会顺便处理其所有的支配边界,处理 load 时会沿着其直接支配节点回溯。这里需要先进行控制依赖分析。

请添加图片描述
一个示例如下图所示:

  • 在处理 store 语句 l 4 : ∗ x = d l_4: *x = d l4:x=d 时,更新完 S l 4 ( a l l o c m ) S_{l_4}(alloc_m) Sl4(allocm) { ( φ , l 4 , d ) } \{(\varphi, l_4, d)\} {(φ,l4,d)} 后,接着找到 l 4 l_4 l4 的支配边界 l 6 l_6 l6,更新 S l 6 ( a l l o c m ) = { φ , l 4 , d } S_{l_6}(alloc_m) = \{\varphi, l_4, d\} Sl6(allocm)={φ,l4,d}。同理,访问 l 5 l_5 l5 也会做对应的更新。

  • 在处理 load 语句 l 6 : f = ∗ x l_6: f = *x l6:f=x 时, x x x 指向 a l l o c m alloc_m allocm,首先读取 S l 6 ( a l l o c m ) S_{l_6}(alloc_m) Sl6(allocm) 的值,获的 ( φ , l 4 , d ) (\varphi, l_4, d) (φ,l4,d),表示value-flow d ⟶ φ f d \stackrel{\varphi}{\longrightarrow} f dφf;随后,追溯到 l 6 l_6 l6 的直接支配节点 l 3 l_3 l3,读取 ( t r u e , l 3 , c ) (true, l_3, c) (true,l3,c) 的值后,根据Algo2第9行 φ = π ∧ σ ∧ β \varphi = \pi \land \sigma \land \beta φ=πσβ 和第14行 σ = ¬ π ∧ β \sigma = \lnot \pi \land \beta σ=¬πβ 的规则更新为 ( ¬ φ , l 3 , c ) (\lnot \varphi, l_3, c) (¬φ,l3,c),表示value-flow c ⟶ ¬ φ f c \stackrel{\lnot \varphi}{\longrightarrow} f c¬φf c c c 会在 φ \varphi φ 下被kill掉)。根据这两个value-flow更新 E ( f ) E(f) E(f)

请添加图片描述

针对问题2,作者将程序的表达式都抽象为bool skeleton,比如将 x < 2, x >=2, y = 100/2 抽象为布尔谓词 p p p, ¬ p \lnot p ¬p, q q q。其次,Falcon并不使用功能齐全的SAT求解器,而是采用几种线性时间的半决策程序,如unit-propagation,用于识别“简单”的不可满足约束,并执行轻量级的逻辑简化,如消除重言式。在实验中,作者发现约70%的路径条件是可满足的。对于其余的路径条件,其中80%是简单约束,可以通过半决策程序解决。value-flow边的修剪和合并例子如下图所示。

请添加图片描述

1.3.跨函数分析

与SVF不同,作者依然采用采用一个按照call-graph的拓扑序自底向上先构造每个函数的Value-Flow Graph然后以summary-based方法先构造整体value-flow graph,也就是先独立分析每个函数生成summary,然后按需获取summary进行进一步分析。

通常summary需要注意的是side-effect,对于下图(a)所示代码片段,在对 foo 函数生成摘要时,通常会假设参数 y 指向一个address-taken variable o。按照作者的设定,foo 初始处有 E ( y ) = { ( t r u e , o ) } E(y) = \{(true, o)\} E(y)={(true,o)},分析完整个程序后, S ( o ) = { ( φ , l 2 , c ) , ( ¬ φ , l 3 , a ) } S(o) = \{(\varphi, l_2, c), (\lnot \varphi, l_3, a)\} S(o)={(φ,l2,c),(¬φ,l3,a)}。由于 y y yfoo 和其 caller 的接口,因此 foo 的side-effect只涉及到 y y y,因此 E ( y ) E(y) E(y) S ( o ) S(o) S(o) 就是需要的side-effect。

请添加图片描述
summary最终会在分析call语句的时候用到,这里 quxbar 函数都调用了 foo,同时 quxbar 的参数和 bar 一致,假设在 qux 的调用处, E ( x ) = { ( φ 1 ′ , o 1 ) , ( φ 2 ′ , o 2 ) , ( φ 3 ′ , o 3 ) } E(x) = \{(\varphi^{'}_1, o_1), (\varphi^{'}_2, o_2), (\varphi^{'}_3, o_3)\} E(x)={(φ1,o1),(φ2,o2),(φ3,o3)},那么将summary展开后就有了 S ( o 1 ) = { ( φ ∧ φ 1 ′ , l 2 , c ) , ( ¬ φ ∧ φ 1 ′ , l 3 , a ) } S(o_1) = \{(\varphi \land \varphi^{'}_1, l_2, c), (\lnot \varphi \land \varphi^{'}_1, l_3, a)\} S(o1)={(φφ1,l2,c),(¬φφ1,l3,a)}, S ( o 2 ) = { ( φ ∧ φ 2 ′ , l 2 , c ) , ( ¬ φ ∧ φ 2 ′ , l 3 , a ) } S(o_2) = \{(\varphi \land \varphi^{'}_2, l_2, c), (\lnot \varphi \land \varphi^{'}_2, l_3, a)\} S(o2)={(φφ2,l2,c),(¬φφ2,l3,a)}, S ( o 3 ) = { ( φ ∧ φ 3 ′ , l 2 , c ) , ( ¬ φ ∧ φ 3 ′ , l 3 , a ) } S(o_3) = \{(\varphi \land \varphi^{'}_3, l_2, c), (\lnot \varphi \land \varphi^{'}_3, l_3, a)\} S(o3)={(φφ3,l2,c),(¬φφ3,l3,a)}。如果调用有多层,那么摘要数量可能指数爆炸。

针对这个问题,作者引入了一个辅助变量 R R R,上面 foo 的摘要变成 E ( y ) = { ( t r u e , o ) } E(y) = \{(true, o)\} E(y)={(true,o)}, S ( o ) = { ( t r u e , l 4 , R ) } S(o) = \{(true, l_4, R)\} S(o)={(true,l4,R)}, R = { ( φ , l 2 , c ) , ( ¬ φ , l 3 , a ) } R = \{(\varphi, l_2, c), (\lnot \varphi, l_3, a)\} R={(φ,l2,c),(¬φ,l3,a)}。具体的构造方法如下图所示,在添加辅助变量的同时,对于 void 返回类型,算法还会添加一个额外返回值,示例如上图(b)所示

请添加图片描述
假设分析 foo

  • 独立分析完 foo 后有: E ( y ) = { ( t r u e , o ) } E(y) = \{(true, o)\} E(y)={(true,o)}, S ( o ) = { ( φ , l 2 , c ) , ( ¬ φ , l 3 , a ) } S(o) = \{(\varphi, l_2, c), (\lnot \varphi, l_3, a)\} S(o)={(φ,l2,c),(¬φ,l3,a)}, E ( c ) = { t r u e , j } E(c) = \{true, j\} E(c)={true,j}, E ( a ) = { t r u e , k } E(a) = \{true, k\} E(a)={true,k}。(此时 a a a, c c c 均为一阶指针,对其指向集进行指针分析意义不大,故忽略 k , j k, j k,j 的指向集)。value-flow edge包括: ( c = & j ) → ( ∗ y = c ) (c = \&j) \rightarrow (*y = c) (c=&j)(y=c) ( a = & k ) → ( ∗ y = a ) (a = \&k) \rightarrow (*y = a) (a=&k)(y=a) ( ∗ y = c ) ⟶ φ ( R = ∗ y ) (*y = c) \stackrel{\varphi}{\longrightarrow} (R = *y) (y=c)φ(R=y) ( ∗ y = a ) ⟶ ¬ φ ( R = ∗ y ) (*y = a) \stackrel{\lnot \varphi}{\longrightarrow} (R = *y) (y=a)¬φ(R=y)

  • 用Algo3分析后得到 E ( y ) = { t r u e , o y } E(y) = \{true, o_y\} E(y)={true,oy}( o y o_y oy 为新建object), S ( o y ) = { ( t r u e ,    , R ) } S(o_y) = \{(true, \; , R)\} S(oy)={(true,,R)} E ( R ) = { ( φ , j ) , ( ¬ φ , k ) } E(R) = \{(\varphi, j), (\lnot \varphi, k)\} E(R)={(φ,j),(¬φ,k)}

利用 f f f 的summary分析caller语句 l , φ : r = f ( u ) l, \varphi: r = f(u) l,φ:r=f(u) 的规则如下,以caller qux 中的 foo(x) 为例,假设 E ( x ) = { ( φ 1 , o 1 ) } E(x) = \{(\varphi_1, o_1)\} E(x)={(φ1,o1)}

  • (1).在callee的summary中替换形参( y y y)和返回值( R R R),构造中间变量 E f ′ ( y ) = { ( φ 1 , o 1 ) } E^{'}_f(y) = \{(\varphi_1, o_1)\} Ef(y)={(φ1,o1)}, S f ′ ( o 1 ) = { ( φ 1 , , R ) } S^{'}_f(o_1) = \{(\varphi_1, , R)\} Sf(o1)={(φ1,,R)} E ( R ) = { ( φ 1 ∧ φ , j ) , ( φ 1 ∧ ¬ φ , k ) } E(R) = \{(\varphi_1 \land \varphi, j), (\varphi_1 \land \lnot \varphi, k)\} E(R)={(φ1φ,j),(φ1¬φ,k)}

  • (2).合并后为 E ′ ( x ) = { ( φ 1 , o 1 ) } E^{'}(x) = \{(\varphi_1, o_1)\} E(x)={(φ1,o1)}, S f ′ ( o 1 ) = { ( φ 1 , , L 1 ) } S^{'}_f(o_1) = \{(\varphi_1, , L_1)\} Sf(o1)={(φ1,,L1)}, E ( L 1 ) = { ( φ 1 ∧ φ , j ) , ( φ 1 ∧ ¬ φ , k ) } E(L_1) = \{(\varphi_1 \land \varphi, j), (\varphi_1 \land \lnot \varphi, k)\} E(L1)={(φ1φ,j),(φ1¬φ,k)}

请添加图片描述在上述示例中,qux 的call语句后面还添加了 store 语句 *x = L1,接着 foo(x) 分析,那么更新了 S ( o 1 ) S(o_1) S(o1) 的值为 { ( φ 1 , l ′ , L 1 ) } \{(\varphi_1, l^{'}, L_1)\} {(φ1,l,L1)}。这里 l ′ l^{'} lstore 语句。

在call分析完后,value-flow中会添加summary edge,主要从callee的return value对应的 store(可能是已有的也可能是新添加的)连接到caller后面的 load 处,在上上张图示例(b)中,foo 结尾插入 R = *y 以及 return R,以及在 quxbar 的caller处添加返回值 L1(还有 L2)以及后面跟着 store 语句 *x = L1 (还有 *z = L2),那么会在新添加的callee的 load 和caller的 store 中间添加一个summary edge。(这里感觉首先需要对IR进行语义等价转换,可能方法和Pinpoint一样)。

2.Evaluation

2.1.设置

下游任务包括:(1).Thin Slicing for Program Understanding、(2).Value-flow Bug Finding: 主要是use-after-free。

baseline包括:

  • (a).指针分析的比较对比:(1).SVF (主要是SVF实现的Andersen算法)、(2).SFS(稀疏值流分析)、(3).DSA (unionfication-based, flow-insensitive, context-sensitive算法,实现采用sea-dsa)

  • (b).slicing与SUPA-FSCS进行对比

  • ©.bug finding与CRED、clang-static-analyzer (CSA) 进行对比。

benchmark采用了6个SPEC INT 2010程序以及10个开源程序。

2.2.value-flow分析

value-flow分析主要对比时间开销,结果如下表和下图所示,SVF、SFS、DSA为baseline,Falcon(PI)和Falcon(SAT)为消融实验,分别表示采用path-insensitive和采用全量SAT进行约束求解的开销。可以看到Falcon的时间开销相比其它方法有着巨大优势。

请添加图片描述

请添加图片描述

2.3.Thin Slicing

为了生成真实的slicing query,作者使用typestate分析提得到的bug report。从对应程序位置的问题变量开始backward分析,结果可以帮助开发人员理解这些报告。主要比较每个slicing query的处理时间(排除value-flow graph的时间),precison和recall。后两者有两位作者进行了人工验证。

时间开销:Falcon对每个slicing query的处理时间少于240ms。总的来说,它比SUPA-FSCS快302倍,平均加速54倍。这一性能提升归因于Falcon生成的value-flow graph比SUPA-FSCS更紧凑。

Precision:Falcon生成的slicing平均大小比SVF、SFS、DSA和SUPA-FSCS分别小5.5倍、1.9倍、2.6倍和1.3倍。表示过滤了很多错误的语句。

Recall:Falcon假设函数参数之间不存在别名,这一不sound的假设对超过90%的query没有影响,经过手动检查结果得到了验证。两项先前的研究也表明,真实世界的C/C++程序中的函数参数往往具有很少的别名关系。

2.4.Bug Detection

use-after-free bug检测的结果如下图所示,分别对比了时间开销和误报率。可以看出,Falcon在大多数大规模程序中超越了CRED和CSA,平均速度提升分别达到10.3倍和1620.8倍(以工具完成的项目为基准)。尽管表中未显示,但作者观察到,如果允许10个线程并发分析,Falcon可以在两小时内完成每个程序的检查。CRED、CSA和Falcon的虚假正例率分别为40.0%、33.3%和27.8%。我们注意到,CSA报告的警告明显少于CRED和Falcon,部分原因是频繁超时和在跨编译单元分析路径时能力有限。Falcon符合工业界对30%误报率的常见要求。

请添加图片描述

参考文献

[1].Yao P, Zhou J, Xiao X, et al. Falcon: A Fused Approach to Path-Sensitive Sparse Data Dependence Analysis[J]. Proceedings of the ACM on Programming Languages, 2024, 8(PLDI): 567-592.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值