抽象解释器(3) AAM风格的0-CFA

这篇博客将主要包含下面的内容

  • 什么是CFA
  • Abstracting Abstract Machine(AAM)风格的静态分析
  • 使用Racket实现AAM style 0-CFA

AAM style CFA

在开发的过程中 ,相信大家都遇到过一些问题,比如能否在编译期知道一个函数的调用位置, 能不能给推测运行时的指针指向, 能不能预测一个程序可能的运行时间? 如果上过一些计算理论的课,相信大家都会知道,想要完全精确的知道这些问题的答案是不可能的, 但是没有一种技术能够在某种程度上近似的得到这些问题的答案呢, 其实是有的,CFA (Control Flow Analysis).
顾名思义,CFA是一种控制流分析的技术,他的目标是获取程序运行的控制流图,并在这基础获取一些信息,比如说值的传递过程。控制流图代表了程序会如何运行, 所以一个比较直接的想法就是通过一个模型去模拟运行这个程序, 在模拟运行的过程中实现某种程度的近似,这样就可以得到程序的控制流图。这里我们就可以使用Abstract Machine来完成操作语义的模拟,特别是之前提到的CEK machine的变种timestamped CESK* machine(假设我们讨论的语言是Call-by-value的)。它的操作语义模型如下所示。

time-stamped CESK* machine

CESK* machine 和CEK 的区别在于把 所有的变量和continuation都放到一个新的Store中, 在environment上只存放一个指针指向store中的值. 使用这个抽象解释器的模型的好处是:

  • 在Continuation中,不存在结构化的递归, 每一个continuation 都是使用指针的方式来连到后续的continuation
  • time stamp 可以用来提供函数的调用位置信息
  • 避免使用meta language自己的栈
  • 使用小步语义,方便证明(大步也是可以的可以看Daris的paper但是是真的是Pearl.)

在之前介绍CEK machine的时候, 不难发现,如果我们的一个精确的语义模型,对于
\Omega-combinator之类的term 是没有办法保证停机的, 或者说我们的分析是不能达到一个不动点(fixpoint)。那么应该如何进行近似才能保证我们能够得到的正确可靠(sound)的分析算法呢? David Van Horn 和 Matt Might 提出了一种做法, 抽象抽象机(Abstracting Abstract Machine) ,(简称AAM) 在这篇Paper中 ,他们发现,如果使用一些特定的抽象解释器,比如CESK* Machine并对Strore Allocate函数和Timestamp Tick生成函数进行进行抽象,根据CESK* Machine的特点我们可以的证明

  1. Alloc和Tick通过Galios Connection抽象得到的抽象值域(Abstract Domain)是有限的,那么 之后得到的新的lattice总是有限高度的也就是说我们的分析总能到达一个fixpoint。也就是说 Abstract CESK* machine是decidable的。
  2. Abstract CESK* machine 是sound的, 它总是能正确的反映Concrete Machine的运行,只是精度上的丢失。


    Abstract CESK* Machine

具体的证明过程在这里就不详细说了。 AAM风格最显而易见的好处就是,它把CFA简化成了只需要实现 \widehat{alloc}\widehat{tick}, 并且能保证有一个有限的Store,也许我们选择的抽象函数不够好,但是我们得到的分析结果总是sound的。

0-CFA

CFA是无法做到完全精确的, 所以根据精度的不同,CFA家族有各种变种,比如k-CFA, m-CFA , pushdown-CFA等等。 其中最常见的是k-CFA种k=0的特例,也就是0-CFA。 0-CFA是CFA家族中最简单的一种CFA,所有的值会被抽象成生成的他们的syntax form.不难发现一种精度丢失比较大的CFA , 0-CFA近似的认为所有长的一样的call都是一样的,而所有和它调用位置相关的信息都会被无视,比如下面的例子

(let* ([id (lambda (x) x)]
       [foo (id 5)])
  (id 3))

这里id函数被调用了两次,0-CFA就会近似的认为任意的id调用,都会返回到任意的id调用位置。在这里id函数并不知道自己会返回到具体哪一个调用位置,因为0-CFA根本就没有记录调用历史。


0-CFA精度丢失

如果我们采用AAM风格来实现0-CFA,那么首先,因为我们不需要调用位置信息,所以我们不需要time-stamp函数\widehat{tick}, 我们只需要实现\widehat{alloc}就可以了。 又因为0-CFA会把所有值都抽象成生成它的syntax form,所以我们可以直接把syntax form当成变量的地址。就这样非常简单的我们就得到了一个0-CFA!

AAM 风格0-CFA的racket实现

