我经常使用lexer / parsers而不是解析器组合器,并且看到从未参加过解析类的人问起解析二进制数据的问题。通常,数据不仅是二进制的,而且是上下文相关的。这基本上导致只有一种令牌,即字节令牌。
有人可以解释为什么对于没有参加语法课程但有理论基础的CS学生来说,用词法分析器/语法分析器解析二进制数据如此错误并且足够清晰。
我经常使用lexer / parsers而不是解析器组合器,并且看到从未参加过解析类的人问起解析二进制数据的问题。通常,数据不仅是二进制的,而且是上下文相关的。这基本上导致只有一种令牌,即字节令牌。
有人可以解释为什么对于没有参加语法课程但有理论基础的CS学生来说,用词法分析器/语法分析器解析二进制数据如此错误并且足够清晰。
Answers:
原则上,没有错。
在实践中,
我知道的大多数非文本数据格式都不是上下文无关的,因此不适合常见的解析器生成器。最常见的原因是它们具有长度字段,该字段给出了必须存在生产的次数。
显然,拥有非上下文无关的语言永远不会阻止解析器生成器的使用:我们解析该语言的超集,然后使用语义规则将其简化为我们想要的。如果结果是确定性的,则该方法可用于非文本格式。问题在于,由于大多数二进制格式都允许嵌入任意数据,因此需要找到除计数之外的其他东西来进行同步。长度字段告诉您它是多少。
然后,您可以开始玩一些技巧,例如使一个手动编写的词法分析器能够通过解析器的反馈来处理该问题(例如,C的lex / yacc处理使用这种技巧来处理typedef)。但是接下来我们要讲第二点。
大多数非文本数据格式都非常简单(即使它们不是上下文无关的)。当忽略上述计数时,语言是常规的,最糟糕的是LL1,因此非常适合于手动解析技术。对于手动解析技术(例如递归下降),处理计数很容易。
让我们将数据分为三类:人类可读的数据(通常是文本,从书籍到程序的不同),打算由计算机读取的数据和其他数据(解析图像或声音)。
对于第一类,我们需要将它们处理为计算机可以使用的东西。由于解析器通常可以很好地捕获人类使用的语言,因此我们通常使用解析器。
第三类数据的示例是您要解析为文本的一本书的页面的扫描图像。对于此类,您几乎总是需要有关输入的非常具体的知识,因此您需要一个特定的程序来对其进行解析。标准的解析技术不会带您到这里。
您的问题是关于第二类的:如果我们拥有二进制数据,那么它几乎总是一个计算机程序的产品,该数据将用于另一个计算机程序。这也立即意味着负责创建数据的程序会选择数据的格式。
计算机程序几乎总是以清晰的格式产生数据。如果我们解析某些输入,那么我们实际上是在试图弄清输入的结构。对于二进制数据,此结构通常非常简单,并且易于计算机解析。
换句话说,弄清楚您已经知道输入结构的输入的结构通常会很浪费。由于解析不是免费的(需要时间,并且会增加程序的复杂性),所以这就是为什么对二进制数据使用词法分析器/解析器“太错了”的原因。
LANGSEC: Language-theoretic Security
提供了一个有趣的观点。其中一篇文章讨论了“怪异的机器”:形成系统输入处理功能的已知格式的临时解析器。它们可能实际上未按预期工作。由于错误的假设,有缺陷的机器将在给定的特殊输入的情况下执行意外状态转换,从而执行不可能进行的计算。这将创建攻击向量。使用形式语法将产生可证明正确的算法。
(+ a (* b (- c d)) e)
a b c d - * + e +
。常用的数学符号比Lisp(需要更多的括号,但免费获取可变的arities,因此需要较少的符号来表示使用大arities的表达式)或RPL(从不需要括号)的冗余度更高。这种冗余对于计算机几乎没有用,而在实际中(即数据中可能存在错误的地方),通常将纠错逻辑与数据的功能含义分开,例如使用适用于任意数据的纠错码。字节序列,无论它们代表什么。
二进制格式通常设计为紧凑的,这意味着很少的简单语言功能(例如,可以由上下文无关的语法表示的平衡括号)。此外,将数据的二进制表示形式规范化(即每个对象具有单个表示形式)通常很有用。这排除了有时多余的功能,例如括号。具有较少冗余的另一个不那么值得推荐的结果是,如果每个输入在语法上都是正确的,则可以节省错误检查。
反对非平凡的二进制数据解析器的另一个因素是,许多二进制格式被设计为可以通过低级代码进行解析,这些低级代码喜欢在恒定内存中以很少的开销进行操作。当适用于允许元素的任意重复时,首选固定大小。一种格式,例如TLV,它允许从左到右的解析器首先为对象分配正确的内存量,然后读取对象的表示形式。从左到右进行解析是一个优势,因为它允许在不使用中间缓冲区的情况下对数据进行处理。