Skip to content

Commit b09781e

Browse files
authored
Parse __eh_frame DWARF info on macOS for per-PC frame descriptions (#430)
1 parent cf6b341 commit b09781e

6 files changed

Lines changed: 365 additions & 17 deletions

File tree

build-logic/conventions/src/main/kotlin/com/datadoghq/native/scanbuild/ScanBuildPlugin.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ class ScanBuildPlugin : Plugin<Project> {
7272
"scan-build",
7373
"-o", outputDir.absolutePath,
7474
"--force-analyze-debug-code",
75+
// core.StackAddressEscape fires on the intentional setjmp/longjmp pattern in
76+
// StackWalker::walkVM: the jmp_buf address is stored in vm_thread->exception()
77+
// for the duration of the stack walk and is always restored before the function
78+
// returns. The analyzer cannot prove the lifetime is safe, but we can.
79+
"-disable-checker", "core.StackAddressEscape",
7580
"--use-analyzer", analyzer,
7681
"make", "-j$parallelJobs"
7782
)

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,13 @@ void CodeCache::copyFrom(const CodeCache& other) {
9999
_imports_patchable = other._imports_patchable;
100100

101101
_dwarf_table_length = other._dwarf_table_length;
102-
_dwarf_table = new FrameDesc[_dwarf_table_length];
103-
memcpy(_dwarf_table, other._dwarf_table,
104-
_dwarf_table_length * sizeof(FrameDesc));
102+
if (_dwarf_table_length > 0) {
103+
_dwarf_table = (FrameDesc*)malloc(_dwarf_table_length * sizeof(FrameDesc));
104+
memcpy(_dwarf_table, other._dwarf_table,
105+
_dwarf_table_length * sizeof(FrameDesc));
106+
} else {
107+
_dwarf_table = nullptr;
108+
}
105109
_default_frame = other._default_frame;
106110

107111
_capacity = other._capacity;
@@ -120,7 +124,7 @@ CodeCache &CodeCache::operator=(const CodeCache &other) {
120124
}
121125

122126
NativeFunc::destroy(_name);
123-
delete[] _dwarf_table;
127+
free(_dwarf_table);
124128
delete[] _blobs;
125129
free(_build_id);
126130

@@ -135,7 +139,7 @@ CodeCache::~CodeCache() {
135139
}
136140
NativeFunc::destroy(_name);
137141
delete[] _blobs;
138-
delete[] _dwarf_table;
142+
free(_dwarf_table);
139143
free(_build_id); // Free build-id memory
140144
}
141145

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

Lines changed: 100 additions & 2 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.
@@ -92,8 +93,7 @@ FrameDesc FrameDesc::default_frame = {0, DW_REG_FP | LINKED_FRAME_SIZE << 8,
9293
FrameDesc FrameDesc::default_clang_frame = {0, DW_REG_FP | LINKED_FRAME_CLANG_SIZE << 8, -LINKED_FRAME_CLANG_SIZE, -LINKED_FRAME_CLANG_SIZE + DW_STACK_SLOT};
9394
FrameDesc FrameDesc::no_dwarf_frame = {0, DW_REG_INVALID, DW_REG_INVALID, DW_REG_INVALID};
9495

95-
DwarfParser::DwarfParser(const char *name, const char *image_base,
96-
const char *eh_frame_hdr) {
96+
void DwarfParser::init(const char *name, const char *image_base) {
9797
_name = name;
9898
_image_base = image_base;
9999

@@ -105,10 +105,21 @@ DwarfParser::DwarfParser(const char *name, const char *image_base,
105105
_code_align = sizeof(instruction_t);
106106
_data_align = -(int)sizeof(void *);
107107
_linked_frame_size = -1;
108+
_has_z_augmentation = false;
109+
}
108110

111+
DwarfParser::DwarfParser(const char *name, const char *image_base,
112+
const char *eh_frame_hdr) {
113+
init(name, image_base);
109114
parse(eh_frame_hdr);
110115
}
111116

117+
DwarfParser::DwarfParser(const char *name, const char *image_base,
118+
const char *eh_frame, size_t eh_frame_size) {
119+
init(name, image_base);
120+
parseEhFrame(eh_frame, eh_frame_size);
121+
}
122+
112123
static constexpr u8 omit_sign_bit(u8 value) {
113124
// each signed flag = unsigned equivalent | 0x80
114125
return value & 0xf7;
@@ -144,6 +155,93 @@ void DwarfParser::parse(const char *eh_frame_hdr) {
144155
}
145156
}
146157

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

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

Lines changed: 35 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;
@@ -152,6 +153,18 @@ class DwarfParser {
152153
}
153154
}
154155

156+
u32 getLeb(const char* end) {
157+
u32 result = 0;
158+
for (u32 shift = 0; _ptr < end && shift < 32; shift += 7) {
159+
u8 b = *_ptr++;
160+
result |= (u32)(b & 0x7f) << shift;
161+
if ((b & 0x80) == 0) {
162+
return result;
163+
}
164+
}
165+
return result;
166+
}
167+
155168
int getSLeb() {
156169
int result = 0;
157170
for (u32 shift = 0; ; shift += 7) {
@@ -166,6 +179,21 @@ class DwarfParser {
166179
}
167180
}
168181

182+
int getSLeb(const char* end) {
183+
int result = 0;
184+
for (u32 shift = 0; _ptr < end; shift += 7) {
185+
u8 b = *_ptr++;
186+
result |= (b & 0x7f) << shift;
187+
if ((b & 0x80) == 0) {
188+
if ((b & 0x40) != 0 && (shift += 7) < 32) {
189+
result |= ~0U << shift;
190+
}
191+
return result;
192+
}
193+
}
194+
return result;
195+
}
196+
169197
void skipLeb() {
170198
while (*_ptr++ & 0x80) {}
171199
}
@@ -178,7 +206,9 @@ class DwarfParser {
178206
return ptr + offset;
179207
}
180208

209+
void init(const char* name, const char* image_base);
181210
void parse(const char* eh_frame_hdr);
211+
void parseEhFrame(const char* eh_frame, size_t size);
182212
void parseCie();
183213
void parseFde();
184214
void parseInstructions(u32 loc, const char* end);
@@ -189,7 +219,11 @@ class DwarfParser {
189219

190220
public:
191221
DwarfParser(const char* name, const char* image_base, const char* eh_frame_hdr);
222+
DwarfParser(const char* name, const char* image_base, const char* eh_frame, size_t eh_frame_size);
192223

224+
// Ownership of the returned pointer transfers to the caller.
225+
// The caller is responsible for freeing it with free() (not delete[]).
226+
// DwarfParser has no destructor; _table is left dangling after this call is used.
193227
FrameDesc* table() const {
194228
return _table;
195229
}

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

Lines changed: 23 additions & 9 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) {
@@ -139,15 +140,19 @@ class MachOParser {
139140
const symtab_command* symtab = NULL;
140141
const dysymtab_command* dysymtab = NULL;
141142
const section_64* stubs_section = NULL;
142-
bool has_eh_frame = false;
143-
143+
const char* eh_frame = NULL;
144+
size_t eh_frame_size = 0;
144145
for (uint32_t i = 0; i < header->ncmds; i++) {
145146
if (lc->cmd == LC_SEGMENT_64) {
146147
const segment_command_64* sc = (const segment_command_64*)lc;
147148
if (strcmp(sc->segname, "__TEXT") == 0) {
148149
_cc->updateBounds(_image_base, add(_image_base, sc->vmsize));
149150
stubs_section = findSection(sc, "__stubs");
150-
has_eh_frame = findSection(sc, "__eh_frame") != NULL;
151+
const section_64* eh_frame_section = findSection(sc, "__eh_frame");
152+
if (eh_frame_section != NULL) {
153+
eh_frame = _vmaddr_slide + eh_frame_section->addr;
154+
eh_frame_size = eh_frame_section->size;
155+
}
151156
} else if (strcmp(sc->segname, "__LINKEDIT") == 0) {
152157
link_base = _vmaddr_slide + sc->vmaddr - sc->fileoff;
153158
} else if (strcmp(sc->segname, "__DATA") == 0 || strcmp(sc->segname, "__DATA_CONST") == 0) {
@@ -171,11 +176,16 @@ class MachOParser {
171176
}
172177
}
173178

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);
179+
if (DWARF_SUPPORTED && eh_frame != NULL && eh_frame_size > 0) {
180+
DwarfParser dwarf(_cc->name(), _vmaddr_slide, eh_frame, eh_frame_size);
181+
_cc->setDwarfTable(dwarf.table(), dwarf.count(), dwarf.detectedDefaultFrame());
182+
} else {
183+
// No __eh_frame (clang compact-unwind-only libraries): fall back to the
184+
// library-wide default frame. On aarch64, clang uses a different frame
185+
// layout from GCC, so we must pass fallback_default_frame() rather than
186+
// letting CodeCache keep its constructor default of FrameDesc::default_frame.
187+
_cc->setDwarfTable(NULL, 0, FrameDesc::fallback_default_frame());
188+
}
179189

180190
return true;
181191
}
@@ -239,4 +249,8 @@ bool Symbols::isLibcOrPthreadAddress(uintptr_t pc) {
239249
return false;
240250
}
241251

252+
void Symbols::clearParsingCaches() {
253+
_parsed_libraries.clear();
254+
}
255+
242256
#endif // __APPLE__

0 commit comments

Comments
 (0)