Skip to content

Commit dfe7b83

Browse files
committed
PROF-14088
1 parent 7d049f2 commit dfe7b83

4 files changed

Lines changed: 265 additions & 11 deletions

File tree

ddprof-lib/src/main/cpp/dwarf.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright 2021 Andrei Pangin
3+
* Copyright 2026, Datadog, Inc.
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
56
* you may not use this file except in compliance with the License.
@@ -105,10 +106,29 @@ DwarfParser::DwarfParser(const char *name, const char *image_base,
105106
_code_align = sizeof(instruction_t);
106107
_data_align = -(int)sizeof(void *);
107108
_linked_frame_size = -1;
109+
_has_z_augmentation = false;
108110

109111
parse(eh_frame_hdr);
110112
}
111113

114+
DwarfParser::DwarfParser(const char *name, const char *image_base,
115+
const char *eh_frame, size_t eh_frame_size) {
116+
_name = name;
117+
_image_base = image_base;
118+
119+
_capacity = 128;
120+
_count = 0;
121+
_table = (FrameDesc *)malloc(_capacity * sizeof(FrameDesc));
122+
_prev = NULL;
123+
124+
_code_align = sizeof(instruction_t);
125+
_data_align = -(int)sizeof(void *);
126+
_linked_frame_size = -1;
127+
_has_z_augmentation = false;
128+
129+
parseEhFrame(eh_frame, eh_frame_size);
130+
}
131+
112132
static constexpr u8 omit_sign_bit(u8 value) {
113133
// each signed flag = unsigned equivalent | 0x80
114134
return value & 0xf7;
@@ -144,6 +164,78 @@ void DwarfParser::parse(const char *eh_frame_hdr) {
144164
}
145165
}
146166

