8370176: Mixed mode jhsdb jstack cannot unwind call stack with -Xcomp

Co-authored-by: Fei Yang <fyang@openjdk.org>
Reviewed-by: cjplummer, kevinw
This commit is contained in:
Yasumasa Suenaga 2025-11-03 14:25:45 +00:00
parent 8dbefc53a9
commit 045018d5f3
6 changed files with 242 additions and 70 deletions

View File

@ -36,6 +36,11 @@ public interface CFrame {
/** Returns null when no more frames on stack */
public CFrame sender(ThreadProxy th);
/** Find sender frame with given FP and PC */
public default CFrame sender(ThreadProxy th, Address fp, Address pc) {
return sender(th);
}
/** Get the program counter of this frame */
public Address pc();

View File

@ -53,24 +53,36 @@ public final class LinuxAARCH64CFrame extends BasicCFrame {
return fp;
}
@Override
public CFrame sender(ThreadProxy thread) {
AARCH64ThreadContext context = (AARCH64ThreadContext) thread.getContext();
Address rsp = context.getRegisterAsAddress(AARCH64ThreadContext.SP);
return sender(thread, null, null);
}
if ((fp == null) || fp.lessThan(rsp)) {
@Override
public CFrame sender(ThreadProxy thread, Address nextFP, Address nextPC) {
// Check fp
// Skip if both nextFP and nextPC are given - do not need to load from fp.
if (nextFP == null && nextPC == null) {
if (fp == null) {
return null;
}
// Check alignment of fp
if (dbg.getAddressValue(fp) % (2 * ADDRESS_SIZE) != 0) {
return null;
}
}
if (nextFP == null) {
nextFP = fp.getAddressAt(0 * ADDRESS_SIZE);
}
if (nextFP == null) {
return null;
}
// Check alignment of fp
if (dbg.getAddressValue(fp) % (2 * ADDRESS_SIZE) != 0) {
return null;
if (nextPC == null) {
nextPC = fp.getAddressAt(1 * ADDRESS_SIZE);
}
Address nextFP = fp.getAddressAt(0 * ADDRESS_SIZE);
if (nextFP == null || nextFP.lessThanOrEqual(fp)) {
return null;
}
Address nextPC = fp.getAddressAt(1 * ADDRESS_SIZE);
if (nextPC == null) {
return null;
}

View File

@ -47,11 +47,9 @@ public final class LinuxAMD64CFrame extends BasicCFrame {
// personality routine and/or Language Specific Data Area (LSDA).
return new LinuxAMD64CFrame(dbg, cfa, rip, dwarf, true);
}
cfa = ((dwarf.getCFARegister() == AMD64ThreadContext.RBP) &&
!dwarf.isBPOffsetAvailable())
? context.getRegisterAsAddress(AMD64ThreadContext.RBP)
: context.getRegisterAsAddress(dwarf.getCFARegister())
.addOffsetTo(dwarf.getCFAOffset());
cfa = context.getRegisterAsAddress(dwarf.getCFARegister())
.addOffsetTo(dwarf.getCFAOffset());
}
return (cfa == null) ? null
@ -101,51 +99,72 @@ public final class LinuxAMD64CFrame extends BasicCFrame {
}
}
private boolean isValidFrame(Address nextCFA, ThreadContext context) {
return (nextCFA != null) &&
!nextCFA.lessThan(context.getRegisterAsAddress(AMD64ThreadContext.RSP));
private boolean isValidFrame(Address nextCFA, boolean isNative) {
// CFA should never be null.
// nextCFA must be greater than current CFA, if frame is native.
// Java interpreter frames can share the CFA (frame pointer).
return nextCFA != null &&
(!isNative || (isNative && nextCFA.greaterThan(cfa)));
}
private Address getNextCFA(DwarfParser nextDwarf, ThreadContext context) {
private Address getNextCFA(DwarfParser nextDwarf, ThreadContext context, Address senderFP) {
Address nextCFA;
boolean isNative = false;
if (senderFP == null) {
senderFP = cfa.getAddressAt(0); // RBP by default
}
if (nextDwarf == null) { // Next frame is Java
nextCFA = (dwarf == null) ? cfa.getAddressAt(0) // Current frame is Java (Use RBP)
nextCFA = (dwarf == null) ? senderFP // Current frame is Java
: cfa.getAddressAt(dwarf.getBasePointerOffsetFromCFA()); // Current frame is Native
} else { // Next frame is Native
if (dwarf == null) { // Current frame is Java (Use RBP)
nextCFA = cfa.getAddressAt(0);
if (dwarf == null) { // Current frame is Java
nextCFA = senderFP.addOffsetTo(-nextDwarf.getBasePointerOffsetFromCFA());
} else { // Current frame is Native
isNative = true;
int nextCFAReg = nextDwarf.getCFARegister();
if (!dwarf.isBPOffsetAvailable() && // Use RBP as CFA
(nextCFAReg == AMD64ThreadContext.RBP) &&
(nextCFAReg != dwarf.getCFARegister())) {
nextCFA = context.getRegisterAsAddress(AMD64ThreadContext.RBP);
if (nextCFA == null) {
return null;
}
nextCFA = nextCFA.getAddressAt(0);
if (nextCFAReg == AMD64ThreadContext.RBP) {
Address rbp = dwarf.isBPOffsetAvailable() ? cfa.addOffsetTo(dwarf.getBasePointerOffsetFromCFA())
: context.getRegisterAsAddress(AMD64ThreadContext.RBP);
Address nextRBP = rbp.getAddressAt(0);
nextCFA = nextRBP.addOffsetTo(-nextDwarf.getBasePointerOffsetFromCFA());
} else if (nextCFAReg == AMD64ThreadContext.RSP) {
// next RSP should be previous slot of return address.
Address nextRSP = cfa.addOffsetTo(dwarf.getReturnAddressOffsetFromCFA())
.addOffsetTo(ADDRESS_SIZE);
nextCFA = nextRSP.addOffsetTo(nextDwarf.getCFAOffset());
} else {
nextCFA = cfa.getAddressAt(dwarf.getBasePointerOffsetFromCFA());
throw new DebuggerException("Unsupported CFA register: " + nextCFAReg);
}
}
if (nextCFA != null) {
nextCFA = nextCFA.addOffsetTo(-nextDwarf.getBasePointerOffsetFromCFA());
}
}
return isValidFrame(nextCFA, context) ? nextCFA : null;
// Sanity check for next CFA address
try {
nextCFA.getAddressAt(0);
} catch (Exception e) {
// return null if next CFA address is invalid
return null;
}
return isValidFrame(nextCFA, isNative) ? nextCFA : null;
}
@Override
public CFrame sender(ThreadProxy thread) {
public CFrame sender(ThreadProxy th) {
return sender(th, null, null);
}
@Override
public CFrame sender(ThreadProxy th, Address fp, Address pc) {
if (finalFrame) {
return null;
}
ThreadContext context = thread.getContext();
ThreadContext context = th.getContext();
Address nextPC = getNextPC(dwarf != null);
Address nextPC = pc != null ? pc : getNextPC(dwarf != null);
if (nextPC == null) {
return null;
}
@ -168,9 +187,9 @@ public final class LinuxAMD64CFrame extends BasicCFrame {
}
}
Address nextCFA = getNextCFA(nextDwarf, context);
return isValidFrame(nextCFA, context) ? new LinuxAMD64CFrame(dbg, nextCFA, nextPC, nextDwarf, false, fallback)
: null;
Address nextCFA = getNextCFA(nextDwarf, context, fp);
return nextCFA == null ? null
: new LinuxAMD64CFrame(dbg, nextCFA, nextPC, nextDwarf, false, fallback);
}
private DwarfParser createDwarfParser(Address pc) throws DebuggerException {

View File

@ -57,27 +57,40 @@ public final class LinuxRISCV64CFrame extends BasicCFrame {
return fp;
}
@Override
public CFrame sender(ThreadProxy thread) {
RISCV64ThreadContext context = (RISCV64ThreadContext) thread.getContext();
Address rsp = context.getRegisterAsAddress(RISCV64ThreadContext.SP);
return sender(thread, null, null);
}
if ((fp == null) || fp.lessThan(rsp)) {
@Override
public CFrame sender(ThreadProxy thread, Address nextFP, Address nextPC) {
// Check fp
// Skip if both nextFP and nextPC are given - do not need to load from fp.
if (nextFP == null && nextPC == null) {
if (fp == null) {
return null;
}
// Check alignment of fp
if (dbg.getAddressValue(fp) % (2 * ADDRESS_SIZE) != 0) {
return null;
}
}
if (nextFP == null) {
nextFP = fp.getAddressAt(C_FRAME_LINK_OFFSET * ADDRESS_SIZE);
}
if (nextFP == null) {
return null;
}
// Check alignment of fp
if (dbg.getAddressValue(fp) % (2 * ADDRESS_SIZE) != 0) {
return null;
if (nextPC == null) {
nextPC = fp.getAddressAt(C_FRAME_RETURN_ADDR_OFFSET * ADDRESS_SIZE);
}
Address nextFP = fp.getAddressAt(C_FRAME_LINK_OFFSET * ADDRESS_SIZE);
if (nextFP == null || nextFP.lessThanOrEqual(fp)) {
return null;
}
Address nextPC = fp.getAddressAt(C_FRAME_RETURN_ADDR_OFFSET * ADDRESS_SIZE);
if (nextPC == null) {
return null;
}
return new LinuxRISCV64CFrame(dbg, nextFP, nextPC);
}

View File

@ -109,6 +109,8 @@ public class PStack extends Tool {
jthread.printThreadInfoOn(out);
}
while (f != null) {
Address senderFP = null;
Address senderPC = null;
ClosestSymbol sym = f.closestSymbolToPC();
Address pc = f.pc();
out.print(pc + "\t");
@ -125,13 +127,13 @@ public class PStack extends Tool {
out.println();
} else {
// look for one or more java frames
String[] names = null;
JavaNameInfo nameInfo = null;
// check interpreter frame
Interpreter interp = VM.getVM().getInterpreter();
if (interp.contains(pc)) {
names = getJavaNames(th, f.localVariableBase());
nameInfo = getJavaNames(th, f.localVariableBase());
// print codelet name if we can't determine method
if (names == null || names.length == 0) {
if (nameInfo == null || nameInfo.names() == null || nameInfo.names().length == 0) {
out.print("<interpreter> ");
InterpreterCodelet ic = interp.getCodeletContaining(pc);
if (ic != null) {
@ -154,9 +156,9 @@ public class PStack extends Tool {
}
out.println(" (Native method)");
} else {
names = getJavaNames(th, f.localVariableBase());
nameInfo = getJavaNames(th, f.localVariableBase());
// just print compiled code, if can't determine method
if (names == null || names.length == 0) {
if (nameInfo == null || nameInfo.names() == null || nameInfo.names().length == 0) {
out.println("<Unknown compiled code>");
}
}
@ -168,17 +170,21 @@ public class PStack extends Tool {
}
}
// print java frames, if any
if (names != null && names.length != 0) {
// print java frame(s)
for (int i = 0; i < names.length; i++) {
if (i > 0) {
out.print(fillerForAddress);
if (nameInfo != null) {
if (nameInfo.names() != null && nameInfo.names().length != 0) {
// print java frame(s)
for (int i = 0; i < nameInfo.names().length; i++) {
if (i > 0) {
out.print(fillerForAddress);
}
out.println(nameInfo.names()[i]);
}
out.println(names[i]);
}
senderFP = nameInfo.senderFP();
senderPC = nameInfo.senderPC();
}
}
f = f.sender(th);
f = f.sender(th, senderFP, senderPC);
}
} catch (Exception exp) {
exp.printStackTrace();
@ -239,17 +245,22 @@ public class PStack extends Tool {
out.println("\t????????");
}
private String[] getJavaNames(ThreadProxy th, Address fp) {
private static record JavaNameInfo(String[] names, Address senderFP, Address senderPC) {};
private JavaNameInfo getJavaNames(ThreadProxy th, Address fp) {
if (fp == null) {
return null;
}
JavaVFrame[] jvframes = jframeCache.get(th);
if (jvframes == null) return null; // not a java thread
List<String> names = new ArrayList<>(10);
JavaVFrame bottomJVFrame = null;
for (int fCount = 0; fCount < jvframes.length; fCount++) {
JavaVFrame vf = jvframes[fCount];
Frame f = vf.getFrame();
if (fp.equals(f.getFP())) {
bottomJVFrame = vf;
StringBuilder sb = new StringBuilder();
Method method = vf.getMethod();
// a special char to identify java frames in output
@ -280,8 +291,16 @@ public class PStack extends Tool {
names.add(sb.toString());
}
}
String[] res = names.toArray(new String[0]);
return res;
Address senderFP = null;
Address senderPC = null;
if (bottomJVFrame != null) {
Frame senderFrame = bottomJVFrame.getFrame().sender((RegisterMap)bottomJVFrame.getRegisterMap().clone());
senderFP = senderFrame.getFP();
senderPC = senderFrame.getPC();
}
return new JavaNameInfo(names.toArray(new String[0]), senderFP, senderPC);
}
public void setVerbose(boolean verbose) {

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, NTT DATA
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.util.ArrayList;
import java.util.List;
import jdk.test.lib.JDKToolLauncher;
import jdk.test.lib.SA.SATestUtils;
import jdk.test.lib.Utils;
import jdk.test.lib.apps.LingeredApp;
import jdk.test.lib.process.OutputAnalyzer;
/**
* @test
* @bug 8370176
* @requires vm.hasSA
* @requires os.family == "linux"
* @requires os.arch == "amd64"
* @library /test/lib
* @run driver TestJhsdbJstackMixedWithXComp
*/
public class TestJhsdbJstackMixedWithXComp {
private static void runJstack(LingeredApp app) throws Exception {
JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jhsdb");
launcher.addVMArgs(Utils.getFilteredTestJavaOpts("-showversion"));
launcher.addToolArg("jstack");
launcher.addToolArg("--mixed");
launcher.addToolArg("--pid");
launcher.addToolArg(Long.toString(app.getPid()));
ProcessBuilder pb = SATestUtils.createProcessBuilder(launcher);
Process jhsdb = pb.start();
OutputAnalyzer out = new OutputAnalyzer(jhsdb);
jhsdb.waitFor();
String stdout = out.getStdout();
System.out.println(stdout);
System.err.println(out.getStderr());
out.stderrShouldBeEmptyIgnoreVMWarnings();
List<String> targetStackTrace = new ArrayList<>();
boolean inStack = false;
for (String line : stdout.split("\n")) {
if (line.contains("<nep_invoker_blob>")) {
inStack = true;
} else if (inStack && line.contains("-----------------")) {
inStack = false;
break;
}
if (inStack) {
targetStackTrace.add(line);
}
}
boolean found = targetStackTrace.stream()
.anyMatch(l -> l.contains("thread_native_entry"));
if (!found) {
throw new RuntimeException("Test failed!");
}
}
public static void main(String... args) throws Exception {
SATestUtils.skipIfCannotAttach(); // throws SkippedException if attach not expected to work.
LingeredApp app = null;
try {
app = new LingeredAppWithVirtualThread();
LingeredApp.startApp(app, "-Xcomp");
System.out.println("Started LingeredApp with pid " + app.getPid());
runJstack(app);
System.out.println("Test Completed");
} catch (Throwable e) {
e.printStackTrace();
throw e;
} finally {
LingeredApp.stopApp(app);
}
}
}