Skip to content

runtime: mid-stack inlining vs. FuncForPC #29582

@randall77

Description

@randall77

I've seen lots of instances of code like this:

pc, _, _, ok := runtime.Caller(skip)
if !ok { ... }
fmt.Printf("%s\n", runtime.FuncForPC(pc).Name())

(e.g. https://github.com/stretchr/testify/blob/ffdc059bfe9ce6a4e144ba849dbedead332c6053/mock/mock.go#L317)

This code won't work as expected when mid-stack inlining is enabled. If we have

func main() {
     f() // f will be inlined here
}
func f() {
     g()
}
func g() {
    pc, _, _, _ := runtime.Caller(1)
    fmt.Printf("%s\n", runtime.FuncForPC(pc).Name())
}

Then 1.12 prints main.main, not main.f as would be printed for 1.11.
This is because FuncForPC is specified as follows:

If pc represents multiple functions because of inlining, it returns the *Func describing the outermost function.

The pc returned by runtime.Caller is in main.main on an instruction from the body of main.f. Because of the spec worded as above, we get main.main as a result.

The current "fix" for this is to require everyone to move to using runtime.CallersFrames (see footnote 1) which handles inlined frames correctly. This is the long term solution, but requires users of runtime.Caller{,s} + runtime.FuncForPC to do something active to keep their code working for 1.12.

I propose instead to change how FuncForPC works so the above code does not need a fix. If we instead use this spec:

If pc represents multiple functions because of inlining, it returns the a *Func describing the innermost function, but with an entry of the outermost function.

(outermost -> innermost, plus some weasel words for what Entry() returns for an inlined function.)

This will fix the above code transparently. There are 2 wrinkles to this otherwise elegant solution:

  1. This changes the spec for an exported function.
  2. The runtime doesn't necessarily have a Func to return from FuncForPC for inlined functions.

I don't think number 1 is a huge problem. The only pcs that are exposed by the runtime are those returned by runtime.Caller{,s}. Most inlinings we do in 1.11 are leaf inlinings, which runtime.Caller{,s} can't observe. We also inline functions which call panic. Those runtime.Caller{,s} can observe, but would have to be called from within a deferred handler, which seems rare (and is ugly anyway because things like runtime.gopanic are on the stack).

Of course, someone could get a pc by other means, using unsafe, a profiling library, or who knows where else. Those users might see something unexpected.

I have a CL which will solve number 2. FuncForPC has enough information to construct a Func dynamically for the inlined body. It means FuncForPC might allocate, but otherwise it's not observable by the user (other than the weird "what does Entry() return" problem mentioned above).

footnote 1: The testify package fixed this problem by introducing //go:noinline directives. That doesn't seem to be a good approach in general.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.release-blocker

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions