cmd/internal/objfile: use 'B' code for ELF and PE BSS symbols

For ELF check for the ELF NOBITS section type. Anything in NOBITS
is a BSS symbols.

For PE look for runtime.bss; for a Go program, anything runtime.bss
is a BSS symbol.

Fixes #77301

Change-Id: I15b9af4513a5eb0e8955014a12073c2b56bcefab
Reviewed-on: https://go-review.googlesource.com/c/go/+/739080
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Emmanuel Odeke <emmanuel@orijtech.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Ian Lance Taylor <iant@golang.org>
diff --git a/src/cmd/internal/objfile/elf.go b/src/cmd/internal/objfile/elf.go
index 6988cea..6da3190 100644
--- a/src/cmd/internal/objfile/elf.go
+++ b/src/cmd/internal/objfile/elf.go
@@ -52,7 +52,11 @@
 			case elf.SHF_ALLOC:
 				sym.Code = 'R'
 			case elf.SHF_ALLOC | elf.SHF_WRITE:
-				sym.Code = 'D'
+				if sect.Type == elf.SHT_NOBITS {
+					sym.Code = 'B'
+				} else {
+					sym.Code = 'D'
+				}
 			}
 		}
 		if elf.ST_BIND(s.Info) == elf.STB_LOCAL {
diff --git a/src/cmd/internal/objfile/pe.go b/src/cmd/internal/objfile/pe.go
index e948212..ca8add7 100644
--- a/src/cmd/internal/objfile/pe.go
+++ b/src/cmd/internal/objfile/pe.go
@@ -34,6 +34,23 @@
 
 	imageBase, _ := f.imageBase()
 
+	// When using internal linking we currently put BSS into the
+	// .data section. In order to set the B code correctly,
+	// we find the address of runtime.bss. Anything in a data section
+	// past runtime.bss is a BSS symbol.
+	//
+	// Just checking the section size in insufficient,
+	// as it will be rounded up to the page size.
+	// That is, there can be BSS symbols at the end of the page
+	// that holds the last data symbols.
+	var bssAddr uint32
+	for _, s := range f.pe.Symbols {
+		if s.Name == "runtime.bss" {
+			bssAddr = s.Value
+			break
+		}
+	}
+
 	var syms []Sym
 	for _, s := range f.pe.Symbols {
 		const (
@@ -67,6 +84,12 @@
 			case ch&data != 0:
 				if ch&permW == 0 {
 					sym.Code = 'R'
+				} else if bssAddr > 0 && s.Value >= bssAddr {
+					// Past runtime.bss is BSS.
+					sym.Code = 'B'
+				} else if s.Value >= sect.Size {
+					// Past section size is BSS.
+					sym.Code = 'B'
 				} else {
 					sym.Code = 'D'
 				}
diff --git a/src/cmd/internal/script/scripttest/setup.go b/src/cmd/internal/script/scripttest/setup.go
index f9d650a..07cf8ea 100644
--- a/src/cmd/internal/script/scripttest/setup.go
+++ b/src/cmd/internal/script/scripttest/setup.go
@@ -6,8 +6,10 @@
 package scripttest
 
 import (
+	"errors"
 	"internal/testenv"
 	"io"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -83,7 +85,7 @@
 		exename += ".exe"
 	}
 	toolpath := filepath.Join(testgoroot, toolsub, exename)
-	if err := os.Remove(toolpath); err != nil {
+	if err := os.Remove(toolpath); err != nil && !errors.Is(err, fs.ErrNotExist) {
 		t.Fatalf("removing %s: %v", toolpath, err)
 	}
 	linkOrCopy(t, newtoolpath, toolpath)
diff --git a/src/cmd/nm/nm_test.go b/src/cmd/nm/nm_test.go
index 424ac72..ce1bd7c 100644
--- a/src/cmd/nm/nm_test.go
+++ b/src/cmd/nm/nm_test.go
@@ -116,6 +116,8 @@
 		"runtime.erodata":   "R",
 		"runtime.epclntab":  "R",
 		"runtime.noptrdata": "D",
+		"runtime.bss":       "B",
+		"runtime.noptrbss":  "B",
 	}
 
 	out, err = testenv.Command(t, testenv.Executable(t), exe).CombinedOutput()
diff --git a/src/cmd/nm/script_test.go b/src/cmd/nm/script_test.go
new file mode 100644
index 0000000..025817d
--- /dev/null
+++ b/src/cmd/nm/script_test.go
@@ -0,0 +1,35 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"cmd/internal/script/scripttest"
+	"flag"
+	"internal/testenv"
+	"os"
+	"testing"
+)
+
+//go:generate go test cmd/nm -v -run=TestScript/README --fixreadme
+
+var fixReadme = flag.Bool("fixreadme", false, "if true, update README for script tests")
+
+func TestScript(t *testing.T) {
+	testenv.MustHaveGoBuild(t)
+
+	testExe, testExeErr := os.Executable()
+
+	repls := []scripttest.ToolReplacement{}
+	if testExeErr == nil {
+		repls = []scripttest.ToolReplacement{
+			scripttest.ToolReplacement{
+				ToolName:        "nm",
+				ReplacementPath: testExe,
+				EnvVar:          "GO_NMTEST_IS_NM=1",
+			},
+		}
+	}
+	scripttest.RunToolScriptTest(t, repls, "testdata/script", *fixReadme)
+}
diff --git a/src/cmd/nm/testdata/script/README b/src/cmd/nm/testdata/script/README
new file mode 100644
index 0000000..9ec997a
--- /dev/null
+++ b/src/cmd/nm/testdata/script/README
@@ -0,0 +1,286 @@
+This file is generated by 'go generate'. DO NOT EDIT.
+
+This directory holds test scripts *.txt run during 'go test cmd/<toolname>'.
+To run a specific script foo.txt
+
+	go test cmd/<toolname> -run=Script/^foo$
+
+In general script files should have short names: a few words,
+ not whole sentences.
+The first word should be the general category of behavior being tested,
+often the name of a go subcommand (build, link, compile, ...) or concept (vendor, pattern).
+
+Each script is a text archive (go doc internal/txtar).
+The script begins with an actual command script to run
+followed by the content of zero or more supporting files to
+create in the script's temporary file system before it starts executing.
+
+As an example, run_hello.txt says:
+
+	# hello world
+	go run hello.go
+	stderr 'hello world'
+	! stdout .
+
+	-- hello.go --
+	package main
+	func main() { println("hello world") }
+
+Each script runs in a fresh temporary work directory tree, available to scripts as $WORK.
+Scripts also have access to other environment variables, including:
+
+	GOARCH=<target GOARCH>
+	GOOS=<target GOOS>
+	TMPDIR=$WORK/tmp
+	devnull=<value of os.DevNull>
+	goversion=<current Go version; for example, 1.12>
+
+On Plan 9, the variables $path and $home are set instead of $PATH and $HOME.
+On Windows, the variables $USERPROFILE and $TMP are set instead of
+$HOME and $TMPDIR.
+
+The lines at the top of the script are a sequence of commands to be executed by
+a small script engine configured in .../cmd/internal/script/scripttest/run.go (not the system shell).
+
+Each line of a script is parsed into a sequence of space-separated command
+words, with environment variable expansion within each word and # marking
+an end-of-line comment. Additional variables named ':' and '/' are expanded
+within script arguments (expanding to the value of os.PathListSeparator and
+os.PathSeparator respectively) but are not inherited in subprocess environments.
+
+Adding single quotes around text keeps spaces in that text from being treated
+as word separators and also disables environment variable expansion. Inside a
+single-quoted block of text, a repeated single quote indicates a literal single
+quote, as in:
+
+    'Don''t communicate by sharing memory.'
+
+A line beginning with # is a comment and conventionally explains what is being
+done or tested at the start of a new section of the script.
+
+Commands are executed one at a time, and errors are checked for each command;
+if any command fails unexpectedly, no subsequent commands in the script are
+executed. The command prefix ! indicates that the command on the rest of the
+line (typically go or a matching predicate) must fail instead of succeeding.
+The command prefix ? indicates that the command may or may not succeed, but the
+script should continue regardless.
+
+The command prefix [cond] indicates that the command on the rest of the line
+should only run when the condition is satisfied.
+
+A condition can be negated: [!root] means to run the rest of the line only if
+the user is not root. Multiple conditions may be given for a single command,
+for example, '[linux] [amd64] skip'. The command will run if all conditions are
+satisfied.
+
+When TestScript runs a script and the script fails, by default TestScript shows
+the execution of the most recent phase of the script (since the last # comment)
+and only shows the # comments for earlier phases.
+
+Note also that in reported output, the actual name of the per-script temporary directory
+has been consistently replaced with the literal string $WORK.
+
+The available commands are:
+cat files...
+	concatenate files and print to the script's stdout buffer
+
+
+cc args...
+	run the platform C compiler
+
+
+cd dir
+	change the working directory
+
+
+chmod perm paths...
+	change file mode bits
+
+	Changes the permissions of the named files or directories to
+	be equal to perm.
+	Only numerical permissions are supported.
+
+cmp [-q] file1 file2
+	compare files for differences
+
+	By convention, file1 is the actual data and file2 is the
+	expected data.
+	The command succeeds if the file contents are identical.
+	File1 can be 'stdout' or 'stderr' to compare the stdout or
+	stderr buffer from the most recent command.
+
+cmpenv [-q] file1 file2
+	compare files for differences, with environment expansion
+
+	By convention, file1 is the actual data and file2 is the
+	expected data.
+	The command succeeds if the file contents are identical
+	after substituting variables from the script environment.
+	File1 can be 'stdout' or 'stderr' to compare the script's
+	stdout or stderr buffer.
+
+cp src... dst
+	copy files to a target file or directory
+
+	src can include 'stdout' or 'stderr' to copy from the
+	script's stdout or stderr buffer.
+
+echo string...
+	display a line of text
+
+
+env [key[=value]...]
+	set or log the values of environment variables
+
+	With no arguments, print the script environment to the log.
+	Otherwise, add the listed key=value pairs to the environment
+	or print the listed keys.
+
+exec program [args...] [&]
+	run an executable program with arguments
+
+	Note that 'exec' does not terminate the script (unlike Unix
+	shells).
+
+exists [-readonly] [-exec] file...
+	check that files exist
+
+
+go [args...] [&]
+	run the 'go' program provided by the script host
+
+
+grep [-count=N] [-q] 'pattern' file
+	find lines in a file that match a pattern
+
+	The command succeeds if at least one match (or the exact
+	count, if given) is found.
+	The -q flag suppresses printing of matches.
+
+help [-v] name...
+	log help text for commands and conditions
+
+	To display help for a specific condition, enclose it in
+	brackets: 'help [amd64]'.
+	To display complete documentation when listing all commands,
+	pass the -v flag.
+
+mkdir path...
+	create directories, if they do not already exist
+
+	Unlike Unix mkdir, parent directories are always created if
+	needed.
+
+mv old new
+	rename a file or directory to a new path
+
+	OS-specific restrictions may apply when old and new are in
+	different directories.
+
+replace [old new]... file
+	replace strings in a file
+
+	The 'old' and 'new' arguments are unquoted as if in quoted
+	Go strings.
+
+rm path...
+	remove a file or directory
+
+	If the path is a directory, its contents are removed
+	recursively.
+
+skip [msg]
+	skip the current test
+
+
+sleep duration [&]
+	sleep for a specified duration
+
+	The duration must be given as a Go time.Duration string.
+
+stderr [-count=N] [-q] 'pattern' file
+	find lines in the stderr buffer that match a pattern
+
+	The command succeeds if at least one match (or the exact
+	count, if given) is found.
+	The -q flag suppresses printing of matches.
+
+stdout [-count=N] [-q] 'pattern' file
+	find lines in the stdout buffer that match a pattern
+
+	The command succeeds if at least one match (or the exact
+	count, if given) is found.
+	The -q flag suppresses printing of matches.
+
+stop [msg]
+	stop execution of the script
+
+	The message is written to the script log, but no error is
+	reported from the script engine.
+
+symlink path -> target
+	create a symlink
+
+	Creates path as a symlink to target.
+	The '->' token (like in 'ls -l' output on Unix) is required.
+
+wait 
+	wait for completion of background commands
+
+	Waits for all background commands to complete.
+	The output (and any error) from each command is printed to
+	the log in the order in which the commands were started.
+	After the call to 'wait', the script's stdout and stderr
+	buffers contain the concatenation of the background
+	commands' outputs.
+
+
+
+The available conditions are:
+[GOARCH:*]
+	runtime.GOARCH == <suffix>
+[GODEBUG:*]
+	GODEBUG contains <suffix>
+[GOEXPERIMENT:*]
+	GOEXPERIMENT <suffix> is enabled
+[GOOS:*]
+	runtime.GOOS == <suffix>
+[asan]
+	GOOS/GOARCH supports -asan
+[buildmode:*]
+	go supports -buildmode=<suffix>
+[cgo]
+	host CGO_ENABLED
+[cgolinkext]
+	platform requires external linking for cgo
+[compiler:*]
+	runtime.Compiler == <suffix>
+[cross]
+	cmd/go GOOS/GOARCH != GOHOSTOS/GOHOSTARCH
+[exec:*]
+	<suffix> names an executable in the test binary's PATH
+[fuzz]
+	GOOS/GOARCH supports -fuzz
+[fuzz-instrumented]
+	GOOS/GOARCH supports -fuzz with instrumentation
+[go-builder]
+	GO_BUILDER_NAME is non-empty
+[link]
+	testenv.HasLink()
+[msan]
+	GOOS/GOARCH supports -msan
+[mustlinkext]
+	platform always requires external linking
+[pielinkext]
+	platform requires external linking for PIE
+[race]
+	GOOS/GOARCH supports -race
+[root]
+	os.Geteuid() == 0
+[short]
+	testing.Short()
+[symlink]
+	testenv.HasSymlink()
+[verbose]
+	testing.Verbose()
+
diff --git a/src/cmd/nm/testdata/script/issue77301.txt b/src/cmd/nm/testdata/script/issue77301.txt
new file mode 100644
index 0000000..aa0c9c6
--- /dev/null
+++ b/src/cmd/nm/testdata/script/issue77301.txt
@@ -0,0 +1,20 @@
+# Tests the correct symbol placement for initialized vs uninitialized
+# globals in the respective sections: https://go.dev/issues/77301
+go build -o main
+exec go tool nm main
+stdout 'B main.globalUninitialized'
+stdout 'D main.globalInitialized'
+
+-- go.mod --
+module nm_bss_symbol_check
+
+-- main.go --
+package main
+
+var globalUninitialized int64
+var globalInitialized int64 = 42
+
+func main() {
+    println(globalUninitialized)
+    println(globalInitialized)
+}