为什么Tcler建议支持你的`表达式?

时间:2013-07-03 15:09:29

标签: tcl

我们可以用两种可能的方式评估这两种表达方式:

   set a 1
   set b 1
   puts [expr $a + $b ]
   puts [expr {$a + $b } ]

但为什么第一个讨厌有经验的Tclers,并认为这是不好的做法? expr的首次使用是否存在安全问题?

3 个答案:

答案 0 :(得分:18)

expr的“问题”是它实现了自己的“迷你语言”,其中包括变量替换(用它们的值替换$a - s)和命令替换(用运行[command ...] s的结果替换那些command事物,所以基本上评估expr $a + $b的过程如下:

  1. Tcl解释器从源字符串中解析出四个单词 - expr$a+$b。由于其中两个单词以$开头,因此会进行变量替换,因此实际上会有expr1+2
  2. 通常,第一个单词被认为是命令的名称,而其他单词是它的参数,因此Tcl解释器查找名为expr的命令,并执行它传递三个参数: 1+2
  3. 实现expr然后连接传递给它的所有参数,将它们解释为字符串,获取字符串1 + 2
  4. 这个字符串然后再次解析 - 这次由expr机器根据自己的规则进行解析,包括变量和命令替换,如前所述。
  5. 以下内容:

    • 如果你brace your expressions,就像expr {$a + $b}一样,这些花括号提供的分组会禁止由{{1}解析的脚本的Tcl解释器 1 进行解释本身。这意味着在我们的玩具示例中,expr命令只会看到一个参数expr,并且会自行执行替换。
    • 上面解释的“双解析”可能会导致安全问题。

      例如,在以下代码中

      $a + $b

      set a {[exec echo rm -rf $::env(HOME)]} set b 2 expr $a + $b 命令本身将解析字符串expr。它的评估将失败,但到那时,你的主目录的内容将被认为已经消失。 (请注意,在稍后编辑我的答案时,有一个Tcler将[exec echo rm -rf $::env(HOME)] + 2放在echo前面,试图保存随机copypasters的脖子,所以写的命令不会调用{{1}但是如果你从中删除rm,它就会。)

    • 双重解析禁止Tcl引擎在处理rm的调用时可以做的某些优化。

    1 好吧,差不多 - "backslash+newline" sequences are still processed even inside {...} blocks

答案 1 :(得分:12)

它肯定存在安全问题。特别是,它会将变量的内容视为表达式片段而不是值,这样就可以解决所有类型的问题。如果这还不够,那么相同的问题也会完全消除性能,因为没有办法为它生成合理的最佳代码:生成的字节码效率会低得多,因为它可以做的就是组装表达式字符串并发送它进行第二轮解析。

让我们深入了解详情

% tcl::unsupported::disassemble lambda {{} {
    set a 1; set b 2
    puts [expr {$a + $b}]
    puts [expr $a + $b]
}}
ByteCode 0x0x50910, refCt 1, epoch 3, interp 0x0x31c10 (epoch 3)
  Source "\n    set a 1; set b 2\n    puts [expr {$a + $b}]\n    put"
  Cmds 6, src 72, inst 65, litObjs 5, aux 0, stkDepth 6, code/src 0.00
  Proc 0x0x6d750, refCt 1, args 0, compiled locals 2
      slot 0, scalar, "a"
      slot 1, scalar, "b"
  Commands 6:
      1: pc 0-4, src 5-11          2: pc 5-18, src 14-20
      3: pc 19-37, src 26-46       4: pc 21-34, src 32-45
      5: pc 38-63, src 52-70       6: pc 40-61, src 58-69
  Command 1: "set a 1"
    (0) push1 0     # "1"
    (2) storeScalar1 %v0    # var "a"
    (4) pop 
  Command 2: "set b 2"
    (5) startCommand +13 1  # next cmd at pc 18
    (14) push1 1    # "2"
    (16) storeScalar1 %v1   # var "b"
    (18) pop 
  Command 3: "puts [expr {$a + $b}]"
    (19) push1 2    # "puts"
  Command 4: "expr {$a + $b}"
    (21) startCommand +14 1     # next cmd at pc 35
    (30) loadScalar1 %v0    # var "a"
    (32) loadScalar1 %v1    # var "b"
    (34) add 
    (35) invokeStk1 2 
    (37) pop 
  Command 5: "puts [expr $a + $b]"
    (38) push1 2    # "puts"
  Command 6: "expr $a + $b"
    (40) startCommand +22 1     # next cmd at pc 62
    (49) loadScalar1 %v0    # var "a"
    (51) push1 3    # " "
    (53) push1 4    # "+"
    (55) push1 3    # " "
    (57) loadScalar1 %v1    # var "b"
    (59) concat1 5 
    (61) exprStk 
    (62) invokeStk1 2 
    (64) done 

特别是,查看地址30-34(expr {$a + $b}的汇编)并与地址49-61(expr $a + $b的汇编)进行比较。最优代码从两个变量中读取值,只有add个; unbraced代码必须读取变量并与表达式的文字部分连接,然后将结果激发到exprStk,这是“评估表达式字符串”操作。 (字节码的相对数量不是问题;问题是运行时评估。)

考虑到这些差异的根本性,请考虑将a设置为1 || 0,将b设置为[exit 1]。在预编译版本的情况下,Tcl将尝试将双方视为要添加的数字(两者都不是实际数字;您将收到错误)。在动态版本的情况下......好吧,你能通过检查来预测吗?

那你做什么?

最佳Tcl代码 总是 限制它执行的表达式的运行时评估量;除非你正在做一些采用用户定义的表达式或类似的东西,否则你通常可以将它归结为零。在拥有的地方,尝试在变量中生成单个表达式字符串,然后使用expr $thatVar而不是更复杂的东西。如果您想要添加数字列表(或通常应用任何运算符来组合它们),请考虑使用此:

set sum [tcl::mathop::+ {*}$theList]

而不是:

set sum [expr [join $theList "+"]]

(另外,从不使用带有ifforwhile的动态表达式,因为这会抑制大量编译。)

请记住,使用Tcl(通常)安全代码是快速代码的情况。你想要快速安全的代码,对吧?

答案 2 :(得分:4)

  • 没有大括号,expr的参数首先转换为字符串,然后再转换回数字。
  • 没有大括号,它们容易发生与SQL注入攻击非常类似的注入攻击。
  • 如果您不使用大括号,则可能会出现您不想要的舍入错误。
  • 使用大括号,可以编译表达式。

我的基础是约翰内斯库恩的answer一段时间后发布的,您可以在数字中找到,wiki上的支撑函数如何更有效,以及其他有趣的东西差异以及你可以省略大括号以实际获得你想要的结果。