模糊测试之书-五-

模糊测试之书(五)

原文:exploringjs.com/ts/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

高效的语法模糊测试

原文:www.fuzzingbook.org/html/GrammarFuzzer.html

在关于语法的章节中,我们看到了如何使用语法进行非常有效和高效的测试。在本章中,我们将之前的基于字符串算法改进为基于树的算法,这要快得多,并且可以更精确地控制模糊输入的生成。

本章中介绍的算法是其他几种技术的基石;因此,本章在书中起到了“枢纽”的作用。

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('Ohl8TLcLl3A') 

先决条件

  • 您应该了解基于语法的模糊测试是如何工作的,例如,可以参考关于语法的章节。

概述

要使用本章提供的代码(导入代码),请编写

>>> from fuzzingbook.GrammarFuzzer import <identifier> 

然后利用以下功能。

高效的语法模糊测试

本章介绍了GrammarFuzzer,这是一个高效的语法模糊测试工具,它接受一个语法来生成符合语法的输入字符串。以下是一个典型的用法示例:

>>> from Grammars import US_PHONE_GRAMMAR
>>> phone_fuzzer = GrammarFuzzer(US_PHONE_GRAMMAR)
>>> phone_fuzzer.fuzz()
'(771)306-0659' 

GrammarFuzzer构造函数接受多个关键字参数来控制其行为。例如,start_symbol允许设置开始扩展的符号(而不是<start>):

>>> area_fuzzer = GrammarFuzzer(US_PHONE_GRAMMAR, start_symbol='<area>')
>>> area_fuzzer.fuzz()
'409' 

这是如何参数化GrammarFuzzer构造函数的示例:

Produce strings from `grammar`, starting with `start_symbol`.
If `min_nonterminals` or `max_nonterminals` is given, use them as limits 
for the number of nonterminals produced.  
If `disp` is set, display the intermediate derivation trees.
If `log` is set, show intermediate steps as text on standard output. 

GrammarFuzzer <a xlink:href="#" xlink:title="class GrammarFuzzer:

使用推导树高效地从语法中生成字符串。">GrammarFuzzer <a xlink:href="#" xlink:title="init(self, grammar: Dict[str, List[Expansion]], start_symbol: str = '', min_nonterminals: int = 0, max_nonterminals: int = 10, disp: bool = False, log: Union[bool, int] = False) -> None:

grammar生成字符串,以start_symbol开始。

如果提供了min_nonterminalsmax_nonterminals,则使用它们作为限制

对于生成的非终结符数量。

如果设置disp,则显示中间推导树。

如果设置log,则将中间步骤以文本形式显示在标准输出上。">init() <a xlink:href="#" xlink:title="fuzz(self) -> str:

从语法生成一个字符串。">fuzz() <a xlink:href="#" xlink:title="fuzz_tree(self) -> DerivationTree:

从语法中生成推导树。">fuzz_tree() any_possible_expansions() <a xlink:href="#" xlink:title="check_grammar(self) -> None:

检查传入的语法。">check_grammar() <a xlink:href="#" xlink:title="choose_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

返回在children_alternatives中要选择的扩展的索引。

children_alternativesnode的可能子节点列表。

默认为随机。在子类中可被覆盖。">choose_node_expansion() <a xlink:href="#" xlink:title="choose_tree_expansion(self, tree: DerivationTree, children: List[DerivationTree]) -> int:

返回要选择的子树在children中的索引。">choose_tree_expansion()

默认为随机。">choose_tree_expansion() expand_node() expand_node_by_cost() expand_node_max_cost() expand_node_min_cost() <a xlink:href="#" xlink:title="expand_node_randomly(self, node: DerivationTree) -> DerivationTree:

node选择一个随机扩展并返回它">expand_node_randomly() <a xlink:href="#" xlink:title="expand_tree(self, tree: DerivationTree) -> DerivationTree:

使用三阶段策略扩展tree,直到所有扩展完成。">expand_tree() <a xlink:href="#" xlink:title="expand_tree_once(self, tree: DerivationTree) -> DerivationTree:

在树中选择一个未扩展的符号并扩展它

可在子类中重载。">expand_tree_once() <a xlink:href="#" xlink:title="expand_tree_with_strategy(self, tree: DerivationTree, expand_node_method: Callable, limit: Optional[int] = None):

使用expand_node_method作为节点扩展函数来扩展树

直到可能的扩展数量达到limit。">expand_tree_with_strategy() expansion_cost() expansion_to_children() init_tree() <a xlink:href="#" xlink:title="log_tree(self, tree: DerivationTree) -> None:

如果 self.log 被设置,则输出一个树形结构;如果 self.display 也被设置,则显示树结构">log_tree() possible_expansions() <a xlink:href="#" xlink:title="process_chosen_children(self, chosen_children: List[DerivationTree], expansion: Expansion) -> List[DerivationTree]:

在选择后处理子节点。默认情况下,不执行任何操作。《process_chosen_children()` <a xlink:href="#" xlink:title="supported_opts(self) -> Set[str]:

支持的选项集。应在子类中重载。《supported_opts()` symbol_cost() Fuzzer <a xlink:href="Fuzzer.html" xlink:title="class Fuzzer:

模糊测试器的基类。《Fuzzer` <a xlink:href="Fuzzer.html" xlink:title="init(self) -> None:

构造函数 <a xlink:href="Fuzzer.html" xlink:title="fuzz(self) -> str:

返回模糊输入 <a xlink:href="Fuzzer.html" xlink:title="run(self, runner: Fuzzer.Runner = <Fuzzer.Runner object>) -> Tuple[subprocess.CompletedProcess, str]:

使用模糊输入运行 runnerrun() 方法 <a xlink:href="Fuzzer.html" xlink:title="runs(self, runner: Fuzzer.Runner = <Fuzzer.PrintRunner object>, trials: int = 10) -> List[Tuple[subprocess.CompletedProcess, str]]:

使用模糊输入运行runnertrials次">runs() GrammarFuzzer->Fuzzer 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

推导树

在内部,GrammarFuzzer利用推导树逐步展开。在生成字符串后,生成的树可以通过derivation_tree属性访问。

>>> display_tree(phone_fuzzer.derivation_tree) 

0 1 0->1 2 ( (40) 1->2 3 1->3 10 ) (41) 1->10 11 1->11 18 - (45) 1->18 19 1->19 4 3->4 6 3->6 8 3->8 5 7 (55) 4->5 7 7 (55) 6->7 9 1 (49) 8->9 12 11->12 14 11->14 16 11->16 13 3 (51) 12->13 15 0 (48) 14->15 17 6 (54) 16->17 20 19->20 22 19->22 24 19->24 26 19->26 21 0 (48) 20->21 23 6 (54) 22->23 25 5 (53) 24->25 27 9 (57) 26->27

在推导树的内部表示中,一个节点是一个对(symbol, children)。对于非终结符,symbol是被展开的符号,而children是进一步节点的列表。对于终结符,symbol是终结符字符串,而children为空。

>>> phone_fuzzer.derivation_tree
('<start>',
 [('<phone-number>',
   [('(', []),
    ('<area>',
     [('<lead-digit>', [('7', [])]),
      ('<digit>', [('7', [])]),
      ('<digit>', [('1', [])])]),
    (')', []),
    ('<exchange>',
     [('<lead-digit>', [('3', [])]),
      ('<digit>', [('0', [])]),
      ('<digit>', [('6', [])])]),
    ('-', []),
    ('<line>',
     [('<digit>', [('0', [])]),
      ('<digit>', [('6', [])]),
      ('<digit>', [('5', [])]),
      ('<digit>', [('9', [])])])])]) 

本章包含各种辅助工具,用于处理推导树,包括可视化工具——特别是上面的display_tree()函数。

一个不充分的算法

在上一章中,我们介绍了simple_grammar_fuzzer()函数,该函数接受一个语法并自动从中生成一个语法上有效的字符串。然而,simple_grammar_fuzzer()正如其名所暗示的那样——简单。为了说明问题,让我们回到在语法章节中从EXPR_GRAMMAR_BNF创建的expr_grammar

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import quiz 
from [typing](https://docs.python.org/3/library/typing.html) import Tuple, List, Optional, Any, Union, Set, Callable, Dict 
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import unicode_escape 
from Grammars import EXPR_EBNF_GRAMMAR, convert_ebnf_grammar, Grammar, Expansion
from Grammars import simple_grammar_fuzzer, is_valid_grammar, exp_string 
expr_grammar = convert_ebnf_grammar(EXPR_EBNF_GRAMMAR)
expr_grammar 
{'<start>': ['<expr>'],
 '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'],
 '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'],
 '<factor>': ['<sign-1><factor>', '(<expr>)', '<integer><symbol-1>'],
 '<sign>': ['+', '-'],
 '<integer>': ['<digit-1>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<symbol>': ['.<integer>'],
 '<sign-1>': ['', '<sign>'],
 '<symbol-1>': ['', '<symbol>'],
 '<digit-1>': ['<digit>', '<digit><digit-1>']}

expr_grammar有一个有趣的特性。如果我们将其输入到simple_grammar_fuzzer()中,该函数会陷入停滞:

from ExpectError import ExpectTimeout 
with ExpectTimeout(1):
    simple_grammar_fuzzer(grammar=expr_grammar, max_nonterminals=3) 
Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_9136/3259437052.py", line 2, in <module>
    simple_grammar_fuzzer(grammar=expr_grammar, max_nonterminals=3)
  File "Grammars.ipynb", line 87, in simple_grammar_fuzzer
    symbol_to_expand = random.choice(nonterminals(term))
                                     ^^^^^^^^^^^^^^^^^^
  File "Grammars.ipynb", line 61, in nonterminals
    return RE_NONTERMINAL.findall(expansion)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "Timeout.ipynb", line 43, in timeout_handler
    raise TimeoutError()
TimeoutError (expected)

为什么会这样?看看语法;记住你关于simple_grammar_fuzzer()的知识;并以log=true参数运行simple_grammar_fuzzer()以查看展开情况。

问答

为什么simple_grammar_fuzzer()会挂起?

确实!问题就在这个规则中:

expr_grammar['<factor>'] 
['<sign-1><factor>', '(<expr>)', '<integer><symbol-1>']

在这里,除了 (expr) 以外的任何选择都会增加符号的数量,即使只是临时的。由于我们对要扩展的符号数量设置了硬限制,因此扩展 <factor> 的唯一选择是 (<expr>),这会导致 无限添加括号

无限扩展的问题只是 simple_grammar_fuzzer() 存在的问题之一。更多问题包括:

  1. 这是低效的。随着每次迭代,这个模糊器会搜索到目前为止产生的字符串以寻找要扩展的符号。随着生成字符串的增长,这变得低效。

  2. 这是难以控制的。即使在限制符号数量的同时,仍然可能获得非常长的字符串——甚至无限长的字符串,如上所述。

让我们通过绘制不同长度字符串所需的时间来阐述这两个问题。

from Grammars import simple_grammar_fuzzer 
from Grammars import START_SYMBOL, EXPR_GRAMMAR, URL_GRAMMAR, CGI_GRAMMAR 
from Grammars import RE_NONTERMINAL, nonterminals, is_nonterminal 
from Timer import Timer 
trials = 50
xs = []
ys = []
for i in range(trials):
    with Timer() as t:
        s = simple_grammar_fuzzer(EXPR_GRAMMAR, max_nonterminals=15)
    xs.append(len(s))
    ys.append(t.elapsed_time())
    print(i, end=" ")
print() 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 

average_time = sum(ys) / trials
print("Average time:", average_time) 
Average time: 0.10001757324207575

%matplotlib inline

import [matplotlib.pyplot](https://matplotlib.org/) as plt
plt.scatter(xs, ys)
plt.title('Time required for generating an output'); 

我们看到,(1) 生成输出所需的时间与输出长度成二次方增长,以及 (2) 产生的输出中有很大一部分长度达到数万个字符。

为了解决这些问题,我们需要一个 更智能的算法——一个更高效、更好地控制扩展并能够在 expr_grammar 中预见 (expr) 选项会导致潜在无限扩展,而与其他两个选项不同的算法。

推导树

为了获得更高效的算法并更好地控制扩展,我们将使用语法产生的字符串的特殊表示。一般想法是使用一个将随后扩展的 结构——所谓的 推导树。这种表示允许我们始终跟踪我们的扩展状态——回答诸如哪些元素被扩展成了哪些其他元素,以及哪些符号还需要扩展等问题。此外,向树中添加新元素比一次又一次替换字符串要高效得多。

与编程中使用的其他树一样,推导树(也称为 解析树具体语法树)由 节点 组成,这些节点有其他节点(称为 子节点)作为它们的 子节点。树从没有父节点的单个节点开始;这被称为 根节点;没有子节点的节点称为 叶节点

推导树语法扩展过程如下所示,使用的是来自语法章节的算术语法 [Grammars.html]。我们从一个节点作为树的根节点开始,代表 起始符号——在我们的情况下是 <start>

root <start>

为了扩展树,我们需要遍历它,寻找没有子节点的非终结符号 \(S\)。因此,\(S\) 是一个仍然需要扩展的符号。然后,我们从语法中选择一个扩展来应用于 \(S\)。接着,我们将扩展作为 \(S\) 的新子节点添加。对于我们的起始符号 <start>,唯一的扩展是 <expr>,所以我们将其添加为子节点。

root <start> <expr> <start>-><expr>

从推导树构建生成的字符串,我们按顺序遍历树并收集树叶处的符号。在上面的例子中,我们得到字符串 "<expr>"

为了进一步扩展树,我们选择另一个符号进行扩展,并将其扩展作为新的子节点添加。这将得到 <expr> 符号,它被扩展为 <expr> + <term>,添加了三个子节点。

root <start> <expr> <start>-><expr> <expr> <expr>-><expr> + + <expr>->+ <term> <expr>-><term>

我们重复扩展,直到没有符号可以扩展:

root <start> <expr> <start>-><expr> <expr> <expr>-><expr> + + <expr>->+ <term> <expr>-><term> <term> <expr> -><term> <factor> <term>-><factor> <factor> <term> -><factor> <integer> <factor> -><integer> <digit> <integer> -><digit> 2 2 <digit> ->2 <integer> <factor>-><integer> <digit> <integer>-><digit> 2 2 <digit>->2

现在我们有了字符串 2 + 2 的表示。然而,与字符串本身相比,推导树记录了生成的字符串的 整个结构(以及生产历史,或 推导 历史)。它还允许进行简单的比较和操作——比如,用一个子树(子结构)替换另一个子树。

表示推导树

在 Python 中表示推导树时,我们使用以下格式。一个节点是一个对

(SYMBOL_NAME, CHILDREN) 

其中 SYMBOL_NAME 是表示节点的字符串(即 "<start>""+"),而 CHILDREN 是子节点列表。

CHILDREN 可以取一些特殊值:

  1. None 作为未来扩展的占位符。这意味着该节点是一个应该进一步扩展的 非终端符号

  2. [](即空列表)表示没有子节点。这意味着该节点是一个不能再扩展的 终端符号

类型 DerivationTree 捕获了这种结构。(Any 实际上应该读作 DerivationTree,但 Python 静态类型检查器无法很好地处理递归类型。)

DerivationTree = Tuple[str, Optional[List[Any]]] 

让我们以一个非常简单的推导树为例,表示上面提到的 <expr> + <term> 的中间步骤。

derivation_tree: DerivationTree = ("<start>",
                   [("<expr>",
                     [("<expr>", None),
                      (" + ", []),
                         ("<term>", None)]
                     )]) 

为了更好地理解这个树的结构,让我们引入一个函数 display_tree(),它可视化这个树。

实现 `display_tree()`

我们使用 graphviz 包中的 dot 绘图程序算法性地遍历上述结构。(除非你对树可视化有浓厚的兴趣,否则你可以直接跳到下面的示例。)

from [graphviz](https://graphviz.readthedocs.io/) import Digraph 
from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import display 
import [re](https://docs.python.org/3/library/re.html)
import [string](https://docs.python.org/3/library/string.html) 
def dot_escape(s: str, show_ascii=None) -> str:
  """Return s in a form suitable for dot.
 If `show_ascii` is True or length of `s` is 1, also append ascii value."""
    escaped_s = ''
    if show_ascii is None:
        show_ascii = (len(s) == 1)  # Default: Single chars only

    if show_ascii and s == '\n':
        return '\\\\n (10)'

    s = s.replace('\n', '\\n')
    for c in s:
        if re.match('[,<>\\\\"]', c):
            escaped_s += '\\' + c
        elif c in string.printable and 31 < ord(c) < 127:
            escaped_s += c
        else:
            escaped_s += '\\\\x' + format(ord(c), '02x')

        if show_ascii:
            escaped_s += f' ({ord(c)})'

    return escaped_s 
assert dot_escape("hello") == "hello" 
assert dot_escape("<hello>, world") == "\\<hello\\>\\, world" 
assert dot_escape("\\n") == "\\\\n" 
assert dot_escape("\n", show_ascii=False) == "\\\\n" 
assert dot_escape("\n", show_ascii=True) == "\\\\n (10)" 
assert dot_escape("\n", show_ascii=True) == "\\\\n (10)" 
assert dot_escape('\x01', show_ascii=False) == "\\\\x01" 
assert dot_escape('\x01') == "\\\\x01 (1)" 

虽然我们现在对可视化 derivation_tree 感兴趣,但我们希望泛化可视化过程。特别是,如果我们的方法 display_tree() 可以显示任何类似的数据结构,那将是有帮助的。为此,我们定义了一个辅助方法 extract_node(),它从给定的数据结构中提取当前符号和子节点。默认实现简单地从任何 derivation_tree 节点中提取符号、子节点和注释。

def extract_node(node, id):
    symbol, children, *annotation = node
    return symbol, children, ''.join(str(a) for a in annotation) 

在可视化树时,有时显示某些节点的方式不同是有用的。例如,有时区分未处理的节点和处理过的节点是有用的。我们定义了一个辅助过程 default_node_attr(),它提供基本的显示,可以被用户自定义。

def default_node_attr(dot, nid, symbol, ann):
    dot.node(repr(nid), dot_escape(symbol)) 

与节点类似,边也可能需要修改。我们定义 default_edge_attr() 作为用户可以定制的辅助过程。

def default_edge_attr(dot, start_node, stop_node):
    dot.edge(repr(start_node), repr(stop_node)) 

在可视化树时,有时可能希望改变树的外观。例如,如果树是从左到右而不是从上到下排列的,有时更容易查看。为此,我们定义了另一个辅助过程 default_graph_attr()

def default_graph_attr(dot):
    dot.attr('node', shape='plain') 

最后,我们定义了一个方法 display_tree(),它接受这四个函数 extract_node()default_edge_attr()default_node_attr()default_graph_attr(),并使用它们来显示树。

def display_tree(derivation_tree: DerivationTree,
                 log: bool = False,
                 extract_node: Callable = extract_node,
                 node_attr: Callable = default_node_attr,
                 edge_attr: Callable = default_edge_attr,
                 graph_attr: Callable = default_graph_attr) -> Any:

    # If we import display_tree, we also have to import its functions
    from [graphviz](https://graphviz.readthedocs.io/) import Digraph

    counter = 0

    def traverse_tree(dot, tree, id=0):
        (symbol, children, annotation) = extract_node(tree, id)
        node_attr(dot, id, symbol, annotation)

        if children:
            for child in children:
                nonlocal counter
                counter += 1
                child_id = counter
                edge_attr(dot, id, child_id)
                traverse_tree(dot, child, child_id)

    dot = Digraph(comment="Derivation Tree")
    graph_attr(dot)
    traverse_tree(dot, derivation_tree)
    if log:
        print(dot)
    return dot 
```</details>

