Skip to content

Commit 74c1391

Browse files
committed
fix: supports registers emitted for different platforms.
JVM signal handlers produce different output / formats depending on the platform. For example an Linux-x64 will of course have different register names, but they will be formated as four per line, while the linux-aarch64 will use one register per line. Links to different `os::print_context` implementations * https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp#L419 * https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_aarch64/os_linux_aarch64.cpp#L341 * https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/bsd_x86/os_bsd_x86.cpp#L601 * https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/bsd_aarch64/os_bsd_aarch64.cpp#L464
1 parent 87823ae commit 74c1391

File tree

4 files changed

+2708
-5
lines changed

4 files changed

+2708
-5
lines changed

dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@
2929
import java.util.regex.Matcher;
3030
import java.util.regex.Pattern;
3131

32+
/**
33+
* Parser for HotSpot JVM fatal error logs ({@code hs_err_pidNNN.log}).
34+
*
35+
* <p>The log is parsed using a linear state machine that mirrors the deterministic section order
36+
* emitted by {@code VMError::report()} in HotSpot. The section order is fixed for a given platform
37+
* but differs across OS/CPU combinations.
38+
*
39+
* <p>If an early sentinel line is absent (e.g. {@code "Native frames:"} is missing because the JVM
40+
* crashed before producing a stack), the state machine will not advance past {@code THREAD} state
41+
* and subsequent sections such as {@code siginfo} and registers will be silently skipped. The
42+
* resulting {@link datadog.crashtracking.dto.CrashLog} will be marked {@code incomplete}.
43+
*/
3244
public final class HotspotCrashLogParser {
3345
private static final DateTimeFormatter ZONED_DATE_TIME_FORMATTER =
3446
DateTimeFormatter.ofPattern("EEE MMM ppd HH:mm:ss yyyy zzz", Locale.getDefault());
@@ -69,9 +81,20 @@ public HotspotCrashLogParser() {
6981
"siginfo:\\s+si_signo:\\s+(\\d+)\\s+\\((\\w+)\\),\\s+si_code:\\s+(\\d+)\\s+\\(([^)]+)\\),\\s+si_addr:\\s+(0x[0-9a-fA-F]+)");
7082
private static final Pattern DYNAMIC_LIBS_PATH_PARSER =
7183
Pattern.compile("^(?:0x)?[0-9a-fA-F]+(?:-[0-9a-fA-F]+)?\\s+(?:[^\\s/\\[]+\\s+)*(.*)$");
72-
// Matches register entries like: RAX=0x..., R8 =0x..., TRAPNO=0x...
84+
// Matches register entries like:
85+
// * RAX=0x..., R8 =0x..., TRAPNO=0x... (x86-64)
86+
// * R0=0x..., R30=0x... (Linux aarch64)
87+
// * x0=0x..., fp=0x..., lr=0x..., sp=0x..., pc=0x... (macOS aarch64)
88+
// Note that register formatting varies by platform, the JVM crash handler can emit one or four
89+
// per line.
7390
private static final Pattern REGISTER_ENTRY_PARSER =
74-
Pattern.compile("([A-Z0-9]+)\\s*=\\s*(0x[0-9a-fA-F]+)");
91+
Pattern.compile("([A-Za-z][A-Za-z0-9]*)\\s*=\\s*(0x[0-9a-fA-F]+)");
92+
// Used for the REGISTERS-state exit condition only: the register name must start the line
93+
// (after optional whitespace). This prevents lines like "Top of Stack: (sp=0x...)" and
94+
// "Instructions: (pc=0x...)" from being mistaken for register entries by REGISTER_ENTRY_PARSER's
95+
// find(), which would otherwise match the lowercase "sp"/"pc" tokens embedded in those lines.
96+
private static final Pattern REGISTER_LINE_START =
97+
Pattern.compile("^\\s*[A-Za-z][A-Za-z0-9]*\\s*=\\s*0x");
7598

7699
private StackFrame parseLine(String line) {
77100
if (line == null || line.isEmpty()) {
@@ -309,9 +332,8 @@ public CrashLog parse(String uuid, String crashLog) {
309332
}
310333
break;
311334
case REGISTERS:
312-
if (!line.isEmpty() && !REGISTER_ENTRY_PARSER.matcher(line).find()) {
313-
// non-empty line with no register entries signals end of section; reprocess in
314-
// STACKTRACE
335+
if (!line.isEmpty() && !REGISTER_LINE_START.matcher(line).find()) {
336+
// non-empty line that does not start with a register entry signals end of section
315337
state = State.STACKTRACE;
316338
} else {
317339
final Matcher m = REGISTER_ENTRY_PARSER.matcher(line);

dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,42 @@ public void testIncompleteParsing() throws Exception {
5252
assertEquals(0, crashLog.error.stack.frames.length);
5353
}
5454

55+
/** macOS aarch64 uses lowercase register names: x0-x28, fp, lr, sp, pc, cpsr */
56+
@Test
57+
public void testRegisterParsingMacosAarch64() throws Exception {
58+
CrashLog crashLog =
59+
new HotspotCrashLogParser()
60+
.parse(
61+
UUID.randomUUID().toString(), readFileAsString("sample-crash-macos-aarch64.txt"));
62+
63+
assertNotNull(crashLog.experimental, "experimental field should be populated");
64+
assertNotNull(crashLog.experimental.ucontext, "ucontext should be populated");
65+
assertEquals("0x0000000000000c55", crashLog.experimental.ucontext.get("x0"));
66+
assertEquals("0x0000000000000000", crashLog.experimental.ucontext.get("x2"));
67+
assertEquals("0x000000016feee210", crashLog.experimental.ucontext.get("fp"));
68+
assertEquals("0x0000000116d0c970", crashLog.experimental.ucontext.get("lr"));
69+
assertEquals("0x000000016feee0f0", crashLog.experimental.ucontext.get("sp"));
70+
assertEquals("0x000000010f8ac794", crashLog.experimental.ucontext.get("pc"));
71+
assertEquals("0x0000000060001000", crashLog.experimental.ucontext.get("cpsr"));
72+
}
73+
74+
/** Linux aarch64 uses uppercase register names: R0-R30 */
75+
@Test
76+
public void testRegisterParsingLinuxAarch64() throws Exception {
77+
CrashLog crashLog =
78+
new HotspotCrashLogParser()
79+
.parse(
80+
UUID.randomUUID().toString(), readFileAsString("sample-crash-linux-aarch64.txt"));
81+
82+
assertNotNull(crashLog.experimental, "experimental field should be populated");
83+
assertNotNull(crashLog.experimental.ucontext, "ucontext should be populated");
84+
assertEquals("0x0000000000000000", crashLog.experimental.ucontext.get("R0"));
85+
assertEquals("0x0000000000000001", crashLog.experimental.ucontext.get("R1"));
86+
assertEquals("0x0000ffff9efa168c", crashLog.experimental.ucontext.get("R30"));
87+
// "Register to memory mapping:" section must NOT be included
88+
assertEquals(31, crashLog.experimental.ucontext.size(), "R0-R30 = 31 registers");
89+
}
90+
5591
private String readFileAsString(String resource) throws IOException {
5692
try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resource)) {
5793
return new BufferedReader(

0 commit comments

Comments
 (0)