11

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

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

但是为什么讨厌第一个经历过 Tclers 的人,并认为这是不好的做法呢?的第一次使用是否expr有一些安全问题?

4

3 回答 3

18

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

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

接下来是:

  • 如果你支持你的expressions,比如 in expr {$a + $b},这些花括号提供的分组会阻止 Tcl 解释器1对打算自行解析的脚本进行解释expr。这意味着在我们的玩具示例中,该expr命令将只看到一个参数 ,$a + $b并将自己执行替换。
  • 上面解释的“双重解析”可能会导致安全问题。

    例如,在下面的代码中

    set a {[exec echo rm -rf $::env(HOME)]}
    set b 2
    expr $a + $b
    

    expr命令本身将解析一个字符串[exec echo rm -rf $::env(HOME)] + 2。它的评估将失败,但到那时,您的主目录的内容应该会消失。(请注意,在稍后对我的答案的编辑中放置echo在前面的rm一种 Tcler 试图保存随机复制粘贴的脖子,因此所写的命令不会调用rm,但如果您echo从中删除,它会调用。)

  • 双重解析抑制了 Tcl 引擎在处理对expr.

1好吧,几乎—— “反斜杠+换行符”序列即使在{...}blocks内部也会被处理。

于 2013-07-03T17:19:56.913 回答
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它们保存起来;未加括号的代码必须读取变量并与表达式的文字部分连接,然后触发exprStk“评估表达式字符串”操作的结果。(字节码的相对数量不是问题;问题是运行时评估。)

对于这些差异的根本性,请考虑设置ato1 || 0bto [exit 1]。在预编译版本的情况下,Tcl 只会尝试将两边都视为要相加的数字(实际上都不是数字;你会得到一个错误)。如果是动态版的话……嗯,你能通过检查来预测吗?

所以你会怎么做?

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

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

代替:

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

(另外,永远不要使用带有 , 的动态表达式iffor否则while会抑制大量编译。)

请记住,对于 Tcl,它(通常)是安全代码是快速代码的情况。您想要快速安全的代码,对吗?

于 2013-07-03T19:59:20.437 回答
4
  • 没有大括号,expr 的参数首先转换为字符串,然后再转换回数字。
  • 如果没有大括号,它们很容易受到与 SQL 注入攻击非常相似的注入攻击。
  • 如果不使用大括号,可能会出现不希望出现的舍入错误。
  • 使用大括号,可以编译表达式。

我基于约翰内斯·库恩 (Johannes Kuhn) 不久前发布的回答,您可以从数字中了解括号函数如何在wiki上更有效,以及其他有关差异的有趣内容,以及在哪里可以省略括号得到你想要的结果。

于 2013-07-03T15:17:55.550 回答