这就是我们的树所可视化的内容:

```py
display_tree(derivation_tree) 

0 1 0->1 2 1->2 3 + 1->3 4 1->4

习题

那么这些中的哪一个是 derivation_tree 的内部表示?

您可以亲自检查:

derivation_tree 
('<start>', [('<expr>', [('<expr>', None), (' + ', []), ('<term>', None)])])

在本书中,我们偶尔也会使用一个函数 display_annotated_tree(),它允许向单个节点添加注释。

为 `display_annotated_tree()` 的源代码和示例

display_annotated_tree() 显示带注释的树结构,并按从左到右的顺序排列图。

def display_annotated_tree(tree: DerivationTree,
                           a_nodes: Dict[int, str],
                           a_edges: Dict[Tuple[int, int], str],
                           log: bool = False):
    def graph_attr(dot):
        dot.attr('node', shape='plain')
        dot.graph_attr['rankdir'] = 'LR'

    def annotate_node(dot, nid, symbol, ann):
        if nid in a_nodes:
            dot.node(repr(nid), 
                     "%s (%s)" % (dot_escape(unicode_escape(symbol)),
                                  a_nodes[nid]))
        else:
            dot.node(repr(nid), dot_escape(unicode_escape(symbol)))

    def annotate_edge(dot, start_node, stop_node):
        if (start_node, stop_node) in a_edges:
            dot.edge(repr(start_node), repr(stop_node),
                     a_edges[(start_node, stop_node)])
        else:
            dot.edge(repr(start_node), repr(stop_node))

    return display_tree(tree, log=log,
                        node_attr=annotate_node,
                        edge_attr=annotate_edge,
                        graph_attr=graph_attr) 
display_annotated_tree(derivation_tree, {3: 'plus'}, {(1, 3): 'op'}, log=False) 

0 1 0->1 2 1->2 3 +  (plus) 1->3 op 4 1->4

如果我们想将树中的所有叶节点作为字符串查看,下面的 all_terminals() 函数就派上用场了:

def all_terminals(tree: DerivationTree) -> str:
    (symbol, children) = tree
    if children is None:
        # This is a nonterminal symbol not expanded yet
        return symbol

    if len(children) == 0:
        # This is a terminal symbol
        return symbol

    # This is an expanded symbol:
    # Concatenate all terminal symbols from all children
    return ''.join([all_terminals(c) for c in children]) 
all_terminals(derivation_tree) 
'<expr> + <term>'

另一个替代的 tree_to_string() 函数也将树转换为字符串;然而,它将非终结符号替换为空字符串。

def tree_to_string(tree: DerivationTree) -> str:
    symbol, children, *_ = tree
    if children:
        return ''.join(tree_to_string(c) for c in children)
    else:
        return '' if is_nonterminal(symbol) else symbol 
tree_to_string(derivation_tree) 
' + '

展开节点

现在我们开发一个算法,该算法接受一个包含未扩展符号的树(例如,上面的 derivation_tree),并依次扩展这些符号。与早期的模糊器一样,我们创建一个特殊的 Fuzzer 子类——在这种情况下,GrammarFuzzerGrammarFuzzer 获取一个语法和一个起始符号;其他参数将稍后用于进一步控制创建并支持调试。

from Fuzzer import Fuzzer 
class GrammarFuzzer(Fuzzer):
  """Produce strings from grammars efficiently, using derivation trees."""

    def __init__(self,
                 grammar: Grammar,
                 start_symbol: str = START_SYMBOL,
                 min_nonterminals: int = 0,
                 max_nonterminals: int = 10,
                 disp: bool = False,
                 log: Union[bool, int] = False) -> None:
  """Produce strings from `grammar`, starting with `start_symbol`.
 If `min_nonterminals` or `max_nonterminals` is given, use them as limits 
 for the number of nonterminals produced. 
 If `disp` is set, display the intermediate derivation trees.
 If `log` is set, show intermediate steps as text on standard output."""

        self.grammar = grammar
        self.start_symbol = start_symbol
        self.min_nonterminals = min_nonterminals
        self.max_nonterminals = max_nonterminals
        self.disp = disp
        self.log = log
        self.check_grammar()  # Invokes is_valid_grammar() 

要向 GrammarFuzzer 添加更多方法,我们使用之前已经介绍过的针对 MutationFuzzer 类 的黑客技巧。该结构

class GrammarFuzzer(GrammarFuzzer):
    def new_method(self, args):
        pass 

允许我们向 GrammarFuzzer 类添加一个新方法 new_method()。(实际上,我们得到一个新的 GrammarFuzzer 类,它扩展了旧的一个,但对我们所有的目的来说,这并不重要。)

`check_grammar()` 实现

我们可以使用上面的技巧来定义辅助方法 check_grammar(),该方法检查给定的语法是否一致:

class GrammarFuzzer(GrammarFuzzer):
    def check_grammar(self) -> None:
  """Check the grammar passed"""
        assert self.start_symbol in self.grammar
        assert is_valid_grammar(
            self.grammar,
            start_symbol=self.start_symbol,
            supported_opts=self.supported_opts())

    def supported_opts(self) -> Set[str]:
  """Set of supported options. To be overloaded in subclasses."""
        return set()  # We don't support specific options 
```</details>

现在我们定义一个辅助方法 `init_tree()`,它构建一个只包含起始符号的树:

