What version of Go are you using (go version)?

$ go version
go version go1.24.5 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/redacted/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/redacted/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1320095087=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/redacted/code/mess/reproduce-questionmark-build/go.mod'
GOMODCACHE='/home/redacted/go/pkg/mod'
GONOPROXY='github.com/redacted/*'
GONOSUMDB='github.com/redacted/*'
GOOS='linux'
GOPATH='/home/redacted/go'
GOPRIVATE='github.com/redacted/*'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/redacted/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.5'
GOWORK=''
PKG_CONFIG='pkg-config'
uname -sr: Linux 6.8.0-60-generic
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.3 LTS
Release:    22.04
Codename:   jammy
/lib/x86_64-linux-gnu/libc.so.6: GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.10) stable release version 2.35.
gdb --version: GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1

What did you do?

Disassembly of annotated source code in pprof looks wrong when cross compiling for ARMv7. I get question marks and seemingly irrelevant instructions. I've made a repository that documents the issue here: https://github.com/tholok97/pprof-weblist-annotation-on-arm-looks-buggy

What did you expect to see?

I expect pprof weblist to show me relevant assembly instructions when clicking on annotated code, like this screenshot from the repository linked above:

Image

What did you see instead?

Instead I see irrelevant instructions and question marks, like this screenshot from the repository linked above:

Image

Comment From: tholok97

Slack thread in performance channel of Gophers Slack about this: https://gophers.slack.com/archives/C0VP8EF3R/p1752488618013959

Comment From: seankhliao

the pprof issue tracker is at https://github.com/google/pprof

Comment From: cherrymui

These PCs are weird. On ARM, instructions are 4-byte aligned, so PCs should be multiple of 4. But these PCs are mostly odd numbers. If pprof starts decoding an instruction at an odd address, it is not the start of an instruction so it will decode to weird things. It seems pprof sometimes got it right, even with odd PCs, but sometimes not. I can also reproduce on ARM64 (where PCs should also be 4-byte aligned) (without cross compilation).

I think there are two issues: 1. Why the profile record weird PCs. It might have something to do with we subtracting 1 from PCs to get "call PC" from "return PC"? 2. Why the pprof command sometimes got it right even if PCs are weird.

Comment From: egonelbre

For the 2. question, I'm guessing that disasm tooling that pprof calls can better handle the incorrect PC-s. From debugging, pprof calls the Disasm func with start: 0xdda77 on both the Go and the upstream version.

https://github.com/google/pprof/blob/6e76a2b096b5fa52e4bb3f7f7a357bd6e6b3b7b1/internal/binutils/binutils.go#L259

Maybe llvm objdump cross-checks against line numbers or something, i.e finds first PC value that has a line number?

Comment From: prattmic

the pprof issue tracker is at https://github.com/google/pprof

cmd/pprof has its own disassembly implementation, so I could see this being a bug in either upstream pprof or just cmd/pprof: https://github.com/golang/go/blob/9159cd4ec6b0e9475dc9c71c830035c1c4c13483/src/cmd/pprof/pprof.go#L191

Comment From: egonelbre

After debugging a bit, it looks like that go tool objdump properly disassembles and the code difference seems to be how it handles calling Decode.

In objdump there's this code https://github.com/golang/go/blob/master/src/cmd/internal/disasm/disasm.go#L221:

for _, sym := range d.syms {
    symStart := sym.Addr
    symEnd := sym.Addr + uint64(sym.Size)
    relocs := sym.Relocs
    if ... ||
        symEnd <= start || end <= symStart ||
        ... {
        continue
    }

    ...

    d.Decode(symStart, symEnd, relocs, gnuAsm, func(pc, size uint64, file string, line int, text string) {

Whereas pprof disasm https://github.com/golang/go/blob/bb07e55aff3041032ac8c851f4fefbd038783240/src/cmd/pprof/pprof.go#L200 calls Decode directly with:

    d.Decode(start, end, nil, false, func(pc, size uint64, file string, line int, text string) {

So it doesn't care whether start and end point to the correct start/end position of a symbol. And the start address in pprof profile is not a correct symbol start location.

Comment From: gopherbot

Change https://go.dev/cl/688955 mentions this issue: cmd/pprof: respect symbol boundaries during Disasm