在David Van Horn的原文中使用的是Direct-style Lambda Calculus 来描述AAM的。Direct style在实际使用的过程中其实并不是最合适 。一般来说,编译器会把语言先编译成IR再做CFA,常见的FP中的IR 有CPS 和ANF, 再Olin Shiver最本来的k-CFA的表述中就使用的是CPS, 但是这其实是对CFA很不利的,它会导致你的结果完全没有可读性,ANF作为函数式语言的SSA是一种比较好的选择。在后面我应该会单独写一些博客来讲这个问题。。。。
所以在我的实现中将会采用针对ANF的\widehat{CESK*} mahcine 来实现算法。 同时,在大多数CFA 的论文中间,都只是对最简单的\lambda-calculus 进行了讨论, 然而实际上加入不同的语法特性可能并不总是一件那么trival的事情。我的实现加入了更加多Core Scheme中的语法,比如数字,布尔值,call/cc, 递归, 多参数函数。

syntax

首先需要environment和store能够支持一个地址对应多个虚拟值,因为根据之前说的0-CFA的函数return我们很容易看到,其实这个Machine是non-deterministic的,同一个变量是很可能对应多个值的。

;; ρ ∈ Env, Environments
(define (env? ρ)
  (and (andmap symbol? (hash-keys ρ))
       (andmap addr? (hash-values ρ))))

;; σ ∈ Sto, Store
(define (sto? σ)
  (and (andmap addr? (hash-keys σ))
       (andmap (lambda (vs)
                 (andmap (or/c kont? value?) (set->list vs)))
               (hash-values σ))))

step关系和之前的CEK machine会有巨大的不同, 因为现在一个step可能会导致多个不同的状态。还有一点可以注意到的是,因为我们采用的是ANF,所以在这里面唯一的返回来自于let, 这其实大大减少了状态数量,在ANF中可以极大的简化多参数求值的问题,多参数可以在一步中被原子的完成。在实现其他的语法特性的过程中其实也有一些区别,不赘述了

;; non-deterministic Step relation: Σ → {Σ}
(define (step ς)
  (displayln "hasdf")
  (pretty-print ς)
  (match ς
    ;; Handle a let: step into `e` while remembering (via the `let`
    ;; continuation) to go back and bind x within ρ before proceeding
    ;; to body and continuation k.
    [`((let ([,x ,e]) ,body) ,ρ ,σ ,κₐ)
     (let ([α (alloc₀ ς)])
       (set `(,e ,ρ ,(store-update σ α `(let ,x ,ρ ,body ,κₐ)) ,α)))]
    ;; letrec, assume e is a curified recursive function  do code transfer to
    ;; Y combinator, and Y combinator here should also in ANF style.
    ;; (U (λ (y) (λ (f)
    ;;              (f (λ (x) (((y y) f) x)))))
    [`((letrec ([,x ,e]) ,body) ,ρ ,σ ,κₐ)
     (set `((let ([,x (((lambda (U-x) (U-x U-x))
                        (lambda (Y-y) (lambda (Y-f)
                                        (Y-f (lambda (Y-x)
                                               (let ([Y-yy (Y-y Y-y)])
                                                 (let ([Y-yyf (Y-yy f)])
                                                   (Y-yyf Y-x))))))))
                       ,e)]) ,body) ,ρ ,σ ,κₐ))]
    ;; Handle `if` statement, first step into cond-expr if cond-expr is
    ;; boolean just return the relate side of branch, if number or lambda
    ;; throw type err, if var, eval it. Since the code has been ANF-converted
    ;; there is no complex expr in cond position
    [`((if ,(? aexpr? econd) ,(? expr? tbranch) ,(? expr? fbranch)) ,ρ ,σ ,κₐ)
     (set-map (aeval econd ρ σ)
              (λ (econdᵥ)
                (match econdᵥ
                  [#t `(,tbranch ,ρ ,σ ,κₐ)]
                  [#f `(,fbranch ,ρ ,σ ,κₐ)])))]
    ;; TODO: support multi arg continuation
    ;; call/cc, step into body and make continuation bind to x
    ;; apply a countinutaion, and it only take one argument
    ;; it will give up current and jump back to call/cc point
    ;; [`((,(? kont? k-ap) ,ae) ,ρ ,k)
    ;;  `(,ae ,ρ ,k-ap)]
    [`((call/cc (lambda (,x) ,body)) ,ρ ,σ ,κₐ)
     (set (let ([α (alloc₀ ς)])
            `(,body ,(hash-set ρ x κₐ) ,σ ,κₐ)))]
    ;; Returns. You should specify how to handle when an atomic
    ;; expression is in return position. Because this is an A-Normal
    ;; Form language, the only return position will be the `let`
    ;; continuation.
    [`(,(? aexpr? ae) ,ρ ,σ ,κₐ )
     (let ([vs (hash-ref σ κₐ)]
           ;; step into a state by a value
           [step₁ (λ (v)
                    (match v
                      [`(let ,x ,ρ-prime ,e ,κₙ)
                       (let ([α (alloc₀ ς)])
                         `(,e ,(hash-set ρ-prime x α)
                              ,(store-∪ σ α (aeval ae ρ σ)) ,κₙ))]))])
       ;; diverge here
       (set-map vs step₁)
       )]
    ;; Application. Each is an atomic expression. Assume that ae0 will
    ;; evaluate to a closure. This means that `(aeval ae0 ρ σ) will
    ;; evaluate to something that matches something like `(clo (lambda
    ;; (,xs ...) ,e-body) ,ρ-prime ,σ).
    ;; if we intro call/cc, (aeval ae0 ρ σ) can also be a continuation
    [`((,ae0 ,aes ...) ,ρ ,σ ,κₐ)
     (set-map (aeval ae0 ρ σ)
              (λ (ae0ᵥ)
                (match ae0ᵥ
                  [(? kont? k-ap)
                   ;; (displayln ae0ᵥ)
                   ;; continuation can only apply to one element
                   `(,(first aes) ,ρ ,σ ,(hash-ref ρ ae0))]
                  [`(clo (lambda (,xs ...) ,e-body) ,ρ-prime)
                   ;; eval in one time
                   (let ([env&stos
                          (foldl (lambda (x ae e&s)
                                   (match e&s
                                     [`(,env ,sto)
                                      (let ([α (alloc₀ ς)])
                                        `(,(hash-set env x α)
                                          ,(store-∪ sto α (aeval ae ρ σ)))
                                        )]))
                                 `(,ρ-prime ,σ) xs aes)])
                     (match env&stos
                       [`(,new-ρ ,new-σ) `(,e-body ,new-ρ ,new-σ ,κₐ)]))]
                  [else (error "LHS value is not a closure can't apply!")]
                  )))]
    ))

算法的核心部分 alloc函数,其实这个反而是非常非常简单的,在0-CFA中

;; allocator function this is key of precision in AAM-style CFA
;; allocₖ(ς) : State → Addr
;; bellow is the allocator funtion for 0-CFA
;; in 0-CFA addrs is the syntax form of control string
(define (addr? κₐ) (expr? κₐ))
(define (alloc₀ ς)
  (match ς
    [`(,e ,ρ ,σ ,κₐ) e]))

CFA算法还有一个和普通的抽象解释器不一样的地方, CFA 是会到达Fixpoint的也就是说,任凭抽象解释器再怎么步进,我们的状态已经不会再增加了, 而状态的增加是单调的, 所以我们如果发现控制流图不再增加,那么就应当结束算法。

;; Is this state at a done configuration?
;; In CFA, if state in state graph doesn't increase
;; which means we have reach fixpoint, we are done
(define (done? state graph prev-graph)
  (if (equal? graph prev-graph)
      #t
      (match state
        [`(,(? aexpr?) ,ρ ,σ •) #t]
        [else #f])))

接下来只要不停的step直到fixpoint就可以啦!这样就完成了0-CFA算法的实现, 相对还是比较简单的。


加上了生成DOT图的实现完整代码

还要做什么

  • 我的实现其实并不完整, 我还需要bypass一些系统库函数进来, 不然加入number和boolean毫无意义。
  • 0-CFA比较简单,我们可以增加k-CFA的k值来提升精度,也就是增加调用历史信息。
  • 实际上大部分的CFA算法的时间复杂度和空间复杂度还是太大了, 我们需要使用store widening 和 Abstract Gabage collection来提升算法速度。
  • 现有的AAM实现都是针对函数式语言的, 我正在做一个针对LLVM IR的。希望能用来实现一些逆向的工具。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容

  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,655评论 0 11
  • 1.import static是Java 5增加的功能,就是将Import类中的静态方法,可以作为本类的静态方法来...
    XLsn0w阅读 1,228评论 0 2
  • 面向对象主要针对面向过程。 面向过程的基本单元是函数。 什么是对象:EVERYTHING IS OBJECT(万物...
    sinpi阅读 1,057评论 0 4
  • 该论文来自Berkeley实验室,英文标题为:Resilient Distributed Datasets: A ...
    九七学姐阅读 1,785评论 0 3
  • 你知不知我有多难过 心心念念的梦和人 最终消散 阳光 我站在那下面 树木的影子同我一起 看你离开的背影 那些 花啊...
    漠北青云阅读 137评论 0 1