go-delve / delve

Delve is a debugger for the Go programming language.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

"empty OP stack" for values running `dlv test`

postcert opened this issue · comments

I originally saw this issue in nvim with dap but was able to reproduce it with the cli.
Running dlv test seems to be building the optimized/inlined version as the output in the cli is missing values.

❯ dlv test ./
Type 'help' for list of commands.
(dlv) break ./emulate_cycle.go:194
Breakpoint 1 set at 0x63cb9d for github.com/postcert/go8.(*Chip8).addRegistersWithCarry() ./emulate_cycle.go:194
(dlv) continue
> github.com/postcert/go8.(*Chip8).addRegistersWithCarry() ./emulate_cycle.go:194 (hits goroutine(36):1 total:1) (PC: 0x63cb9d)
   189:         chip.V[registerX>>8] ^= chip.V[registerY>>4]
   190: }
   191:
   192: func (chip *Chip8) addRegistersWithCarry(opcode uint16, value uint16) {
   193:         // TODO: 255 + 1 = 0, not 256 becuase of type. Failing overflow test below
=> 194:         temp := uint16(chip.V[opcode>>8]) + uint16(chip.V[opcode>>4])
   195:         if temp > 255 {
   196:                 chip.V[0xF] = 1
   197:         } else {
   198:                 chip.V[0xF] = 0
   199:         }
(dlv) stack -full
0  0x000000000063cb9d in github.com/postcert/go8.(*Chip8).addRegistersWithCarry
   at ./emulate_cycle.go:194
       chip = (unreadable empty OP stack)
       opcode = (unreadable empty OP stack)
       value = (unreadable empty OP stack)
...

but when I specify the -N or -l flags for the gcflags, it is able to pull the values.

❯ dlv test ./ --build-flags='-gcflags=all=-N'
Type 'help' for list of commands.
(dlv) break ./emulate_cycle.go:194
Breakpoint 1 set at 0x6ad940,0x6b2865 for (multiple functions)() ./emulate_cycle.go:194
(dlv) continue
> github.com/postcert/go8.TestAddRegistersWithCarry_Success() ./emulate_cycle.go:194 (hits goroutine(50):1 total:1) (PC: 0x6b2865)
Warning: debugging optimized function
   189:         chip.V[registerX>>8] ^= chip.V[registerY>>4]
   190: }
   191:
   192: func (chip *Chip8) addRegistersWithCarry(opcode uint16, value uint16) {
   193:         // TODO: 255 + 1 = 0, not 256 becuase of type. Failing overflow test below
=> 194:         temp := uint16(chip.V[opcode>>8]) + uint16(chip.V[opcode>>4])
   195:         if temp > 255 {
   196:                 chip.V[0xF] = 1
   197:         } else {
   198:                 chip.V[0xF] = 0
   199:         }
(dlv) stack -full
0  0x00000000006b2865 in github.com/postcert/go8.(*Chip8).addRegistersWithCarry
   at ./emulate_cycle.go:194
       chip = ("*github.com/postcert/go8.Chip8")(0xc000142640)
       opcode = 16
       value = 16

1  0x00000000006b2865 in github.com/postcert/go8.TestAddRegistersWithCarry_Success
   at ./emulate_cycle_test.go:254
       t = (*testing.T)(0xc0001c0820)
       chip = ("*github.com/postcert/go8.Chip8")(0xc000142640)
       opcode = 16
       value = 16
       chip = ("*github.com/postcert/go8.Chip8")(0xc000142640)
       temp = 108
...
(dlv) exit
  1. What version of Delve are you using (dlv version)?
❯ dlv version && echo $(which dlv)
Delve Debugger
Version: 1.22.0
Build: $Id: 61ecdbbe1b574f0dd7d7bad8b6a5d564cce981e9 $
/usr/bin/dlv
  1. What version of Go are you using? (go version)?
❯ go version && echo $(which go)
go version go1.21.6 linux/amd64
/usr/bin/go
  1. What operating system and processor architecture are you using?
    Arch Linux - x86_64

  2. What did you do?
    Requested stack -full to see the state variables outside of the current frame.

  3. What did you expect to see?
    The variable information as show when dlv is ran when I manually specify the build flags.

  4. What did you see instead?
    unreadable empty OP stack for all variables out of frame.

I'm at a bit of a loss on why specifying the gcflags parameter again is "fixing" it but at this point in the day I'm not going to make any sense of it.

Maybe because you are using a different value for -gcflags, delve uses -gcflags='all=-N -l' you are passing -gcflags='all=-N' thus enabling inlining. However I don't think this should make a difference in this case. Do you have a way for me to reproduce this?

Yeah, I’ll see if I can break it down to a minimal example and repro it on another machine/docker as well.

It’s just odd to me that the bare ‘delv test’ fails as well assuming it’s setting the desired flags.

Yes, it is odd. Especially given that it is not printing the optimization warning after continue.

I've created repro cases here in docker: https://github.com/postcert/go8/tree/dlv_debug

Was able to reproduce the issue in Arch and Ubuntu containers with go1.21.6 and delve 1.22.0.

cc @dr2chase I think this is a compiler bug. This exact problem doesn't reproduce on go1.22 but similar instances of variables getting an empty location still do, they can be found by doing objdump --dwarf exe | grep -B4 '0 byte', any executable compiled with -gcflags=all=-N -lwill do. For example I can see it for the variablerunExitHookinruntime.runExitHooks`.

I've bisected this and it looks like this was (accidentally?) fixed by 505e50b1e34cdf6dff29615a076e26fb0780d10d https://go.dev/cl/509856. It only happens rarely and I could only find it in return parameters.

Closing this in favor of golang/go bug (also it's almost completely fixed in 1.22).