【问题标题】:Why does the Ruby debugger return different values than the code at run time?为什么 Ruby 调试器在运行时返回的值与代码不同?
【发布时间】:2017-03-09 13:53:07
【问题描述】:

查看这个简单的 Ruby 类:

require 'byebug'

class Foo
  def run
    byebug

    puts defined?(bar)
    puts bar.inspect

    bar = 'local string'

    puts defined?(bar)
    puts bar.inspect
  end

  def bar
    'string from method'
  end
end

Foo.new.run

在运行此类时,可以在调试器的控制台中观察到以下行为:

    $ ruby byebug.rb

    [2, 11] in /../test.rb
        2:
        3: class Foo
        4:   def run
        5:     byebug
        6:
    =>  7:     puts defined?(bar)
        8:     puts bar.inspect
        9:
       10:     bar = 'local string'
       11:

调试器在断点处返回以下值:

    (byebug) defined?(bar)
    "local-variable"
    (byebug) bar.inspect
    "nil"

请注意——尽管调试器的断点在#5 行——它已经知道在#10 行中定义了一个局部变量bar,它会影响bar 方法,而调试器实际上不是能够再调用bar 方法。目前不知道的是字符串'local string' 将被分配给bar。调试器为bar 返回nil

让我们继续 Ruby 文件中的原始代码并查看其输出:

    (byebug) continue
    method
    "string from method"
    local-variable
    "local string"

在运行时#7 行中,Ruby 仍然知道bar 确实是一个方法,并且它仍然能够在行#8 中调用它。然后行 #10 实际上定义了隐藏具有相同名称的方法的局部变量,并且 tTherefore Ruby 在行 #12#13 中返回预期的那样。

问题:为什么调试器返回的值与原始代码不同?它似乎能够展望未来。这被认为是功能还是错误?是否记录了这种行为?

【问题讨论】:

  • 我不会说它是 Ruby,而是 byebug。如果在赋值之前和之后将其包装在rescue NoMethodErrorputs defined?(bar) 中,您会看到该行之前是"method",之后是"local-variable"
  • @Stefan,我虽然也一样,但事实并非如此。如果您在两者之间单独放置另一条执行某项操作的语句,您仍然会得到相同的结果。
  • @Stefan:是的,有。拿起一份“显微镜下的红宝石”
  • @SergioTulentsev 书明天到货,谢谢! :-)
  • @ndn: 好久没看它了,但我记得它以最详细的方式描述了方法解析和执行。下到操作码。 :)

标签: ruby debugging binding byebug


【解决方案1】:

每当您进入调试会话时,您实际上是在针对代码中该位置的绑定执行eval。下面是一段更简单的代码,它重现了让你发疯的行为:

def make_head_explode
  puts "== Proof bar isn't defined"
  puts defined?(bar)   # => nil

  puts "== But WTF?! It shows up in eval"
  eval(<<~RUBY)
    puts defined?(bar) # => 'local-variable'
    puts bar.inspect   # => nil
  RUBY

  bar = 1
  puts "\n== Proof bar is now defined"
  puts defined?(bar)   # => 'local-variable'
  puts bar.inspect     # => 1
end

make_head_explode 方法被提供给解释器时,它被编译成 YARV 指令、一个本地表,它存储有关方法的参数和方法中的所有局部变量的信息,以及一个包含救援信息的 catch 表。方法(如果存在)。

此问题的根本原因是,由于您在运行时使用 eval 动态编译代码,Ruby 还将包含未设置变量 enry 的本地表传递给 eval。

首先,让我们使用一个非常简单的方法来演示我们所期望的行为。

def foo_boom
  foo         # => NameError
  foo = 1     # => 1
  foo         # => 1
end

我们可以通过使用RubyVM::InstructionSequence.disasm(method) 提取现有方法的 YARV 字节码来检查这一点。请注意,我将忽略跟踪调用以保持说明整洁。

RubyVM::InstructionSequence.disasm(method(:foo_boom)) 的输出更少跟踪:

== disasm: #<ISeq:foo_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] foo
0004 putself
0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave                                                            ( 253)

现在让我们遍历跟踪。

local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] foo

我们可以在这里看到 YARV 已经识别出我们有局部变量 foo,并将其存储在索引 [2] 处的本地表中。如果我们有其他局部变量和参数,它们也会出现在此表中。

接下来我们会在分配之前尝试调用foo 时生成指令:

  0004 putself
  0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
  0008 pop

让我们剖析这里发生的事情。 Ruby 根据以下模式为 YARV 编译函数调用:

  • 推送接收者:putself,指的是顶层函数范围
  • 推送参数:这里没有
  • 调用方法/函数:函数调用(FCALL)到foo

接下来,我们将在获取foo 成为全局变量时进行设置说明:

0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave                                                            ( 253)

关键要点:当 YARV 拥有完整的源代码时,它知道何时定义了局部变量,并将对局部变量的过早调用视为 FCALL,正如您所期望的那样。

现在让我们看看使用eval的“行为不端”版本

def bar_boom
  eval 'bar'     # => nil, but we'd expect an errror
  bar = 1         # => 1
  bar
end

RubyVM::InstructionSequence.disasm(method(:bar_boom)) 的输出更少跟踪:

== disasm: #<ISeq:bar_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] bar
0004 putself
0005 putstring        "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave                                                            ( 264)

我们再次在索引 2 处的 locals 表中看到一个局部变量 bar。我们还有以下 eval 指令:

0004 putself
0005 putstring        "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop

让我们剖析一下这里发生了什么:

  • 推送接收器:又是putself,指的是顶层函数范围
  • 推送参数:“bar”
  • 调用方法/函数:函数调用(FCALL)到eval

之后,我们对bar 进行了我们所期望的标准分配。

0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave                                                            ( 264)

如果这里没有eval,Ruby 会知道将对bar 的调用视为函数调用,这会像我们之前的示例一样被炸毁。但是,由于eval 是动态评估的,并且直到运行时才会生成其代码的指令,因此评估发生在已经确定的指令和本地表的上下文中,该表包含您看到的幻象bar。不幸的是,在这个阶段,Ruby 并不知道 bar 是在 eval 语句“下方”初始化的。

要深入了解,我建议阅读Ruby Under a MicroscopeRuby Hacking Guide's 评估部分。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-21
    • 2014-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    相关资源
    最近更新 更多