```py
class GrammarFuzzer(GrammarFuzzer):
    def init_tree(self) -> DerivationTree:
        return (self.start_symbol, None) 
f = GrammarFuzzer(EXPR_GRAMMAR)
display_tree(f.init_tree()) 

0

这是我们想要扩展的树。

选择一个要扩展的子节点

其中一个中心方法在 GrammarFuzzer 中是 choose_node_expansion()。此方法获取一个节点(例如,<start> 节点)和要扩展的可能子节点列表(每个可能的语法扩展对应一个),从中选择一个,并返回其在可能子节点列表中的索引。

通过重载此方法(特别是在后面的章节中),我们可以实现不同的策略——目前,它只是随机选择给定列表中的一个子节点(这些子节点本身也是推导树的列表)。

class GrammarFuzzer(GrammarFuzzer):
    def choose_node_expansion(self, node: DerivationTree,
                              children_alternatives: List[List[DerivationTree]]) -> int:
  """Return index of expansion in `children_alternatives` to be selected.
 'children_alternatives`: a list of possible children for `node`.
 Defaults to random. To be overloaded in subclasses."""
        return random.randrange(0, len(children_alternatives)) 

获取可能的扩展列表

要实际获取可能的子节点列表,我们需要一个辅助函数 expansion_to_children(),它接受一个扩展字符串并将其分解成一个推导树列表——每个符号(终结符或非终结符)对应一个字符串。

实现 `expansion_to_children()`

函数 expansion_to_children() 使用 re.split() 方法将扩展字符串分割成一个子节点列表:

def expansion_to_children(expansion: Expansion) -> List[DerivationTree]:
    # print("Converting " + repr(expansion))
    # strings contains all substrings -- both terminals and nonterminals such
    # that ''.join(strings) == expansion

    expansion = exp_string(expansion)
    assert isinstance(expansion, str)

    if expansion == "":  # Special case: epsilon expansion
        return [("", [])]

    strings = re.split(RE_NONTERMINAL, expansion)
    return [(s, None) if is_nonterminal(s) else (s, [])
            for s in strings if len(s) > 0] 
```</details>

```py
expansion_to_children("<term> + <expr>") 
[('<term>', None), (' + ', []), ('<expr>', None)]

对于 epsilon 扩展 的情况,即扩展到空字符串,如 <symbol> ::=,需要特殊处理:

expansion_to_children("") 
[('', [])]

就像 语法章节 中的 nonterminals() 一样,我们为未来的扩展提供了支持,允许扩展是一个包含额外数据的元组(这些数据将被忽略)。

expansion_to_children(("+<term>", {"extra_data": 1234})) 
[('+', []), ('<term>', None)]

我们将这个辅助函数实现为 GrammarFuzzer 中的一个方法,以便子类可以重载它:

class GrammarFuzzer(GrammarFuzzer):
    def expansion_to_children(self, expansion: Expansion) -> List[DerivationTree]:
        return expansion_to_children(expansion) 

将事物组合起来

使用这个,我们现在可以

  1. 树中的某个未扩展的节点,

  2. 选择一个随机的扩展,并且

  3. 返回新的树。

这就是方法 expand_node_randomly() 所做的。

`expand_node_randomly()` 实现

expand_node_randomly() 函数使用辅助函数 choose_node_expansion() 从可能的子节点数组中随机选择一个索引。(choose_node_expansion() 可以在子类中重载。)

import [random](https://docs.python.org/3/library/random.html) 
class GrammarFuzzer(GrammarFuzzer):
    def expand_node_randomly(self, node: DerivationTree) -> DerivationTree:
  """Choose a random expansion for `node` and return it"""
        (symbol, children) = node
        assert children is None

        if self.log:
            print("Expanding", all_terminals(node), "randomly")

        # Fetch the possible expansions from grammar...
        expansions = self.grammar[symbol]
        children_alternatives: List[List[DerivationTree]] = [
            self.expansion_to_children(expansion) for expansion in expansions
        ]

        # ... and select a random expansion
        index = self.choose_node_expansion(node, children_alternatives)
        chosen_children = children_alternatives[index]

        # Process children (for subclasses)
        chosen_children = self.process_chosen_children(chosen_children,
                                                       expansions[index])

        # Return with new children
        return (symbol, chosen_children) 

通用 expand_node() 方法可以后来用于选择不同的展开策略;到目前为止,它只使用 expand_node_randomly()

class GrammarFuzzer(GrammarFuzzer):
    def expand_node(self, node: DerivationTree) -> DerivationTree:
        return self.expand_node_randomly(node) 

辅助函数 process_chosen_children() 什么都不做;它可以被子类重载以在一旦选择子节点后处理它们。

class GrammarFuzzer(GrammarFuzzer):
    def process_chosen_children(self,
                                chosen_children: List[DerivationTree],
                                expansion: Expansion) -> List[DerivationTree]:
  """Process children after selection.  By default, does nothing."""
        return chosen_children 
```</details>

这是 `expand_node_randomly()` 的工作方式:

```py
f = GrammarFuzzer(EXPR_GRAMMAR, log=True)

print("Before expand_node_randomly():")
expr_tree = ("<integer>", None)
display_tree(expr_tree) 
Before expand_node_randomly():

0

print("After expand_node_randomly():")
expr_tree = f.expand_node_randomly(expr_tree)
display_tree(expr_tree) 
After expand_node_randomly():
Expanding <integer> randomly

0 1 0->1

习题

如果我们展开 <digit> 子树,我们会得到什么树?

我们当然可以对其进行测试,对吧?下面我们开始:

digit_subtree = expr_tree[1][0]
display_tree(digit_subtree) 

0

print("After expanding the <digit> subtree:")
digit_subtree = f.expand_node_randomly(digit_subtree)
display_tree(digit_subtree) 
After expanding the <digit> subtree:
Expanding <digit> randomly

0 1 7 (55) 0->1

我们看到 <digit> 根据语法规则再次展开——即展开成一个单独的数字。

习题

原始的 expr_tree 是否会受到这个更改的影响?

尽管我们已经更改了一个子树,但原始的 expr_tree 没有受到影响:

display_tree(expr_tree) 

0 1 0->1

这是因为 expand_node_randomly() 返回一个新的(展开的)树,并且不会改变作为参数传递的树。

展开树

现在我们将应用我们的单节点展开函数到树中的某个节点。为此,我们首先需要 搜索树中的未展开节点possible_expansions() 函数计算树中未展开符号的数量:

class GrammarFuzzer(GrammarFuzzer):
    def possible_expansions(self, node: DerivationTree) -> int:
        (symbol, children) = node
        if children is None:
            return 1

        return sum(self.possible_expansions(c) for c in children) 
f = GrammarFuzzer(EXPR_GRAMMAR)
print(f.possible_expansions(derivation_tree)) 
2

如果树有任何未展开的节点,any_possible_expansions() 方法返回 True。

class GrammarFuzzer(GrammarFuzzer):
    def any_possible_expansions(self, node: DerivationTree) -> bool:
        (symbol, children) = node
        if children is None:
            return True

        return any(self.any_possible_expansions(c) for c in children) 
f = GrammarFuzzer(EXPR_GRAMMAR)
f.any_possible_expansions(derivation_tree) 
True

下面是 expand_tree_once(),这是我们树展开算法的核心方法。它首先检查当前是否正在一个未展开的非终结符号上应用;如果是,它将调用上面讨论的 expand_node()

如果节点已经展开(即有子节点),它会检查仍有未展开符号的子节点子集,随机选择其中一个,并递归地应用自身于该子节点。

`expand_tree_once()` 实现

expand_tree_once() 方法在原地替换子节点,这意味着它实际上是在修改作为参数传递的树,而不是返回一个新的树。这种原地修改使得这个函数特别高效。再次强调,我们使用辅助方法 (choose_tree_expansion()) 从可以展开的子节点列表中返回选择的索引。

class GrammarFuzzer(GrammarFuzzer):
    def choose_tree_expansion(self,
                              tree: DerivationTree,
                              children: List[DerivationTree]) -> int:
  """Return index of subtree in `children` to be selected for expansion.
 Defaults to random."""
        return random.randrange(0, len(children))

    def expand_tree_once(self, tree: DerivationTree) -> DerivationTree:
  """Choose an unexpanded symbol in tree; expand it.
 Can be overloaded in subclasses."""
        (symbol, children) = tree
        if children is None:
            # Expand this node
            return self.expand_node(tree)

        # Find all children with possible expansions
        expandable_children = [
            c for c in children if self.any_possible_expansions(c)]

        # `index_map` translates an index in `expandable_children`
        # back into the original index in `children`
        index_map = [i for (i, c) in enumerate(children)
                     if c in expandable_children]

        # Select a random child
        child_to_be_expanded = \
            self.choose_tree_expansion(tree, expandable_children)

        # Expand in place
        children[index_map[child_to_be_expanded]] = \
            self.expand_tree_once(expandable_children[child_to_be_expanded])

        return tree 
```</details>

让我们说明 `expand_tree_once()` 的工作原理。我们以上面的推导树开始...

```py
derivation_tree = ("<start>",
                   [("<expr>",
                     [("<expr>", None),
                      (" + ", []),
                         ("<term>", None)]
                     )])
display_tree(derivation_tree) 

0 1 0->1 2 1->2 3 + 1->3 4 1->4

...现在将其展开两次:

f = GrammarFuzzer(EXPR_GRAMMAR, log=True)
derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <expr> randomly

0 1 0->1 2 1->2 4 + 1->4 5 1->5 3 2->3

derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <term> randomly

0 1 0->1 2 1->2 4 + 1->4 5 1->5 3 2->3 6 5->6 7 * 5->7 8 5->8

我们看到,随着每一步,会有一个符号被展开。现在只需要重复应用这个过程,进一步展开树。

关闭展开

使用 expand_tree_once(),我们可以继续扩展树 - 但我们实际上如何停止呢?这里的关键思想,由 Luke 在 [Luke et al, 2000] 中提出,是在将推导树膨胀到某个最大大小之后,我们只想应用那些至少增加树大小的扩展。例如,对于 <factor>,我们更倾向于扩展到 <integer>,因为这不会引入进一步的递归(和潜在的大小膨胀);对于 <integer>,同样,扩展到 <digit> 更受青睐,因为它比 <digit><integer> 更少增加树的大小。

为了识别扩展一个符号的成本,我们引入了两个相互依赖的函数:

  • symbol_cost() 返回使用 expansion_cost() 计算每个扩展的成本后,一个符号所有扩展中的最小成本。

  • expansion_cost() 返回 expansions 中所有扩展的总和。如果在遍历过程中再次遇到非终结符,扩展的成本是 \(\infty\),表示(可能是无限的)递归。

实现成本函数
class GrammarFuzzer(GrammarFuzzer):
    def symbol_cost(self, symbol: str, seen: Set[str] = set()) \
            -> Union[int, float]:
        expansions = self.grammar[symbol]
        return min(self.expansion_cost(e, seen | {symbol}) for e in expansions)

    def expansion_cost(self, expansion: Expansion,
                       seen: Set[str] = set()) -> Union[int, float]:
        symbols = nonterminals(expansion)
        if len(symbols) == 0:
            return 1  # no symbol

        if any(s in seen for s in symbols):
            return float('inf')

        # the value of a expansion is the sum of all expandable variables
        # inside + 1
        return sum(self.symbol_cost(s, seen) for s in symbols) + 1 
```</details>

这里有两个例子:扩展一个数字的最小成本是 1,因为我们必须从其扩展中选择一个。

```py
f = GrammarFuzzer(EXPR_GRAMMAR)
assert f.symbol_cost("<digit>") == 1 

然而,扩展 <expr> 的最小成本是五,因为这是最小数量的扩展所需。(<expr> \(\rightarrow\) <term> \(\rightarrow\) <factor> \(\rightarrow\) <integer> \(\rightarrow\) <digit> \(\rightarrow\) 1)

assert f.symbol_cost("<expr>") == 5 

我们定义 expand_node_by_cost(self, node, choose),这是 expand_node() 的一个变体,它考虑了上述成本。它确定所有子节点中的最小成本 cost,然后使用 choose 函数从列表中选择一个子节点,默认情况下是成本最小。如果有多个子节点具有相同的最小成本,它将在这些子节点之间随机选择。

`expand_node_by_cost()` 实现
class GrammarFuzzer(GrammarFuzzer):
    def expand_node_by_cost(self, node: DerivationTree, 
                            choose: Callable = min) -> DerivationTree:
        (symbol, children) = node
        assert children is None

        # Fetch the possible expansions from grammar...
        expansions = self.grammar[symbol]

        children_alternatives_with_cost = [(self.expansion_to_children(expansion),
                                            self.expansion_cost(expansion, {symbol}),
                                            expansion)
                                           for expansion in expansions]

        costs = [cost for (child, cost, expansion)
                 in children_alternatives_with_cost]
        chosen_cost = choose(costs)
        children_with_chosen_cost = [child for (child, child_cost, _) 
                                     in children_alternatives_with_cost
                                     if child_cost == chosen_cost]
        expansion_with_chosen_cost = [expansion for (_, child_cost, expansion)
                                      in children_alternatives_with_cost
                                      if child_cost == chosen_cost]

        index = self.choose_node_expansion(node, children_with_chosen_cost)

        chosen_children = children_with_chosen_cost[index]
        chosen_expansion = expansion_with_chosen_cost[index]
        chosen_children = self.process_chosen_children(
            chosen_children, chosen_expansion)

        # Return with a new list
        return (symbol, chosen_children) 
```</details>

简略的 `expand_node_min_cost()` 将 `min()` 作为 `choose` 函数传递,这使得它以最小成本扩展节点。

```py
class GrammarFuzzer(GrammarFuzzer):
    def expand_node_min_cost(self, node: DerivationTree) -> DerivationTree:
        if self.log:
            print("Expanding", all_terminals(node), "at minimum cost")

        return self.expand_node_by_cost(node, min) 

我们现在可以使用这个函数通过 expand_tree_once() 和上面的 expand_node_min_cost() 作为扩展函数来关闭我们的推导树的扩展。

class GrammarFuzzer(GrammarFuzzer):
    def expand_node(self, node: DerivationTree) -> DerivationTree:
        return self.expand_node_min_cost(node) 
f = GrammarFuzzer(EXPR_GRAMMAR, log=True)
display_tree(derivation_tree) 

0 1 0->1 2 1->2 4 + 1->4 5 1->5 3 2->3 6 5->6 7 * 5->7 8 5->8

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <term> at minimum cost

0 1 0->1 2 1->2 5 + 1->5 6 1->6 3 2->3 4 3->4 7 6->7 8 * 6->8 9 6->9

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <factor> at minimum cost

0 1 0->1 2 1->2 5 + 1->5 6 1->6 3 2->3 4 3->4 7 6->7 9 * 6->9 10 6->10 8 7->8

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <term> at minimum cost

0 1 0->1 2 1->2 5 + 1->5 6 1->6 3 2->3 4 3->4 7 6->7 9 * 6->9 10 6->10 8 7->8 11 10->11

我们会一直扩展,直到所有非终结符都被扩展。

while f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree) 
Expanding <integer> at minimum cost
Expanding <digit> at minimum cost
Expanding <factor> at minimum cost
Expanding <integer> at minimum cost
Expanding <factor> at minimum cost
Expanding <integer> at minimum cost
Expanding <digit> at minimum cost
Expanding <digit> at minimum cost

这里是最终的树:

display_tree(derivation_tree) 

0 1 0->1 2 1->2 8 + 1->8 9 1->9 3 2->3 4 3->4 5 4->5 6 5->6 7 7 (55) 6->7 10 9->10 14 * 9->14 15 9->15 11 10->11 12 11->12 13 5 (53) 12->13 16 15->16 17 16->17 18 17->18 19 8 (56) 18->19

我们看到在每一步中,expand_node_min_cost() 选择一个不会增加符号数量的扩展,最终关闭所有打开的扩展。

节点膨胀

尤其是在扩展的开始阶段,我们可能对获取尽可能多的节点感兴趣——也就是说,我们希望优先选择给我们提供更多可扩展非终端的扩展。这实际上与 expand_node_min_cost() 给我们的完全相反,我们可以实现一个 expand_node_max_cost() 方法,它将始终在具有最高成本的节点之间进行选择:

class GrammarFuzzer(GrammarFuzzer):
    def expand_node_max_cost(self, node: DerivationTree) -> DerivationTree:
        if self.log:
            print("Expanding", all_terminals(node), "at maximum cost")

        return self.expand_node_by_cost(node, max) 

为了说明 expand_node_max_cost(),我们再次重新定义 expand_node() 以使用它,然后使用 expand_tree_once() 来展示几个扩展步骤:

class GrammarFuzzer(GrammarFuzzer):
    def expand_node(self, node: DerivationTree) -> DerivationTree:
        return self.expand_node_max_cost(node) 
derivation_tree = ("<start>",
                   [("<expr>",
                     [("<expr>", None),
                      (" + ", []),
                         ("<term>", None)]
                     )]) 
f = GrammarFuzzer(EXPR_GRAMMAR, log=True)
display_tree(derivation_tree) 

0 1 0->1 2 1->2 3 + 1->3 4 1->4

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <term> at maximum cost

0 1 0->1 2 1->2 3 + 1->3 4 1->4 5 4->5 6 * 4->6 7 4->7

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <factor> at maximum cost

0 1 0->1 2 1->2 3 + 1->3 4 1->4 5 4->5 8 * 4->8 9 4->9 6 - (45) 5->6 7 5->7

if f.any_possible_expansions(derivation_tree):
    derivation_tree = f.expand_tree_once(derivation_tree)
display_tree(derivation_tree) 
Expanding <expr> at maximum cost

0 1 0->1 2 1->2 6 + 1->6 7 1->7 3 2->3 4 + 2->4 5 2->5 8 7->8 11 * 7->11 12 7->12 9 - (45) 8->9 10 8->10

我们可以看到,在每一步中,非终端的数量都在增加。显然,我们必须对这个数量进行限制。

三种扩展阶段

我们现在可以将这三个阶段合并到一个单独的函数 expand_tree() 中,该函数将按以下方式工作:

  1. 最大成本扩展。使用最大成本的扩展来扩展树,直到我们至少有 min_nonterminals 个非终端。这个阶段可以通过将 min_nonterminals 设置为零来轻松跳过。

  2. 随机扩展。继续随机扩展树,直到我们达到 max_nonterminals 个非终端。

  3. 最小成本扩展。以最小成本关闭扩展。

我们通过让 expand_node 引用扩展方法来实施这三个阶段。这是通过设置 expand_node(方法引用)首先为 expand_node_max_cost(即调用 expand_node() 将调用 expand_node_max_cost()),然后 expand_node_randomly,最后 expand_node_min_cost 来控制的。在前两个阶段,我们还分别设置了 min_nonterminalsmax_nonterminals 的最大限制。

三个阶段 `expand_tree()` 的实现
class GrammarFuzzer(GrammarFuzzer):
    def log_tree(self, tree: DerivationTree) -> None:
  """Output a tree if self.log is set; if self.display is also set, show the tree structure"""
        if self.log:
            print("Tree:", all_terminals(tree))
            if self.disp:
                display(display_tree(tree))
            # print(self.possible_expansions(tree), "possible expansion(s) left")

    def expand_tree_with_strategy(self, tree: DerivationTree,
                                  expand_node_method: Callable,
                                  limit: Optional[int] = None):
  """Expand tree using `expand_node_method` as node expansion function
 until the number of possible expansions reaches `limit`."""
        self.expand_node = expand_node_method
        while ((limit is None
                or self.possible_expansions(tree) < limit)
               and self.any_possible_expansions(tree)):
            tree = self.expand_tree_once(tree)
            self.log_tree(tree)
        return tree

    def expand_tree(self, tree: DerivationTree) -> DerivationTree:
  """Expand `tree` in a three-phase strategy until all expansions are complete."""
        self.log_tree(tree)
        tree = self.expand_tree_with_strategy(
            tree, self.expand_node_max_cost, self.min_nonterminals)
        tree = self.expand_tree_with_strategy(
            tree, self.expand_node_randomly, self.max_nonterminals)
        tree = self.expand_tree_with_strategy(
            tree, self.expand_node_min_cost)

        assert self.possible_expansions(tree) == 0

        return tree 

让我们在我们的例子中尝试一下。我们从一个半展开的推导树开始:

initial_derivation_tree: DerivationTree = ("<start>",
                   [("<expr>",
                     [("<expr>", None),
                      (" + ", []),
                         ("<term>", None)]
                     )]) 
display_tree(initial_derivation_tree) 

0 1 0->1 2 1->2 3 + 1->3 4 1->4

我们现在将我们的扩展策略应用于这棵树。我们看到,最初节点以最大成本扩展,然后随机扩展,最后以最小成本关闭扩展。

f = GrammarFuzzer(
    EXPR_GRAMMAR,
    min_nonterminals=3,
    max_nonterminals=5,
    log=True)
derivation_tree = f.expand_tree(initial_derivation_tree) 
Tree: <expr> + <term>
Expanding <expr> at maximum cost
Tree: <term> + <expr> + <term>
Expanding <expr> randomly
Tree: <term> + <term> + <term>
Expanding <term> randomly
Tree: <factor> / <term> + <term> + <term>
Expanding <term> randomly
Tree: <factor> / <factor> + <term> + <term>
Expanding <factor> randomly
Tree: <integer> / <factor> + <term> + <term>
Expanding <term> randomly
Tree: <integer> / <factor> + <factor> * <term> + <term>
Expanding <factor> at minimum cost
Tree: <integer> / <integer> + <factor> * <term> + <term>
Expanding <integer> at minimum cost
Tree: <integer> / <digit> + <factor> * <term> + <term>
Expanding <factor> at minimum cost
Tree: <integer> / <digit> + <integer> * <term> + <term>
Expanding <integer> at minimum cost
Tree: <digit> / <digit> + <integer> * <term> + <term>
Expanding <term> at minimum cost
Tree: <digit> / <digit> + <integer> * <term> + <factor>
Expanding <digit> at minimum cost
Tree: <digit> / 5 + <integer> * <term> + <factor>
Expanding <factor> at minimum cost
Tree: <digit> / 5 + <integer> * <term> + <integer>
Expanding <integer> at minimum cost
Tree: <digit> / 5 + <integer> * <term> + <digit>
Expanding <integer> at minimum cost
Tree: <digit> / 5 + <digit> * <term> + <digit>
Expanding <digit> at minimum cost
Tree: 7 / 5 + <digit> * <term> + <digit>
Expanding <digit> at minimum cost
Tree: 7 / 5 + <digit> * <term> + 0
Expanding <term> at minimum cost
Tree: 7 / 5 + <digit> * <factor> + 0
Expanding <factor> at minimum cost
Tree: 7 / 5 + <digit> * <integer> + 0
Expanding <integer> at minimum cost
Tree: 7 / 5 + <digit> * <digit> + 0
Expanding <digit> at minimum cost
Tree: 7 / 5 + 4 * <digit> + 0
Expanding <digit> at minimum cost
Tree: 7 / 5 + 4 * 2 + 0

这是最终的推导树:

display_tree(derivation_tree) 

0 1 0->1 2 1->2 27 + 1->27 28 1->28 3 2->3 14 + 2->14 15 2->15 4 3->4 8 / 3->8 9 3->9 5 4->5 6 5->6 7 7 (55) 6->7 10 9->10 11 10->11 12 11->12 13 5 (53) 12->13 16 15->16 17 16->17 21 * 16->21 22 16->22 18 17->18 19 18->19 20 4 (52) 19->20 23 22->23 24 23->24 25 24->25 26 2 (50) 25->26 29 28->29 30 29->30 31 30->31 32 0 (48) 31->32

这就是生成的字符串:

all_terminals(derivation_tree) 
'7 / 5 + 4 * 2 + 0'

将所有这些放在一起

基于此,我们现在可以定义一个函数 fuzz(),它就像 simple_grammar_fuzzer() 一样,简单地从一个语法中生成一个字符串。因此,它不再暴露推导树的复杂性。

class GrammarFuzzer(GrammarFuzzer):
    def fuzz_tree(self) -> DerivationTree:
  """Produce a derivation tree from the grammar."""
        tree = self.init_tree()
        # print(tree)

        # Expand all nonterminals
        tree = self.expand_tree(tree)
        if self.log:
            print(repr(all_terminals(tree)))
        if self.disp:
            display(display_tree(tree))
        return tree

    def fuzz(self) -> str:
  """Produce a string from the grammar."""
        self.derivation_tree = self.fuzz_tree()
        return all_terminals(self.derivation_tree) 

我们现在可以在所有定义的语法上应用这个策略(并可视化推导树)。

f = GrammarFuzzer(EXPR_GRAMMAR)
f.fuzz() 
'18.3 * 21.95 / 0'

在调用 fuzz() 之后,生成的推导树可以在 derivation_tree 属性中访问:

display_tree(f.derivation_tree) 

0 1 0->1 2 1->2 3 2->3 14 * 2->14 15 2->15 4 3->4 10 . (46) 3->10 11 3->11 5 4->5 7 4->7 6 1 (49) 5->6 8 7->8 9 8 (56) 8->9 12 11->12 13 3 (51) 12->13 16 15->16 30 / 15->30 31 15->31 17 16->17 23 . (46) 16->23 24 16->24 18 17->18 20 17->20 19 2 (50) 18->19 21 20->21 22 1 (49) 21->22 25 24->25 27 24->27 26 9 (57) 25->26 28 27->28 29 5 (53) 28->29 32 31->32 33 32->33 34 33->34 35 0 (48) 34->35

让我们在其他语法格式上尝试语法模糊器(及其树)。

f = GrammarFuzzer(URL_GRAMMAR)
f.fuzz() 
'ftp://user:password@www.google.com:53/def?abc=def'

display_tree(f.derivation_tree) 

0 1 0->1 2 1->2 4 😕/ 1->4 5 1->5 18 1->18 22 1->22 3 ftp 2->3 6 5->6 8 @ (64) 5->8 9 5->9 11 : (58) 5->11 12 5->12 7 user:password 6->7 10 www.google.com 9->10 13 12->13 14 13->14 16 13->16 15 5 (53) 14->15 17 3 (51) 16->17 19 / (47) 18->19 20 18->20 21 def 20->21 23 ? (63) 22->23 24 22->24 25 24->25 26 25->26 28 = (61) 25->28 29 25->29 27 abc 26->27 30 def 29->30

f = GrammarFuzzer(CGI_GRAMMAR, min_nonterminals=3, max_nonterminals=5)
f.fuzz() 
'e+d+'

display_tree(f.derivation_tree) 

0 1 0->1 2 1->2 5 1->5 3 2->3 4 e (101) 3->4 6 5->6 9 5->9 7 6->7 8 + (43) 7->8 10 9->10 13 9->13 11 10->11 12 d (100) 11->12 14 13->14 15 14->15 16 + (43) 15->16

我们与 simple_grammar_fuzzer() 的表现如何?

trials = 50
xs = []
ys = []
f = GrammarFuzzer(EXPR_GRAMMAR, max_nonterminals=20)
for i in range(trials):
    with Timer() as t:
        s = f.fuzz()
    xs.append(len(s))
    ys.append(t.elapsed_time())
    print(i, end=" ")
print() 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 

average_time = sum(ys) / trials
print("Average time:", average_time) 
Average time: 0.0263434316823259

%matplotlib inline

import [matplotlib.pyplot](https://matplotlib.org/) as plt
plt.scatter(xs, ys)
plt.title('Time required for generating an output'); 

我们生成的测试速度更快,但我们的输入也更小。我们看到,通过推导树,我们可以更好地控制语法的生成。

最后,当 simple_grammar_fuzzer() 失败时,GrammarFuzzer 如何与 expr_grammar 一起工作?它没有任何问题:

f = GrammarFuzzer(expr_grammar, max_nonterminals=10)
f.fuzz() 
'(9 + 7) * 7 * 2 * 5 + 7 * 2 / 6'

使用GrammarFuzzer,我们现在有一个坚实的基础来构建更多的模糊器,并展示生成软件测试领域的更多激动人心的概念。其中许多甚至不需要编写语法——相反,它们从当前领域推断出一个语法,因此即使不编写语法也可以使用基于语法的模糊测试。请保持关注!

经验教训

  • 推导树对于表达输入结构很重要

  • 基于推导树的语法模糊测试

    1. 比基于字符串的语法模糊测试更有效率,

    2. 对输入生成提供了更好的控制,

    3. 有效避免了无限扩展的问题。

下一步

恭喜!你已经到达了本书的核心“枢纽”之一。从这里,有一系列基于语法模糊的技术。

扩展语法

首先,我们有一些技术,它们都以某种形式扩展语法:

  • 解析和重新组合输入允许使用现有输入,再次使用推导树

  • 覆盖语法扩展允许组合覆盖

  • 概率分配给单个扩展提供了对扩展的额外控制

  • 约束分配给单个扩展允许对单个规则表达语义约束

应用语法

其次,我们可以在各种涉及某种形式自动学习的上下文中应用语法:

  • 模糊 API,从 API 学习语法

  • 模糊图形用户界面,从用户界面学习语法以进行后续模糊测试

  • 挖掘语法,学习任意输入格式的语法

继续扩展!

背景

推导树(通常称为解析树)是一种标准的数据结构,解析器将输入分解到其中。在《龙书》(也称为《编译原理、技术和工具》)[Aho 等人,2006]中讨论了将解析作为编译程序的一部分到推导树中。我们也在解析和重新组合输入时使用推导树。

本章的关键思想,即扩展直到达到符号的限制,然后总是选择最短路径,源自 Luke [Luke 等人,2000]。

练习

练习 1:缓存方法结果

跟踪GrammarFuzzer显示某些方法被反复调用,总是使用相同的值。

设置一个具有缓存的类FasterGrammarFuzzer,该缓存检查方法是否之前已被调用,如果是,则返回之前计算的“缓存”值。对expansion_to_children()做这件事。比较优化前后的调用次数。

重要:对于 expansion_to_children(),确保返回的每个列表都是单独的副本。如果你返回相同的(缓存的)列表,这将干扰 GrammarFuzzer 的就地修改。为此,请使用 Python 的 copy.deepcopy() 函数。

使用笔记本来练习题目并查看解决方案。

练习 2:语法预编译

一些方法,如 symbol_cost()expansion_cost(),返回一个仅依赖于语法的值。设置一个类 EvenFasterGrammarFuzzer(),在初始化时预先计算这些值,这样后续调用 symbol_cost()expansion_cost() 只需查找这些值。

使用笔记本来练习题目并查看解决方案。

练习 3:维护要扩展的树

expand_tree_once() 中,算法反复遍历树以找到仍然可以扩展的非终结符。通过保留树中仍然可以扩展的非终结符符号列表来加快这个过程。

使用笔记本来练习题目并查看解决方案。

练习 4:交替随机扩展

我们可以定义 expand_node_randomly(),使其简单地调用 expand_node_by_cost(node, random.choice)

class ExerciseGrammarFuzzer(GrammarFuzzer):
    def expand_node_randomly(self, node: DerivationTree) -> DerivationTree:
        if self.log:
            print("Expanding", all_terminals(node), "randomly by cost")

        return self.expand_node_by_cost(node, random.choice) 

原始实现和这个替代方案有什么区别?

使用笔记本来练习题目并查看解决方案。

Creative Commons License 本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后更改:2023-11-11 18:18:06+01:00 • 引用 • 印记

如何引用这篇作品

安德烈亚斯·泽勒(Andreas Zeller)、拉胡尔·戈皮纳特(Rahul Gopinath)、马塞尔·博姆(Marcel Böhme)、戈登·弗朗西斯(Gordon Fraser)和克里斯蒂安·霍勒(Christian Holler): "高效的语法模糊测试". 在安德烈亚斯·泽勒、拉胡尔·戈皮纳特、马塞尔·博姆、戈登·弗朗西斯和克里斯蒂安·霍勒的 "模糊测试书" 中,www.fuzzingbook.org/html/GrammarFuzzer.html. 2023-11-11 18:18:06+01:00 获取。

@incollection{fuzzingbook2023:GrammarFuzzer,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Efficient Grammar Fuzzing},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/GrammarFuzzer.html}},
    note = {Retrieved 2023-11-11 18:18:06+01:00},
    url = {https://www.fuzzingbook.org/html/GrammarFuzzer.html},
    urldate = {2023-11-11 18:18:06+01:00}
}

语法覆盖率

原文:www.fuzzingbook.org/html/GrammarCoverageFuzzer.html

从语法生成输入 给出规则的所有可能的扩展相同的可能性。然而,为了生成一个全面的测试套件,最大化 多样性 更有意义——例如,通过不重复相同的扩展。在本章中,我们探讨了如何系统地 覆盖 语法元素,以最大化多样性,并且不遗漏个别元素。

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('GGb3e5p0HC8') 

先决条件

  • 您应该已经阅读了 关于语法的章节。

  • 您应该已经阅读了 关于高效语法模糊的章节。

概述

要 使用本章提供的代码,请编写

>>> from fuzzingbook.GrammarCoverageFuzzer import <identifier> 

然后利用以下功能。

本章介绍了 GrammarCoverageFuzzer,这是一个高效的语法模糊器,它扩展了来自关于高效语法模糊的章节的 GrammarFuzzer。它力求至少覆盖所有扩展一次,从而确保功能的覆盖率。

在以下示例中,例如,我们使用 GrammarCoverageFuzzer 生成一个表达式。我们看到生成的表达式覆盖了单个表达式中的所有数字和所有运算符。

>>> from Grammars import EXPR_GRAMMAR
>>> expr_fuzzer = GrammarCoverageFuzzer(EXPR_GRAMMAR)
>>> expr_fuzzer.fuzz()
'-(2 + 3) * 4.5 / 6 - 2.0 / +8 + 7 + 3' 

模糊后,expansion_coverage() 方法返回一个语法扩展覆盖的映射。

>>> expr_fuzzer.expansion_coverage()
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9',
 '<expr> -> <term>',
 '<expr> -> <term> + <expr>',
 '<expr> -> <term> - <expr>',
 '<factor> -> (<expr>)',
 '<factor> -> +<factor>',
 '<factor> -> -<factor>',
 '<factor> -> <integer>',
 '<factor> -> <integer>.<integer>',
 '<integer> -> <digit>',
 '<integer> -> <digit><integer>',
 '<start> -> <expr>',
 '<term> -> <factor>',
 '<term> -> <factor> * <term>',
 '<term> -> <factor> / <term>'} 

后续对 fuzz() 的调用将寻求进一步的覆盖(即,覆盖其他区域代码数字,例如);对 reset() 的调用清除记录的覆盖,重新开始。

由于这种覆盖率在输入中也产生了更高的代码覆盖率,因此 GrammarCoverageFuzzerGrammarFuzzer 的一个推荐扩展。

GrammarCoverageFuzzer <a xlink:href="#" xlink:title="class GrammarCoverageFuzzer:

从语法生成,旨在覆盖所有扩展。《GrammarCoverageFuzzer》GrammarCoverageFuzzer _new_child_coverage() <a xlink:href="#" xlink:title="choose_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

children_alternatives 中选择 node 的一个扩展。

返回 n,使得扩展 children_alternatives[n]

产生最高的额外覆盖范围。">choose_node_expansion() <a xlink:href="#" xlink:title="new_child_coverage(self, symbol: str, children: List[DerivationTree], max_depth: Union[int, float] = inf) -> Set[str]:

返回将获得的新覆盖范围

通过扩展 (symbol, children)">new_child_coverage() <a xlink:href="#" xlink:title="new_coverages(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> Optional[List[Set[str]]]:

返回每个子节点在最小深度下的覆盖范围。《text text-anchor="start" x="45.12" y="-7.75" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-size="10.00">new_coverages() SimpleGrammarCoverageFuzzer <a xlink:href="#" xlink:title="class SimpleGrammarCoverageFuzzer:

在选择扩展时,优先选择未被覆盖的扩展。">SimpleGrammarCoverageFuzzer <a xlink:href="#" xlink:title="choose_covered_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

返回在 _covered_ children_alternatives 中的扩展索引

需要被选择。

在子类中需要重载。">choose_covered_node_expansion() <a xlink:href="#" xlink:title="choose_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

返回 children_alternatives 中要选择的扩展索引。

如果有,选择未被覆盖的扩展。">choose_node_expansion() <a xlink:href="#" xlink:title="choose_uncovered_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

返回在 _uncovered_ children_alternatives 中的扩展索引

需要被选择。

在子类中重载。">choose_uncovered_node_expansion() GrammarCoverageFuzzer->SimpleGrammarCoverageFuzzer TrackingGrammarCoverageFuzzer <a xlink:href="#" xlink:title="class TrackingGrammarCoverageFuzzer:

在生产过程中跟踪语法覆盖率">TrackingGrammarCoverageFuzzer <a xlink:href="#" xlink:title="init(self, *args, **kwargs) -> None:

grammar生成字符串,从start_symbol开始。

如果提供了min_nonterminalsmax_nonterminals,则使用它们作为限制

计算产生的非终结符数量。

如果设置了disp,则显示中间推导树。

如果设置了log,则将中间步骤以文本形式显示在标准输出上。">init() <a xlink:href="#" xlink:title="expansion_coverage(self) -> Set[str]:

返回作为字符串的已覆盖扩展集 SYMBOL -> EXPANSION">expansion_coverage() <a xlink:href="#" xlink:title="max_expansion_coverage(self, symbol: Optional[str] = None, max_depth: Union[int, float] = inf) -> Set[str]:

返回语法中所有扩展的集合

symbol开始(默认:起始符号)。

如果提供了max_depth,则仅扩展到该深度。">max_expansion_coverage() <a xlink:href="#" xlink:title="missing_expansion_coverage(self) -> Set[str]:

返回尚未覆盖的扩展">missing_expansion_coverage() <a xlink:href="#" xlink:title="reset_coverage(self) -> None:

清除迄今为止跟踪的覆盖信息">reset_coverage() _max_expansion_coverage() add_coverage() <a xlink:href="#" xlink:title="choose_node_expansion(self, node: DerivationTree, children_alternatives: List[List[DerivationTree]]) -> int:

返回在children_alternatives中要选择的扩展的索引。

'children_alternatives':node的可能子节点列表。

默认为随机。在子类中重载。">choose_node_expansion() SimpleGrammarCoverageFuzzer->TrackingGrammarCoverageFuzzer GrammarFuzzer <a xlink:href="GrammarFuzzer.html" xlink:title="class GrammarFuzzer:

使用推导树高效地生成语法字符串。">GrammarFuzzer <a xlink:href="GrammarFuzzer.html" xlink:title="init(self, grammar: Dict[str, List[Expansion]], start_symbol: str = '', min_nonterminals: int = 0, max_nonterminals: int = 10, disp: bool = False, log: Union[bool, int] = False) -> None:

grammar生成字符串,从start_symbol开始。

如果提供了min_nonterminalsmax_nonterminals,则使用它们作为限制。

对于产生的非终结符数量。

如果disp被设置,则显示中间的推导树。

如果log被设置,则将中间步骤作为文本显示在标准输出上。">init() <a xlink:href="GrammarFuzzer.html" xlink:title="fuzz(self) -> str:

从语法生成字符串。 <a xlink:href="GrammarFuzzer.html" xlink:title="fuzz_tree(self) -> DerivationTree:

从语法生成推导树。 TrackingGrammarCoverageFuzzer->GrammarFuzzer Fuzzer <a xlink:href="Fuzzer.html" xlink:title="class Fuzzer:

模糊测试器的基类。 <a xlink:href="Fuzzer.html" xlink:title="run(self, runner: Fuzzer.Runner = <Fuzzer.Runner object>) -> Tuple[subprocess.CompletedProcess, str]:

使用模糊输入运行 runnerrun() 方法: <a xlink:href="Fuzzer.html" xlink:title="runs(self, runner: Fuzzer.Runner = <Fuzzer.PrintRunner object>, trials: int = 10) -> List[Tuple[subprocess.CompletedProcess, str]]:

使用模糊输入运行runnertrials次">runs() GrammarFuzzer->Fuzzer 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

覆盖语法元素

测试生成的目的是覆盖程序的所有功能——当然,希望包括失败的功能。然而,这种功能与输入结构相关:如果我们无法生成某些输入元素,那么相关的代码和功能也不会被触发,从而消除了我们在那里找到错误的机会。

例如,考虑我们表达式语法EXPR_GRAMMAR来自关于语法的章节:

  • 如果我们不生成负数,则不会测试负数。

  • 如果我们不生成浮点数,则不会测试浮点数。

因此,我们的目标必须是覆盖所有可能的扩展——而且不仅仅是偶然的,而是有计划的。

要最大化这种多样性的一种方法是通过跟踪语法生成过程中发生的扩展:如果我们已经看到了一些扩展,我们就可以从可能的扩展集中优先考虑其他可能的扩展候选者。考虑我们表达式语法中的以下规则:

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import quiz 
from Fuzzer import Fuzzer 
from [typing](https://docs.python.org/3/library/typing.html) import Dict, List, Set, Union, Optional 
from Grammars import EXPR_GRAMMAR, CGI_GRAMMAR, URL_GRAMMAR, START_SYMBOL
from Grammars import is_valid_grammar, extend_grammar, Grammar 
EXPR_GRAMMAR["<factor>"] 
['+<factor>', '-<factor>', '(<expr>)', '<integer>.<integer>', '<integer>']

假设我们已经在前一个 <factor> 的扩展中产生了一个 <integer>。当扩展下一个因子时,我们会标记 <integer> 扩展为已覆盖,并选择尚未覆盖的替代方案之一,例如 -<factor>(一个负数)或 <integer>.<integer>(一个浮点数)。只有当我们覆盖了所有替代方案后,我们才会回过头来重新考虑之前覆盖的扩展。

问答

表达式 1 + 2 覆盖了 EXPR_GRAMMAR 的哪些扩展?

的确!该表达式从 <start> 扩展到单个数字。

跟踪语法覆盖率

这种 语法覆盖率 的概念很容易实现。我们引入一个类 TrackingGrammarCoverageFuzzer,它跟踪当前实现的语法覆盖率:

from Grammars import Grammar, Expansion
from GrammarFuzzer import GrammarFuzzer, all_terminals, nonterminals, \
    display_tree, DerivationTree 
import [random](https://docs.python.org/3/library/random.html) 
class TrackingGrammarCoverageFuzzer(GrammarFuzzer):
  """Track grammar coverage during production"""

    def __init__(self, *args, **kwargs) -> None:
        # invoke superclass __init__(), passing all arguments
        super().__init__(*args, **kwargs)
        self.reset_coverage() 

跟踪扩展

在集合 covered_expansions 中,我们存储看到的单个扩展。

class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def expansion_coverage(self) -> Set[str]:
  """Return the set of covered expansions as strings SYMBOL -> EXPANSION"""
        return self.covered_expansions

    def reset_coverage(self) -> None:
  """Clear coverage info tracked so far"""
        self.covered_expansions: Set[str] = set() 

我们将扩展保存为字符串 "符号 -> 扩展",使用函数 expansion_key() 为 (符号, 扩展) 对生成字符串表示。

def expansion_key(symbol: str, 
                  expansion: Union[Expansion,
                                   DerivationTree, 
                                   List[DerivationTree]]) -> str:
  """Convert (symbol, `expansion`) into a key "SYMBOL -> EXPRESSION". 
 `expansion` can be an expansion string, a derivation tree,
 or a list of derivation trees."""

    if isinstance(expansion, tuple):
        # Expansion or single derivation tree
        expansion, _ = expansion

    if not isinstance(expansion, str):
        # Derivation tree
        children = expansion
        expansion = all_terminals((symbol, children))

    assert isinstance(expansion, str)

    return symbol + " -> " + expansion 

这里有一个例子:

expansion_key(START_SYMBOL, EXPR_GRAMMAR[START_SYMBOL][0]) 
'<start> -> <expr>'

除了 扩展,我们还可以传递一个子节点列表作为参数,然后它将自动转换为字符串。

children: List[DerivationTree] = [("<expr>", None), (" + ", []), ("<term>", None)]
expansion_key("<expr>", children) 
'<expr> -> <expr> + <term>'

计算可能的扩展

我们可以通过枚举所有扩展来计算语法中的可能扩展集。方法 max_expansion_coverage() 从给定的符号(默认:语法起始符号)递归遍历语法,并将所有扩展累积到集合 expansions 中。通过 max_depth 参数(默认:\(\infty\)),我们可以控制语法探索的深度;我们将在本章后面需要它。

class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def _max_expansion_coverage(self, symbol: str, 
                                max_depth: Union[int, float]) -> Set[str]:
        if max_depth <= 0:
            return set()

        self._symbols_seen.add(symbol)

        expansions = set()
        for expansion in self.grammar[symbol]:
            expansions.add(expansion_key(symbol, expansion))
            for nonterminal in nonterminals(expansion):
                if nonterminal not in self._symbols_seen:
                    expansions |= self._max_expansion_coverage(
                        nonterminal, max_depth - 1)

        return expansions

    def max_expansion_coverage(self, symbol: Optional[str] = None,
                               max_depth: Union[int, float] = float('inf')) \
            -> Set[str]:
  """Return set of all expansions in a grammar 
 starting with `symbol` (default: start symbol).
 If `max_depth` is given, expand only to that depth."""
        if symbol is None:
            symbol = self.start_symbol

        self._symbols_seen: Set[str] = set()
        cov = self._max_expansion_coverage(symbol, max_depth)

        if symbol == START_SYMBOL:
            assert len(self._symbols_seen) == len(self.grammar)

        return cov 

我们可以使用 max_expansion_coverage() 来计算表达式语法中的所有扩展:

expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR)
expr_fuzzer.max_expansion_coverage() 
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9',
 '<expr> -> <term>',
 '<expr> -> <term> + <expr>',
 '<expr> -> <term> - <expr>',
 '<factor> -> (<expr>)',
 '<factor> -> +<factor>',
 '<factor> -> -<factor>',
 '<factor> -> <integer>',
 '<factor> -> <integer>.<integer>',
 '<integer> -> <digit>',
 '<integer> -> <digit><integer>',
 '<start> -> <expr>',
 '<term> -> <factor>',
 '<term> -> <factor> * <term>',
 '<term> -> <factor> / <term>'}

在模糊过程中跟踪扩展

在扩展过程中,我们可以跟踪已看到的扩展。为此,我们钩入方法 choose_node_expansion(),在语法模糊器中扩展单个节点。

class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def add_coverage(self, symbol: str,
                     new_child: Union[Expansion, List[DerivationTree]]) -> None:
        key = expansion_key(symbol, new_child)

        if self.log and key not in self.covered_expansions:
            print("Now covered:", key)
        self.covered_expansions.add(key)

    def choose_node_expansion(self, node: DerivationTree,
                              children_alternatives: 
                              List[List[DerivationTree]]) -> int:
        (symbol, children) = node
        index = super().choose_node_expansion(node, children_alternatives)
        self.add_coverage(symbol, children_alternatives[index])
        return index 

方法 missing_expansion_coverage() 是一个辅助方法,它返回仍需覆盖的扩展:

class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def missing_expansion_coverage(self) -> Set[str]:
  """Return expansions not covered yet"""
        return self.max_expansion_coverage() - self.expansion_coverage() 

将事物组合在一起

让我们展示跟踪是如何工作的。为了使事情简单,让我们只关注 <digit> 扩展。

digit_fuzzer = TrackingGrammarCoverageFuzzer(
    EXPR_GRAMMAR, start_symbol="<digit>", log=True)
digit_fuzzer.fuzz() 
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 9
Tree: 9
'9'

'9'

digit_fuzzer.fuzz() 
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 0
Tree: 0
'0'

'0'

digit_fuzzer.fuzz() 
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 5
Tree: 5
'5'

'5'

这里是迄今为止覆盖的扩展集合:

digit_fuzzer.expansion_coverage() 
{'<digit> -> 0', '<digit> -> 5', '<digit> -> 9'}

这是我们可以覆盖的所有扩展集合:

digit_fuzzer.max_expansion_coverage() 
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9'}

这是缺失的覆盖率:

digit_fuzzer.missing_expansion_coverage() 
{'<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8'}

平均来说,我们需要产生多少个字符才能覆盖所有扩展?

def average_length_until_full_coverage(fuzzer: TrackingGrammarCoverageFuzzer) -> float:
    trials = 50

    sum = 0
    for trial in range(trials):
        # print(trial, end=" ")
        fuzzer.reset_coverage()
        while len(fuzzer.missing_expansion_coverage()) > 0:
            s = fuzzer.fuzz()
            sum += len(s)

    return sum / trials 
digit_fuzzer.log = False
average_length_until_full_coverage(digit_fuzzer) 
28.4

对于完整表达式,这需要更长的时间:

expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR)
average_length_until_full_coverage(expr_fuzzer) 
138.12

覆盖语法扩展

现在,让我们不仅跟踪覆盖率,而且实际 产生 覆盖率。思路如下:

  1. 我们确定尚未覆盖的子节点(在 uncovered_children 中)

  2. 如果所有子节点都已覆盖,我们将回退到原始方法(即随机选择一个扩展)

  3. 否则,我们从未覆盖的子节点中选择一个子节点并将其标记为已覆盖。

为了这个目的,我们引入了一个新的模糊器 SimpleGrammarCoverageFuzzer,它在 choose_node_expansion() 方法中实现了这个策略——这是GrammarFuzzer超类用来选择要扩展的子类的方法。

class SimpleGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
  """When choosing expansions, prefer expansions not covered."""

    def choose_node_expansion(self,
                              node: DerivationTree,
                              children_alternatives: List[List[DerivationTree]]) -> int:
  """Return index of expansion in `children_alternatives` to be selected.
 Picks uncovered expansions, if any."""

        # Prefer uncovered expansions
        (symbol, children) = node
        uncovered_children = [c for (i, c) in enumerate(children_alternatives)
                              if expansion_key(symbol, c)
                              not in self.covered_expansions]
        index_map = [i for (i, c) in enumerate(children_alternatives)
                     if c in uncovered_children]

        if len(uncovered_children) == 0:
            # All expansions covered - use superclass method
            return self.choose_covered_node_expansion(node, children_alternatives)

        # Select from uncovered nodes
        index = self.choose_uncovered_node_expansion(node, uncovered_children)

        return index_map[index] 

提供了两种方法 choose_covered_node_expansion()choose_uncovered_node_expansion(),供子类钩子使用:

class SimpleGrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer):
    def choose_uncovered_node_expansion(self,
                                        node: DerivationTree,
                                        children_alternatives: List[List[DerivationTree]]) \
            -> int:
  """Return index of expansion in _uncovered_ `children_alternatives`
 to be selected.
 To be overloaded in subclasses."""
        return TrackingGrammarCoverageFuzzer.choose_node_expansion(
            self, node, children_alternatives)

    def choose_covered_node_expansion(self,
                                      node: DerivationTree,
                                      children_alternatives: List[List[DerivationTree]]) \
            -> int:
  """Return index of expansion in _covered_ `children_alternatives`
 to be selected.
 To be overloaded in subclasses."""
        return TrackingGrammarCoverageFuzzer.choose_node_expansion(
            self, node, children_alternatives) 

通过返回迄今为止覆盖的扩展集,我们可以多次调用模糊器,每次都增加语法覆盖率。例如,使用 EXPR_GRAMMAR 语法生成数字,模糊器会一个接一个地生成数字:

f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="<digit>")
f.fuzz() 
'5'

f.fuzz() 
'2'

f.fuzz() 
'1'

到目前为止,这是已覆盖的扩展集:

f.expansion_coverage() 
{'<digit> -> 1', '<digit> -> 2', '<digit> -> 5'}

让我们再模糊一些。我们看到,随着每次迭代,我们都会覆盖另一个扩展:

for i in range(7):
    print(f.fuzz(), end=" ") 
0 9 7 4 8 3 6 

最后,所有扩展都已覆盖:

f.missing_expansion_coverage() 
set()

让我们在一个更复杂的语法上应用这个——例如,完整的表达式语法。我们看到,经过几次迭代,我们覆盖了每个数字、运算符和扩展:

f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)
for i in range(10):
    print(f.fuzz()) 
+(0.31 / (5) / 9 + 4 * 6 / 3 - 8 - 7) * -2
+++2 / 87360
((4) * 0 - 1) / -9.6 + 7 / 6 + 1 * 8 + 7 * 8
++++26 / -64.45
(8 / 1 / 6 + 9 + 7 + 8) * 1.1 / 0 * 1
7.7
++(3.5 / 3) - (-4 + 3) / (8 / 0) / -4 * 2 / 1
+(90 / --(28 * 8 / 5 + 5 / (5 / 8))) - +9.36 / 2.5 * (5 * (7 * 6 * 5) / 8)
9.11 / 7.28
1 / (9 - 5 * 6) / 6 / 7 / 7 + 1 + 1 - 7 * -3

再次,所有扩展都已覆盖:

f.missing_expansion_coverage() 
set()

我们看到,我们的策略在实现覆盖率方面比随机方法更有效:

average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)) 
52.28

深度预见

为单个规则选择扩展是一个好的开始;然而,正如以下示例所示,这还不够。我们对来自语法章节的 CGI 语法应用我们的覆盖率模糊器:

CGI_GRAMMAR 
{'<start>': ['<string>'],
 '<string>': ['<letter>', '<letter><string>'],
 '<letter>': ['<plus>', '<percent>', '<other>'],
 '<plus>': ['+'],
 '<percent>': ['%<hexdigit><hexdigit>'],
 '<hexdigit>': ['0',
  '1',
  '2',
  '3',
  '4',
  '5',
  '6',
  '7',
  '8',
  '9',
  'a',
  'b',
  'c',
  'd',
  'e',
  'f'],
 '<other>': ['0', '1', '2', '3', '4', '5', 'a', 'b', 'c', 'd', 'e', '-', '_']}

f = SimpleGrammarCoverageFuzzer(CGI_GRAMMAR)
for i in range(10):
    print(f.fuzz()) 
c
+%a6++
+-
+
++
%18%b7
+e
_
d2+%e3
%d0

经过 10 次迭代后,我们仍然有一些扩展未被覆盖:

f.missing_expansion_coverage() 
{'<hexdigit> -> 2',
 '<hexdigit> -> 4',
 '<hexdigit> -> 5',
 '<hexdigit> -> 9',
 '<hexdigit> -> c',
 '<hexdigit> -> f',
 '<other> -> 0',
 '<other> -> 1',
 '<other> -> 3',
 '<other> -> 4',
 '<other> -> 5',
 '<other> -> a',
 '<other> -> b'}

为什么会这样?问题是,在 CGI 语法中,要覆盖的最大变化数发生在 hexdigit 规则中。然而,我们首先需要到达这个扩展。当扩展 <letter> 符号时,我们有三种可能的扩展选择:

CGI_GRAMMAR["<letter>"] 
['<plus>', '<percent>', '<other>']

如果所有三个扩展都已经覆盖,那么上面的 choose_node_expansion() 将随机选择一个——即使在选择 <percent> 时可能还有更多扩展需要覆盖。

我们需要的是一个更好的策略,如果之后还有更多未覆盖的扩展,它会选择 <percent>——即使 <percent> 已经被覆盖。这种策略最初由 W. Burkhardt [Burkhardt et al, 1967] 以“最短路径选择”的名义讨论。

这个版本从几个开发选择中选择了,在其中有未使用的单位可用,从最短路径开始。

这是我们将在下一步中实现的内容。

确定最大符号覆盖率

为了解决这个问题,我们引入了一个新的类 GrammarCoverageFuzzer,它基于 SimpleGrammarCoverageFuzzer,但具有更好的策略。首先,我们需要计算从特定符号可以到达的最大扩展集,正如我们在 max_expansion_coverage() 中已经实现的。想法是稍后计算这个集和已覆盖扩展的交集,这样我们就可以优先考虑那些具有非空交集的扩展。

第一步——计算从一个符号可以到达的最大展开集——已经实现。通过传递一个 symbol 参数给 max_expansion_coverage(),我们可以计算每个符号的可能展开:

f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)
f.max_expansion_coverage('<integer>') 
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9',
 '<integer> -> <digit>',
 '<integer> -> <digit><integer>'}

我们看到,通过展开 <integer>,我们可以覆盖总共 12 个生成式。

问答

f.max_expansion_coverage('<digit>') 会返回多少个生成式?

的确。以下是 <digit> 所有可能的展开:

f.max_expansion_coverage('<digit>') 
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9'}

确定尚未覆盖的子节点

我们现在可以开始实现 GrammarCoverageFuzzer。我们的想法是确定每个子节点的 缺失覆盖率

给定一个子节点列表,我们可以使用 max_expansion_coverage() 来计算每个子节点的最大覆盖率。从这些中,我们 减去 已经看到的覆盖率(expansion_coverage())。这结果是我们可以获得的覆盖率。

class GrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer):
  """Produce from grammars, aiming for coverage of all expansions."""

    def new_child_coverage(self,
                           symbol: str,
                           children: List[DerivationTree],
                           max_depth: Union[int, float] = float('inf')) -> Set[str]:
  """Return new coverage that would be obtained 
 by expanding (`symbol`, `children`)"""

        new_cov = self._new_child_coverage(children, max_depth)
        new_cov.add(expansion_key(symbol, children))
        new_cov -= self.expansion_coverage()   # -= is set subtraction
        return new_cov

    def _new_child_coverage(self, children: List[DerivationTree],
                            max_depth: Union[int, float]) -> Set[str]:
        new_cov: Set[str] = set()
        for (c_symbol, _) in children:
            if c_symbol in self.grammar:
                new_cov |= self.max_expansion_coverage(c_symbol, max_depth)

        return new_cov 

让我们说明 new_child_coverage()。我们再次开始模糊测试,随机选择展开。

f = GrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="<digit>", log=True)
f.fuzz() 
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 2
Tree: 2
'2'

'2'

这是我们的当前覆盖率:

f.expansion_coverage() 
{'<digit> -> 2'}

如果我们想将 <digit> 展开为 0,这将给我们新的覆盖率:

f.new_child_coverage("<digit>", [('0', [])]) 
{'<digit> -> 0'}

如果我们想再次将 <digit> 展开为 2,这将给我们 没有 新的覆盖率:

f.new_child_coverage("<digit>", [('2', [])]) 
set()

当我们遍历 <digit> 的单个展开可能性时,我们看到所有展开都提供了额外的覆盖率,除了 我们已经覆盖的 2

for expansion in EXPR_GRAMMAR["<digit>"]:
    children = f.expansion_to_children(expansion)
    print(expansion, f.new_child_coverage("<digit>", children)) 
0 {'<digit> -> 0'}
1 {'<digit> -> 1'}
2 set()
3 {'<digit> -> 3'}
4 {'<digit> -> 4'}
5 {'<digit> -> 5'}
6 {'<digit> -> 6'}
7 {'<digit> -> 7'}
8 {'<digit> -> 8'}
9 {'<digit> -> 9'}

这意味着在每次选择展开时,我们可以利用 new_child_coverage() 并在提供最大新(未见)覆盖率的展开中选择。

自适应前瞻

在选择子节点时,我们并不寻求获得的最大总体覆盖率,因为这会使具有许多未覆盖可能性的展开完全主导其他展开。相反,我们追求一种 广度优先 策略,首先覆盖所有给定深度的展开,然后才寻找更深的深度。

new_coverages() 方法是这个策略的核心:从最大深度(max_depth)为零开始,它增加深度直到找到至少一个未覆盖的展开。

实现 `new_coverage()`
class GrammarCoverageFuzzer(GrammarCoverageFuzzer):
    def new_coverages(self, node: DerivationTree,
                      children_alternatives: List[List[DerivationTree]]) \
            -> Optional[List[Set[str]]]:
  """Return coverage to be obtained for each child at minimum depth"""

        (symbol, children) = node
        for max_depth in range(len(self.grammar)):
            new_coverages = [
                self.new_child_coverage(
                    symbol, c, max_depth) for c in children_alternatives]
            max_new_coverage = max(len(new_coverage)
                                   for new_coverage in new_coverages)
            if max_new_coverage > 0:
                # Uncovered node found
                return new_coverages

        # All covered
        return None 
```</details>

### 全部一起

我们现在可以定义 `choose_node_expansion()` 来利用这种策略:

1.  我们确定要获得的可能覆盖率(使用 `new_coverages()`)。

1.  我们(随机地)在具有最大覆盖率的子节点之间进行选择(使用 `choose_uncovered_node_expansion()`)。

<details id="Excursion:-Implementing-choose_node_expansion()"><summary>实现 `choose_node_expansion()`</summary>

```py
class GrammarCoverageFuzzer(GrammarCoverageFuzzer):
    def choose_node_expansion(self, node: DerivationTree,
                              children_alternatives: List[List[DerivationTree]]) -> int:
  """Choose an expansion of `node` among `children_alternatives`.
 Return `n` such that expanding `children_alternatives[n]`
 yields the highest additional coverage."""

        (symbol, children) = node
        new_coverages = self.new_coverages(node, children_alternatives)

        if new_coverages is None:
            # All expansions covered - use superclass method
            return self.choose_covered_node_expansion(node, children_alternatives)

        max_new_coverage = max(len(cov) for cov in new_coverages)

        children_with_max_new_coverage = [c for (i, c) in enumerate(children_alternatives)
                                          if len(new_coverages[i]) == max_new_coverage]
        index_map = [i for (i, c) in enumerate(children_alternatives)
                     if len(new_coverages[i]) == max_new_coverage]

        # Select a random expansion
        new_children_index = self.choose_uncovered_node_expansion(
            node, children_with_max_new_coverage)
        new_children = children_with_max_new_coverage[new_children_index]

        # Save the expansion as covered
        key = expansion_key(symbol, new_children)

        if self.log:
            print("Now covered:", key)
        self.covered_expansions.add(key)

        return index_map[new_children_index] 
```</details>

现在,我们的 `GrammarCoverageFuzzer` 已经完成!让我们在一系列示例中应用它。在表达式上,它很快就能覆盖所有数字和运算符:

```py
f = GrammarCoverageFuzzer(EXPR_GRAMMAR, min_nonterminals=3)
f.fuzz() 
'-4.02 / (1) * +3 + 5.9 / 7 * 8 - 6'

f.max_expansion_coverage() - f.expansion_coverage() 
set()

平均来说,这比简单策略要快:

average_length_until_full_coverage(GrammarCoverageFuzzer(EXPR_GRAMMAR)) 
50.74

在 CGI 语法上,只需几次迭代就能覆盖所有字母和数字:

f = GrammarCoverageFuzzer(CGI_GRAMMAR, min_nonterminals=5)
while len(f.max_expansion_coverage() - f.expansion_coverage()) > 0:
    print(f.fuzz()) 
%18%d03
%c3%94%7f+cd
%a6%b5%e2%5e%4c-54e01a2
%5eb%7cb_ec%a0+

这种改进也可以在比较随机、仅展开和深度前瞻策略在 CGI 语法上的效果时看到:

average_length_until_full_coverage(TrackingGrammarCoverageFuzzer(CGI_GRAMMAR)) 
211.34

average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(CGI_GRAMMAR)) 
68.64

average_length_until_full_coverage(GrammarCoverageFuzzer(CGI_GRAMMAR)) 
40.38

覆盖率在上下文中

有时候,语法元素不仅仅在一个地方使用。例如,在我们的表达式语法中,<integer> 符号既用于整数数字,也用于浮点数字:

EXPR_GRAMMAR["<factor>"] 
['+<factor>', '-<factor>', '(<expr>)', '<integer>.<integer>', '<integer>']

如上所述,我们的覆盖生产将确保所有 <integer> 扩展(即所有 <digit> 扩展)都被覆盖。然而,单个数字将分布在语法中 <integer> 的所有出现中。如果我们基于覆盖的 fuzzer 产生,例如,1234.567890,我们将对所有数字扩展实现全面覆盖。然而,上面的 <integer>.<integer><factor> 扩展中的 <integer> 将分别只覆盖一部分数字。如果浮点数和整数有不同的函数来读取它们,我们希望每个这样的函数都使用所有数字进行测试;也许我们还想用所有数字测试浮点数的整数部分和小数部分。

忽略符号使用的上下文(在我们的例子中,是 <factor> 上下文中 <integer><digit> 的各种使用)可能是有用的,如果我们假设这个符号的所有出现都被同等对待的话。如果不这样,确保符号的出现系统地被独立于其他出现覆盖的一种方法是将出现分配给一个新的符号,这个新符号是旧符号的副本。我们首先将展示如何手动创建这样的副本,然后是一个自动执行此操作的专用函数。

通过手动扩展语法以实现上下文覆盖

如上所述,通过复制符号以及它们引用的规则,这是一种简单的方法来实现上下文覆盖。例如,我们可以将 <integer>.<integer> 替换为 <integer-1>.<integer-2>,并给 <integer-1><integer-2> 赋予与原始 <integer> 相同的定义。这意味着不仅 <integer> 的所有扩展,还包括 <integer-1><integer-2> 的所有扩展都将被覆盖。

让我们用实际的代码来说明这一点:

dup_expr_grammar = extend_grammar(EXPR_GRAMMAR,
                                  {
                                      "<factor>": ["+<factor>", "-<factor>", "(<expr>)", "<integer-1>.<integer-2>", "<integer>"],
                                      "<integer-1>": ["<digit-1><integer-1>", "<digit-1>"],
                                      "<integer-2>": ["<digit-2><integer-2>", "<digit-2>"],
                                      "<digit-1>":
                                      ["0", "1", "2", "3", "4",
                                          "5", "6", "7", "8", "9"],
                                      "<digit-2>":
                                      ["0", "1", "2", "3", "4",
                                          "5", "6", "7", "8", "9"]
                                  }
                                  ) 
assert is_valid_grammar(dup_expr_grammar) 

如果我们现在在扩展的语法上运行基于覆盖的 fuzzer,我们将覆盖所有数字,包括常规整数中的所有数字,以及浮点数的整数部分和小数部分中的所有数字:

f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="<factor>")
for i in range(10):
    print(f.fuzz()) 
-(43.76 / 8.0 * 5.5 / 6.9 * 6 / 4 + +03)
(90.1 - 1 * 7.3 * 9 + 5 / 8 / 7)
2.8
1.2
10.4
2
4386
7
0
08929.4302

我们看到我们的“预见性”覆盖 fuzzer 如何专门生成覆盖整数部分和小数部分所有数字的浮点数。

通过程序扩展语法以实现上下文覆盖

如果我们想在上下文中增强覆盖范围,手动调整我们的语法可能不是最佳选择,因为任何对语法的更改都必须在所有副本中重复。相反,我们引入了一个函数,它会为我们执行复制。

函数 duplicate_context() 接收一个语法、语法中的一个符号以及该符号的扩展(None 或未提供:符号的所有扩展),并将扩展更改为引用所有原始引用的规则的副本。我们的想法是调用它如下:

dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>") 

并得到与上面手动更改相似的结果。

下面是代码:

from Grammars import new_symbol, unreachable_nonterminals
from GrammarFuzzer import expansion_to_children 
def duplicate_context(grammar: Grammar, 
                      symbol: str,
                      expansion: Optional[Expansion] = None, 
                      depth: Union[float, int] = float('inf')):
  """Duplicate an expansion within a grammar.

 In the given grammar, take the given expansion of the given `symbol`
 (if `expansion` is omitted: all symbols), and replace it with a
 new expansion referring to a duplicate of all originally referenced rules.

 If `depth` is given, limit duplication to `depth` references
 (default: unlimited)
 """
    orig_grammar = extend_grammar(grammar)
    _duplicate_context(grammar, orig_grammar, symbol,
                       expansion, depth, seen={})

    # After duplication, we may have unreachable rules; delete them
    for nonterminal in unreachable_nonterminals(grammar):
        del grammar[nonterminal] 
实现 `_duplicate_context()`

大部分工作都发生在这个辅助函数中。额外的参数 seen 跟踪已经展开的符号,以避免无限递归。

import [copy](https://docs.python.org/3/library/copy.html) 
def _duplicate_context(grammar: Grammar,
                       orig_grammar: Grammar,
                       symbol: str,
                       expansion: Optional[Expansion],
                       depth: Union[float, int],
                       seen: Dict[str, str]) -> None:
  """Helper function for `duplicate_context()`"""

    for i in range(len(grammar[symbol])):
        if expansion is None or grammar[symbol][i] == expansion:
            new_expansion = ""
            for (s, c) in expansion_to_children(grammar[symbol][i]):
                if s in seen:                 # Duplicated already
                    new_expansion += seen[s]
                elif c == [] or depth == 0:   # Terminal symbol or end of recursion
                    new_expansion += s
                else:                         # Nonterminal symbol - duplicate
                    # Add new symbol with copy of rule
                    new_s = new_symbol(grammar, s)
                    grammar[new_s] = copy.deepcopy(orig_grammar[s])

                    # Duplicate its expansions recursively
                    # {**seen, **{s: new_s}} is seen + {s: new_s}
                    _duplicate_context(grammar, orig_grammar, new_s, expansion=None,
                                       depth=depth - 1, seen={**seen, **{s: new_s}})
                    new_expansion += new_s

            grammar[symbol][i] = new_expansion 
```</details>

下面是我们的上述示例,展示了 `duplicate_context()` 的工作原理,现在有了结果。我们让它重复我们的表达式语法中的 `<integer>.<integer>` 展开式,并得到一个新的语法,其中 `<integer-1>` 和 `<integer-2>` 都指的是原始规则的副本:

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>")
dup_expr_grammar 
{'<start>': ['<expr>'],
 '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'],
 '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'],
 '<factor>': ['+<factor>',
  '-<factor>',
  '(<expr>)',
  '<integer-1>.<integer-2>',
  '<integer>'],
 '<integer>': ['<digit><integer>', '<digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-1>': ['<digit-1><integer-1>', '<digit-2>'],
 '<digit-1>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<digit-2>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-2>': ['<digit-3><integer-2>', '<digit-4>'],
 '<digit-3>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<digit-4>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']}

就像上面一样,使用这样的语法进行覆盖模糊测试现在将覆盖数字在多个上下文中的情况。更准确地说,有五个上下文:常规整数,以及单个和多个数字的浮点数的整数部分和小数部分。

f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="<factor>")
for i in range(10):
    print(f.fuzz()) 
(57.5)
2
+-(1 / 3 + 6 / 0 - 7 * 59 * 3 + 8 * 4)
374.88
5.709
0.93
01.1
892.27
219.50
6.636

depth 参数控制重复应该深入到什么程度。将 depth 设置为 1 将只会重复下一个规则:

dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>", depth=1)
dup_expr_grammar 
{'<start>': ['<expr>'],
 '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'],
 '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'],
 '<factor>': ['+<factor>',
  '-<factor>',
  '(<expr>)',
  '<integer-1>.<integer-2>',
  '<integer>'],
 '<integer>': ['<digit><integer>', '<digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-1>': ['<digit><integer-1>', '<digit>'],
 '<integer-2>': ['<digit><integer-2>', '<digit>']}

assert is_valid_grammar(dup_expr_grammar) 

默认情况下,depth 被设置为 \(\infty\),表示无限制的重复。真正的无限制重复可能会对像 EXPR_GRAMMAR 这样的递归语法造成问题,因此 duplicate_context() 被设置为一旦重复就不再重复符号。尽管如此,如果我们将其应用于重复 所有 <expr> 展开式,我们将得到一个包含至少 292 条规则的语法:

dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<expr>") 
assert is_valid_grammar(dup_expr_grammar)
len(dup_expr_grammar) 
292

这为我们提供了近 2000 个展开式来覆盖:

f = GrammarCoverageFuzzer(dup_expr_grammar)
len(f.max_expansion_coverage()) 
1981

再次重复一次会继续增加语法和覆盖要求:

dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<expr>")
duplicate_context(dup_expr_grammar, "<expr-1>")
len(dup_expr_grammar) 
594

f = GrammarCoverageFuzzer(dup_expr_grammar)
len(f.max_expansion_coverage()) 
3994

在这一点上,可以单独覆盖许多上下文——例如,加法中元素的乘法:

dup_expr_grammar["<expr>"] 
['<term-1> + <expr-4>', '<term-5> - <expr-8>', '<term-9>']

dup_expr_grammar["<term-1-1>"] 
['<factor-1-1> * <term-1-1>', '<factor-2-1> / <term-1-1>', '<factor-3-1>']

dup_expr_grammar["<factor-1-1>"] 
['+<factor-1-1>',
 '-<factor-1-1>',
 '(<expr-1-1>)',
 '<integer-1-1>.<integer-2-1>',
 '<integer-3-1>']

结果语法可能不再适用于人类维护;但运行覆盖驱动的模糊器,如 GrammarCoverageFuzzer(),将覆盖所有这些展开式在所有上下文中的情况。如果您想覆盖许多上下文中的元素,那么 duplicate_context() 后跟覆盖驱动的模糊器就是您的朋友。

通过覆盖语法来覆盖代码

有无上下文:通过系统地覆盖所有输入元素,我们在输入中得到了更大的多样性——但这是否意味着程序行为的多样性也更大?毕竟,我们想要覆盖的行为,包括意外行为。

在语法中,有一些元素直接对应于程序功能。处理算术表达式的程序将具有由单个元素直接触发的功能——比如说,由 + 触发的加法功能,由 - 触发的减法,以及由输入中浮点数触发的浮点运算。

这种输入结构与功能之间的联系导致了语法覆盖与代码覆盖之间的强 相关性。换句话说:如果我们能够实现高语法覆盖,这也将导致高代码覆盖。

CGI 语法

让我们在我们的语法之一上探索这种关系——比如说,从 覆盖率章节 中的 CGI 解码器。

创建图表

我们计算一个映射 coverages,其中在 coverages[x] = {y_1, y_2, ...}x 是获得的语法覆盖率,而 y_n 是第 n 次运行获得的代码覆盖率。

我们首先计算最大覆盖率,就像在 覆盖率章节 中所做的那样:

from Coverage import Coverage, cgi_decode 
with Coverage() as cov_max:
    cgi_decode('+')
    cgi_decode('%20')
    cgi_decode('abc')
    try:
        cgi_decode('%?a')
    except:
        pass 

现在,我们运行我们的实验:

f = GrammarCoverageFuzzer(CGI_GRAMMAR, max_nonterminals=2)
coverages: Dict[float, List[float]] = {}

trials = 100
for trial in range(trials):
    f.reset_coverage()
    overall_cov = set()
    max_cov = 30

    for i in range(10):
        s = f.fuzz()
        with Coverage() as cov:
            cgi_decode(s)
        overall_cov |= cov.coverage()

        x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage())
        y = len(overall_cov) * 100 / len(cov_max.coverage())
        if x not in coverages:
            coverages[x] = []
        coverages[x].append(y) 

我们计算 y 值的平均值:

xs = list(coverages.keys())
ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages] 

并创建一个散点图:

%matplotlib inline 
import [matplotlib.pyplot](https://matplotlib.org/) as plt 
import [matplotlib.ticker](https://matplotlib.org/) as mtick 
ax = plt.axes(label="CGI coverage")
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
ax.xaxis.set_major_formatter(mtick.PercentFormatter())

plt.xlim(0, max(xs))
plt.ylim(0, max(ys))

plt.title('Coverage of cgi_decode() vs. grammar coverage')
plt.xlabel('grammar coverage (expansions)')
plt.ylabel('code coverage (lines)') 
Text(0, 0.5, 'code coverage (lines)')

这个散点图显示了语法覆盖率(X 轴)和代码覆盖率(Y 轴)之间的关系。

我们可以看到,语法覆盖率越高,代码覆盖率也越高。

这也转化为约 0.9 的相关系数,表明有很强的相关性:

import [numpy](https://numpy.org/) as np 
np.corrcoef(xs, ys) 
array([[1\.        , 0.81663071],
       [0.81663071, 1\.        ]])

这也由 Spearman 排名相关系数所证实:

from [scipy.stats](https://docs.scipy.org/doc/scipy/reference/) import spearmanr 
spearmanr(xs, ys) 
SignificanceResult(statistic=np.float64(0.9477544699285101), pvalue=np.float64(2.2771918715723359e-10))

URL 语法

让我们在 URL 语法上重复这个实验。我们使用上面的相同代码,除了交换语法和函数:

from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse 
创建图表

再次,我们首先计算最大覆盖率,就像在 覆盖率章节 中所做的那样,进行有根据的猜测:

with Coverage() as cov_max:
    urlparse("http://foo.bar/path")
    urlparse("https://foo.bar#fragment")
    urlparse("ftp://user:password@foo.bar?query=value")
    urlparse("ftps://127.0.0.1/?x=1&y=2") 

接下来是实际的实验:

f = GrammarCoverageFuzzer(URL_GRAMMAR, max_nonterminals=2)
coverages: Dict[float, List[float]] = {}

trials = 100
for trial in range(trials):
    f.reset_coverage()
    overall_cov = set()

    for i in range(20):
        s = f.fuzz()
        with Coverage() as cov:
            urlparse(s)
        overall_cov |= cov.coverage()

        x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage())
        y = len(overall_cov) * 100 / len(cov_max.coverage())
        if x not in coverages:
            coverages[x] = []
        coverages[x].append(y) 
xs = list(coverages.keys())
ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages] 
ax = plt.axes(label="URL coverage")
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
ax.xaxis.set_major_formatter(mtick.PercentFormatter())

plt.xlim(0, max(xs))
plt.ylim(0, max(ys))

plt.title('Coverage of urlparse() vs. grammar coverage')
plt.xlabel('grammar coverage (expansions)')
plt.ylabel('code coverage (lines)') 
Text(0, 0.5, 'code coverage (lines)')

这个散点图显示了语法覆盖率(X 轴)和代码覆盖率(Y 轴)之间的关系。

plt.scatter(xs, ys); 

在这里,我们有一个更强的相关性,超过 .95:

np.corrcoef(xs, ys) 
array([[1\.       , 0.8819171],
       [0.8819171, 1\.       ]])

这也由 Spearman 排名相关系数所证实:

spearmanr(xs, ys) 
SignificanceResult(statistic=np.float64(0.9486832980505139), pvalue=np.float64(0.05131670194948612))

我们得出结论:如果想要获得高代码覆盖率,首先努力实现高语法覆盖率是一个好主意。

这是否总是有效?

对于 CGI 和 URL 示例观察到的相关性,并不适用于每个程序和每个结构。

等价元素

首先,一些语法元素被程序统一处理,即使语法将它们视为不同的符号。例如,在 URL 的主机名中,我们可以有多个字符,尽管 URL 处理程序将它们都视为相同。同样,一旦组成数字,单个数字与数字本身的价值相比,影响较小。因此,在数字或字符上实现多样性并不一定会导致功能上的大差异。

通过区分依赖于上下文的元素,并为每个上下文覆盖替代方案,可以解决这个问题,正如上面所讨论的。关键是确定需要多样性的上下文,以及不需要多样性的上下文。

深度数据处理

其次,数据处理的方式可以产生很大的差异。考虑一个媒体播放器的输入,它由压缩的媒体数据组成。在处理媒体数据时,媒体播放器将表现出行为差异(特别是在其输出中),但这些差异不能通过媒体数据的单个元素直接触发。同样,在一个大型输入集上训练的机器学习器通常不会由输入的单一句法元素控制。(好吧,它可以,但那样的话,我们就不需要机器学习器了。)在这些“深度”数据处理的情况下,在语法中实现结构覆盖率并不一定会引起代码覆盖率。

解决这个问题的方法之一是不仅要实现句法多样性,实际上还要实现语义多样性。在关于有约束的模糊测试的章节中,我们将看到如何具体生成和过滤输入值,特别是数值。这样的生成器也可以在上下文中应用,以便可以单独控制输入的每个方面。此外,在上面的例子中,一些输入部分在结构上仍然可以覆盖:元数据(例如作者名称或媒体播放器的作曲家)或配置数据(例如机器学习器的设置)可以并且应该系统地覆盖;我们将在“配置模糊测试”章节中看到这是如何实现的[ConfigurationFuzzer.html]。

经验教训

  • 快速实现语法覆盖率会导致大量不同输入的产生。

  • 复制语法规则允许在特定的上下文中覆盖元素。

  • 实现语法覆盖率有助于获得代码覆盖率

下一步

从这里,你可以学习如何

  • 使用语法覆盖率系统地测试配置。

背景

确保语法中的每个扩展至少使用一次的想法可以追溯到 Burkhardt [Burkhardt et al, 1967],后来由 Paul Purdom [Purdom et al, 1972]重新发现。语法覆盖率和代码覆盖率之间的关系是由 Nikolas Havrikov 发现的,他在他的博士论文中探讨了这一点。

练习

练习 1:测试 ls

考虑 Unix 的ls程序,它用于列出目录的内容。为调用ls创建一个语法:

LS_EBNF_GRAMMAR: Grammar = {
    '<start>': ['-<options>'],
    '<options>': ['<option>*'],
    '<option>': ['1', 'A', '@',
                 # many more
                 ]
} 
assert is_valid_grammar(LS_EBNF_GRAMMAR) 

使用GrammarCoverageFuzzer测试所有选项。确保使用每个选项集调用ls

使用笔记本 进行练习并查看解决方案。

练习 2:缓存

max_expansion_coverage()函数的值仅取决于语法。修改实现方式,使得每个符号和深度的值在初始化(__init__())时预先计算;这样,max_expansion_coverage()函数就可以简单地从表中查找值。

使用笔记本 进行练习并查看解决方案。

Creative Commons License 本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,均受MIT License许可。最后修改时间:2023-11-11 18:18:06+01:00。引用 • 版权信息

如何引用本作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "语法覆盖率". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "Fuzzing Book", www.fuzzingbook.org/html/GrammarCoverageFuzzer.html. Retrieved 2023-11-11 18:18:06+01:00.

@incollection{fuzzingbook2023:GrammarCoverageFuzzer,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Grammar Coverage},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/GrammarCoverageFuzzer.html}},
    note = {Retrieved 2023-11-11 18:18:06+01:00},
    url = {https://www.fuzzingbook.org/html/GrammarCoverageFuzzer.html},
    urldate = {2023-11-11 18:18:06+01:00}
}

posted @ 2025-12-13 18:14  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报