167+
// Parse raw .eh_frame (or __eh_frame on macOS) without a binary-search index.
168+
// Records are CIE/FDE sequences laid out linearly; terminated by a 4-byte zero.
169+
void DwarfParser::parseEhFrame(const char *eh_frame, size_t size) {
170+
const char *section_end = eh_frame + size;
171+
_ptr = eh_frame;
172+
173+
while (_ptr + 4 <= section_end) {
174+
const char *record_start = _ptr;
175+
u32 length = get32();
176+
if (length == 0) {
177+
break; // terminator
178+
}
179+
if (length == 0xffffffff) {
180+
break; // 64-bit DWARF not supported
181+
}
182+
183+
const char *record_end = record_start + 4 + length;
184+
if (record_end > section_end) {
185+
break;
186+
}
187+
188+
u32 cie_id = get32();
189+
190+
if (cie_id == 0) {
191+
// CIE: update code and data alignment factors.
192+
// Layout after cie_id: [1-byte version][augmentation string][code_align LEB][data_align SLEB]...
193+
//
194+
// _has_z_augmentation is overwritten by every CIE encountered. The DWARF spec allows
195+
// multiple CIEs with different augmentation strings in a single .eh_frame section, so
196+
// strictly speaking each FDE should resolve its own CIE via the backward cie_id offset.
197+
// We intentionally skip that: macOS binaries compiled by clang always emit a single CIE
198+
// per module, and this parser is only called for macOS __eh_frame sections. Multi-CIE
199+
// binaries are not produced by the toolchains we target here.
200+
_ptr++; // skip version
201+
_has_z_augmentation = (*_ptr == 'z');
202+
while (_ptr < record_end && *_ptr++) {
203+
} // skip null-terminated augmentation string
204+
if (_ptr + 2 > record_end) {
205+
_ptr = record_end;
206+
continue;
207+
}
208+
_code_align = getLeb();
209+
_data_align = getSLeb();
210+
} else {
211+
// FDE: parse frame description for the covered PC range.
212+
// After cie_id: [pcrel-range-start 4 bytes][range-len 4 bytes][aug-data-len LEB][aug-data][instructions]
213+
// The augmentation data length field (and the data itself) is only present when the CIE
214+
// augmentation string starts with 'z'.
215+
if (_ptr + 8 > record_end) {
216+
break;
217+
}
218+
u32 range_start = (u32)(getPtr() - _image_base);
219+
u32 range_len = get32();
220+
if (_has_z_augmentation) {
221+
_ptr += getLeb(); // skip augmentation data length + data
222+
if (_ptr > record_end) {
223+
break;
224+
}
225+
}
226+
parseInstructions(range_start, record_end);
227+
addRecord(range_start + range_len, DW_REG_FP, LINKED_FRAME_SIZE,
228+
-LINKED_FRAME_SIZE, -LINKED_FRAME_SIZE + DW_STACK_SLOT);
229+
}
230+
231+
_ptr = record_end;
232+
}
233+
234+
if (_count > 1) {
235+
qsort(_table, _count, sizeof(FrameDesc), FrameDesc::comparator);
236+
}
237+
}
238+
147239
void DwarfParser::parseCie() {
148240
u32 cie_len = get32();
149241
if (cie_len == 0 || cie_len == 0xffffffff) {

ddprof-lib/src/main/cpp/dwarf.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Copyright The async-profiler authors
3-
* Copyright 2025, Datadog, Inc.
3+
* Copyright 2025, 2026, Datadog, Inc.
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

@@ -116,6 +116,7 @@ class DwarfParser {
116116
u32 _code_align;
117117
int _data_align;
118118
int _linked_frame_size; // detected from FP-based DWARF entries; -1 = undetected
119+
bool _has_z_augmentation;
119120

120121
const char* add(size_t size) {
121122
const char* ptr = _ptr;
@@ -179,6 +180,7 @@ class DwarfParser {
179180
}
180181

181182
void parse(const char* eh_frame_hdr);
183+
void parseEhFrame(const char* eh_frame, size_t size);
182184
void parseCie();
183185
void parseFde();
184186
void parseInstructions(u32 loc, const char* end);
@@ -189,6 +191,7 @@ class DwarfParser {
189191

190192
public:
191193
DwarfParser(const char* name, const char* image_base, const char* eh_frame_hdr);
194+
DwarfParser(const char* name, const char* image_base, const char* eh_frame, size_t eh_frame_size);
192195

193196
FrameDesc* table() const {
194197
return _table;

ddprof-lib/src/main/cpp/symbols_macos.cpp

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright The async-profiler authors
3+
* Copyright 2026, Datadog, Inc.
34
* SPDX-License-Identifier: Apache-2.0
45
*/
56

@@ -11,8 +12,8 @@
1112
#include <mach-o/dyld.h>
1213
#include <mach-o/loader.h>
1314
#include <mach-o/nlist.h>
14-
#include "symbols.h"
1515
#include "dwarf.h"
16+
#include "symbols.h"
1617
#include "log.h"
1718

1819
UnloadProtection::UnloadProtection(const CodeCache *cc) {
@@ -33,6 +34,8 @@ class MachOParser {
3334
CodeCache* _cc;
3435
const mach_header* _image_base;
3536
const char* _vmaddr_slide;
37+
const char* _eh_frame;
38+
size_t _eh_frame_size;
3639

3740
static const char* add(const void* base, uint64_t offset) {
3841
return (const char*)base + offset;
@@ -124,7 +127,8 @@ class MachOParser {
124127

125128
public:
126129
MachOParser(CodeCache* cc, const mach_header* image_base, const char* vmaddr_slide) :
127-
_cc(cc), _image_base(image_base), _vmaddr_slide(vmaddr_slide) {}
130+
_cc(cc), _image_base(image_base), _vmaddr_slide(vmaddr_slide),
131+
_eh_frame(NULL), _eh_frame_size(0) {}
128132

129133
bool parse() {
130134
if (_image_base->magic != MH_MAGIC_64) {
@@ -139,15 +143,17 @@ class MachOParser {
139143
const symtab_command* symtab = NULL;
140144
const dysymtab_command* dysymtab = NULL;
141145
const section_64* stubs_section = NULL;
142-
bool has_eh_frame = false;
143-
144146
for (uint32_t i = 0; i < header->ncmds; i++) {
145147
if (lc->cmd == LC_SEGMENT_64) {
146148
const segment_command_64* sc = (const segment_command_64*)lc;
147149
if (strcmp(sc->segname, "__TEXT") == 0) {
148150
_cc->updateBounds(_image_base, add(_image_base, sc->vmsize));
149151
stubs_section = findSection(sc, "__stubs");
150-
has_eh_frame = findSection(sc, "__eh_frame") != NULL;
152+
const section_64* eh_frame_section = findSection(sc, "__eh_frame");
153+
if (eh_frame_section != NULL) {
154+
_eh_frame = _vmaddr_slide + eh_frame_section->addr;
155+
_eh_frame_size = eh_frame_section->size;
156+
}
151157
} else if (strcmp(sc->segname, "__LINKEDIT") == 0) {
152158
link_base = _vmaddr_slide + sc->vmaddr - sc->fileoff;
153159
} else if (strcmp(sc->segname, "__DATA") == 0 || strcmp(sc->segname, "__DATA_CONST") == 0) {
@@ -171,11 +177,16 @@ class MachOParser {
171177
}
172178
}
173179

174-
// GCC emits __eh_frame (DWARF CFI); clang emits __unwind_info (compact unwind).
175-
// On aarch64, GCC and clang use different frame layouts, so detecting the
176-
// compiler matters. On x86_64 both use the same layout (no-op distinction).
177-
const FrameDesc& frame = has_eh_frame ? FrameDesc::default_frame : FrameDesc::fallback_default_frame();
178-
_cc->setDwarfTable(NULL, 0, frame);
180+
if (DWARF_SUPPORTED && _eh_frame != NULL && _eh_frame_size > 0) {
181+
DwarfParser dwarf(_cc->name(), _vmaddr_slide, _eh_frame, _eh_frame_size);
182+
_cc->setDwarfTable(dwarf.table(), dwarf.count(), dwarf.detectedDefaultFrame());
183+
} else {
184+
// No __eh_frame (clang compact-unwind-only libraries): fall back to the
185+
// library-wide default frame. On aarch64, clang uses a different frame
186+
// layout from GCC, so we must pass fallback_default_frame() rather than
187+
// letting CodeCache keep its constructor default of FrameDesc::default_frame.
188+
_cc->setDwarfTable(NULL, 0, FrameDesc::fallback_default_frame());
189+
}
179190

180191
return true;
181192
}
@@ -239,4 +250,8 @@ bool Symbols::isLibcOrPthreadAddress(uintptr_t pc) {
239250
return false;
240251
}
241252

253+
void Symbols::clearParsingCaches() {
254+
_parsed_libraries.clear();
255+
}
256+
242257
#endif // __APPLE__
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <gtest/gtest.h>
7+
8+
#include "dwarf.h"
9+
#include "../../main/cpp/gtest_crash_handler.h"
10+
11+
#include <cstdint>
12+
#include <cstring>
13+
#include <vector>
14+
15+
// Test name for crash handler
16+
static constexpr char DWARF_TEST_NAME[] = "DwarfTest";
17+
18+
class DwarfGlobalSetup {
19+
public:
20+
DwarfGlobalSetup() {
21+
installGtestCrashHandler<DWARF_TEST_NAME>();
22+
}
23+
~DwarfGlobalSetup() {
24+
restoreDefaultSignalHandlers();
25+
}
26+
};
27+
static DwarfGlobalSetup dwarf_global_setup;
28+
29+
#if DWARF_SUPPORTED
30+
31+
// Helpers to write little-endian integers into a byte buffer.
32+
static void put32(std::vector<uint8_t>& buf, uint32_t v) {
33+
buf.push_back(static_cast<uint8_t>(v));
34+
buf.push_back(static_cast<uint8_t>(v >> 8));
35+
buf.push_back(static_cast<uint8_t>(v >> 16));
36+
buf.push_back(static_cast<uint8_t>(v >> 24));
37+
}
38+
39+
static void put8(std::vector<uint8_t>& buf, uint8_t v) {
40+
buf.push_back(v);
41+
}
42+
43+
// Append a minimal CIE with "z" augmentation to buf.
44+
// Layout: [4-len=11][4-cie_id=0][1-ver=1][2-aug="z\0"][1-code_align=4][1-data_align=-8][1-ra=30][1-aug_data_len=0]
45+
// Total: 15 bytes.
46+
static void appendCie(std::vector<uint8_t>& buf) {
47+
// body size = cie_id(4) + version(1) + "z\0"(2) + code_align(1) + data_align(1) + ra_col(1) + aug_data_len(1) = 11
48+
put32(buf, 11); // length
49+
put32(buf, 0); // cie_id = 0
50+
put8(buf, 1); // version
51+
put8(buf, 'z'); // augmentation "z"
52+
put8(buf, 0); // null terminator
53+
put8(buf, 4); // code_align = 4 (LEB128)
54+
put8(buf, 0x78); // data_align = -8 (SLEB128: 0x78)
55+
put8(buf, 30); // return address column = 30 (lr)
56+
put8(buf, 0); // augmentation data length = 0 (LEB128)
57+
}
58+
59+
// Append an FDE referencing the CIE at cie_start_offset from the buf start.
60+
// cie_offset in the FDE = offset from FDE's cie_id field to the CIE start.
61+
// range_start is encoded as a 4-byte PC-relative signed integer (pcrel).
62+
// With pcrel=0 and image_base=&buf[0]: range_start = offset_of_pcrel_field_within_buf.
63+
// Layout: [4-len=13][4-cie_offset][4-pcrel=0][4-range_len][1-aug_data_len=0]
64+
// Total: 17 bytes.
65+
static void appendFde(std::vector<uint8_t>& buf, uint32_t cie_start_offset, uint32_t range_len) {
66+
// The FDE's cie_id field will be at buf.size() + 4 (after length field).
67+
uint32_t cie_id_field_offset = static_cast<uint32_t>(buf.size()) + 4;
68+
uint32_t cie_offset = cie_id_field_offset - cie_start_offset;
69+
70+
// body = cie_offset(4) + range_start(4) + range_len(4) + aug_data_len(1) = 13
71+
put32(buf, 13); // length
72+
put32(buf, cie_offset); // cie_offset from this field back to CIE start
73+
put32(buf, 0); // range_start pcrel = 0 (absolute value = field_address - image_base)
74+
put32(buf, range_len); // range_len
75+
put8(buf, 0); // aug data length = 0 (LEB128, for "z" augmentation)
76+
// no DWARF call frame instructions
77+
}
78+
79+
static void appendTerminator(std::vector<uint8_t>& buf) {
80+
put32(buf, 0);
81+
}
82+
83+
// Parse a raw __eh_frame section using the linear DwarfParser constructor.
84+
// image_base is set to buf.data() so that pcrel=0 yields range_start = field_offset_in_buf.
85+
static DwarfParser* parseBuf(const std::vector<uint8_t>& buf) {
86+
const char* base = reinterpret_cast<const char*>(buf.data());
87+
return new DwarfParser("test", base, base, buf.size());
88+
}
89+
90+
TEST(DwarfEhFrame, EmptySection) {
91+
std::vector<uint8_t> buf;
92+
DwarfParser* dwarf = parseBuf(buf);
93+
EXPECT_EQ(dwarf->count(), 0);
94+
free(dwarf->table());
95+
delete dwarf;
96+
}
97+
98+
TEST(DwarfEhFrame, TerminatorOnly) {
99+
std::vector<uint8_t> buf;
100+
appendTerminator(buf);
101+
DwarfParser* dwarf = parseBuf(buf);
102+
EXPECT_EQ(dwarf->count(), 0);
103+
free(dwarf->table());
104+
delete dwarf;
105+
}
106+
107+
TEST(DwarfEhFrame, CieOnly) {
108+
std::vector<uint8_t> buf;
109+
appendCie(buf);
110+
appendTerminator(buf);
111+
DwarfParser* dwarf = parseBuf(buf);
112+
// CIE alone generates no frame records.
113+
EXPECT_EQ(dwarf->count(), 0);
114+
free(dwarf->table());
115+
delete dwarf;
116+
}
117+
118+
TEST(DwarfEhFrame, CieAndFde) {
119+
// CIE starts at offset 0.
120+
std::vector<uint8_t> buf;
121+
appendCie(buf); // 15 bytes
122+
appendFde(buf, 0, 256); // 17 bytes (cie_offset = 19)
123+
appendTerminator(buf); // 4 bytes
124+
ASSERT_EQ(buf.size(), static_cast<size_t>(36));
125+
126+
DwarfParser* dwarf = parseBuf(buf);
127+
// The FDE with no instructions generates two records:
128+
// one from parseInstructions (initial state at range_start) and one sentinel (at range_start + range_len).
129+
EXPECT_EQ(dwarf->count(), 2);
130+
131+
// Table must be in ascending loc order (sorted).
132+
const FrameDesc* table = dwarf->table();
133+
ASSERT_NE(table, nullptr);
134+
EXPECT_LT(table[0].loc, table[1].loc);
135+
136+
// Sentinel record covers the end of the FDE's range.
137+
// range_start = offset of pcrel field in buf = 15+4+4 = 23; range_end = 23+256 = 279.
138+
EXPECT_EQ(table[1].loc, static_cast<uint32_t>(279));
139+
140+
free(dwarf->table());
141+
delete dwarf;
142+
}
143+
144+
#endif // DWARF_SUPPORTED

0 commit comments

Comments
 (0)