Skip to content

Commit

Permalink
On-device symblication
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Dec 6, 2021
1 parent a1067c8 commit 1ff3d64
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 7 deletions.
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ let package = Package(
],
dependencies: [],
targets: [
.target(name: "Meter", dependencies: []),
.target(name: "BinaryImage", dependencies: [], publicHeadersPath: ""),
.target(name: "Meter", dependencies: ["BinaryImage"]),
.testTarget(name: "MeterTests",
dependencies: ["Meter"],
resources: [
.copy("Resources"),
]),
.testTarget(name: "BinaryImageTests",
dependencies: ["BinaryImage"]),
]
)
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Meter is a companion library to [MetricKit](https://developer.apple.com/document
- API for `MXCallStackTree`
- Types for `MXDiagnostic` emulation and coding
- `MXMetricManager`-like interface for unsupported platforms
- On-device symbolication (still under investigation)
- On-device symbolication

## Integration

Expand All @@ -34,7 +34,7 @@ for frame in tree.callStacks[0].frames {

### MXMetricManager and Diagnostics Polyfill

MetricKit's crash reporting facilities require iOS 14, and isn't supported at all for tvOS, watchOS, or macOS. You may want to start moving towards using it as a standard interface between your app and whatever system consumes the data. Meter offers an API that's very similar to MetricKit's `MXMetricManager` to help do just that.
MetricKit's crash reporting facilities require iOS 14/macOS 12.0, and isn't supported at all for tvOS or watchOS. You may want to start moving towards using it as a standard interface between your app and whatever system consumes the data. Meter offers an API that's very similar to MetricKit's `MXMetricManager` to help do just that.

```swift
// adding a subscriber
Expand All @@ -51,17 +51,28 @@ extension MyObject: MeterPayloadSubscriber {
MeterPayloadManager.shared.deliver(payloads)
```

This makes it easier to support the full capabilities of MetricKit when available, and gracefully degrade when they aren't. It can be nice to have a uniform interface to whatever backend system you are using to consume the reports. And, as you move towards an iOS 14 minimum, and as (hopefully) Apple starts supporting MetricKit on more platforms, it will be easier to pull out Meter altogether.
This makes it easier to support the full capabilities of MetricKit when available, and gracefully degrade when they aren't. It can be nice to have a uniform interface to whatever backend system you are using to consume the reports. And, as you move towards a supported minimum, and as (hopefully) Apple starts supporting MetricKit on all platforms, it will be easier to pull out Meter altogether.

Backwards compatibility is still up to you, though. One solution is [ImpactMeterAdapter](https://github.com/ChimeHQ/ImpactMeterAdapter), which uses [Impact](https://github.com/ChimeHQ/Impact) to collect crash data for OSes that don't support `MXCrashDiagnostic`.

If you're also looking for a way to transmit report data to your server, check out [Wells](https://github.com/ChimeHQ/Wells).

### On-Device Symbolication

The stack traces provided by MetricKit, like other types of crash logs, are not symbolicated. There are a bunch of different ways to tackle this problem, but one very convenient option is just to do it as a post-processing step on the device where the crash occurred. The `dlopen` family of APIs could be one approach. It has had some real limitions in the past, particularly on iOS. But, still worth a look.
The stack traces provided by MetricKit, like other types of crash logs, are not symbolicated. There are a bunch of different ways to tackle this problem, but one very convenient option is just to do it as a post-processing step on the device where the crash occurred. This does come, however, with one major drawback. It only works when you still have access to the same binaries. OS updates will almost certainly change all the OS binaries. The same is true for an app update, though in that case, an off-line symbolication step using a dSYM is still doable.

Right now, this functionality is still in the investigation phase. But, if you have thoughts, please get in touch!
Meter provides an API for performing symbolication, via the `Symbolicator` protocol. The core of this protocol should be usable to symbolicate any address, and is not tied to MetricKit. But, the protocol also does include a number of convenience methods that can operate on the various MetricKit classes. The result uses the Meter's wrapper classes to return `Frame` instances which include a `symbolInfo` property. This property can be accessed directly or just re-encoded for transport.

```swift
let symbolicator = DlfcnSymbolicator()
let symPayload = symbolicator.symbolicate(payload: diagnosticPayload)
```

#### DlfcnSymbolicator

This class implements the `Symbolicator` protocol, and uses the functions with `dlfcn.h` to determine symbol/offset. This works, but does have some limitations. First, it relies on looking up symbols in the **currently executing** process, so it will only work if the needed binary is currently loaded. Second, these functions return `<redacted>` for some binary's symbols. I know the symbol information is still accessible from the binary, so it's unclear why this is done.

This is a relatively inexpensive symbolication pass, and is a first effort. Further work here is definitely necessary.

### Suggestions or Feedback

Expand Down
71 changes: 71 additions & 0 deletions Sources/BinaryImage/BinaryImage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#ifndef BinaryImage_h
#define BinaryImage_h

#define _Noescape __attribute__((noescape))
#define ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin")
#define ASSUME_NONNULL_END _Pragma("clang assume_nonnull end")

#include <stdint.h>
#include <stdbool.h>
#include <mach-o/loader.h>

#if __OBJC__
#import <Foundation/Foundation.h>
#endif

ASSUME_NONNULL_BEGIN

typedef struct {
uintptr_t address;
intptr_t loadAddress;
uintptr_t length;
} MachODataRegion;

typedef struct {
const uint8_t* uuid;
intptr_t slide;
MachODataRegion ehFrameRegion;
MachODataRegion unwindInfoRegion;
uintptr_t loadAddress;
uintptr_t textSize;
const char* path;
} MachOData;

#if __LP64__
typedef struct mach_header_64 MachOHeader;
typedef struct section_64 MachOSection;
typedef struct segment_command_64 SegmentCommand;
typedef struct section_64 Section;

const static uint32_t LCSegment = LC_SEGMENT_64;
#else
typedef struct mach_header MachOHeader;
typedef struct section MachOSection;
typedef struct segment_command SegmentCommand;
typedef struct section Section;

const static uint32_t LCSegment = LC_SEGMENT;
#endif

typedef struct {
const char* name;
const MachOHeader* header;
} BinaryImage;

typedef void (^BinaryImageIterator)(BinaryImage image, bool* stop);

void BinaryImageEnumerateLoadedImages(_Noescape BinaryImageIterator iterator);

typedef void (^BinaryImageLoadCommandIterator)(const struct load_command* lcmd, uint32_t cmdCode, bool* stop);

void BinaryImageEnumerateLoadCommands(const MachOHeader* header, _Noescape BinaryImageLoadCommandIterator iterator);

uint8_t* _Nullable BinaryImageGetUUIDBytesFromLoadCommand(const struct load_command* lcmd, uint32_t cmdCode);

#if __OBJC__
NSUUID* _Nullable BinaryuImageUUIDFromLoadCommand(const struct load_command* lcmd, uint32_t cmdCode);
#endif

ASSUME_NONNULL_END

#endif /* BinaryImage_h */
71 changes: 71 additions & 0 deletions Sources/BinaryImage/BinaryImage.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#import <Foundation/Foundation.h>

#include "BinaryImage.h"
#include <mach-o/dyld.h>

void BinaryImageEnumerateLoadedImages(BinaryImageIterator iterator) {
for (uint32_t i = 0; i < _dyld_image_count(); ++i) {
BinaryImage image = {0};

image.name = _dyld_get_image_name(i);
image.header = (MachOHeader*)_dyld_get_image_header(i);

bool stop = false;

iterator(image, &stop);

if (stop) {
break;
}
}
}

void BinaryImageEnumerateLoadCommands(const MachOHeader* header, BinaryImageLoadCommandIterator iterator) {
if (header == NULL) {
return;
}

const uint8_t *ptr = (uint8_t *)header + sizeof(MachOHeader);

for (uint32_t i = 0; i < header->ncmds; ++i) {
const struct load_command* const lcmd = (struct load_command*)ptr;
const uint32_t cmdCode = lcmd->cmd & ~LC_REQ_DYLD;

bool stop = false;

iterator(lcmd, cmdCode, &stop);

if (stop) {
break;
}

ptr += lcmd->cmdsize;
}
}

uint8_t* BinaryImageGetUUIDBytesFromLoadCommand(const struct load_command* lcmd, uint32_t cmdCode) {
if (lcmd == NULL || cmdCode != LC_UUID) {
return NULL;
}

return ((struct uuid_command*)lcmd)->uuid;
}

NSUUID* BinaryuImageUUIDFromLoadCommand(const struct load_command* lcmd, uint32_t cmdCode) {
const uint8_t* bytes = BinaryImageGetUUIDBytesFromLoadCommand(lcmd, cmdCode);

return [[NSUUID alloc] initWithUUIDBytes:bytes];
}

bool ImpactBinaryImageGetData(const MachOHeader* header, const char* path, MachOData* data) {
if (header == NULL || data == NULL) {
return false;
}

const uint8_t *ptr = (uint8_t *)header + sizeof(MachOHeader);

data->loadAddress = (uintptr_t)header;
data->path = path;

return true;
}
28 changes: 27 additions & 1 deletion Sources/Meter/CallStackTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,26 @@ public struct Frame: Codable {
public var address: Int
public var subFrames: [Frame]?

public var symbolInfo: [SymbolInfo]?

public init(binaryUUID: UUID? = nil, offsetIntoBinaryTextSegment: Int? = nil, sampleCount: Int? = nil, binaryName: String? = nil, address: Int, subFrames: [Frame]) {
self.binaryUUID = binaryUUID
self.offsetIntoBinaryTextSegment = offsetIntoBinaryTextSegment
self.sampleCount = sampleCount
self.binaryName = binaryName
self.address = address
self.subFrames = subFrames
self.symbolInfo = nil
}

public init(frame: Frame, symbolInfo: [SymbolInfo], symbolicatedSubFrames: [Frame]?) {
self.binaryUUID = frame.binaryUUID
self.offsetIntoBinaryTextSegment = frame.offsetIntoBinaryTextSegment
self.sampleCount = frame.sampleCount
self.binaryName = frame.binaryName
self.address = frame.address
self.subFrames = symbolicatedSubFrames
self.symbolInfo = symbolInfo
}

public var flattenedFrames: [Frame] {
Expand Down Expand Up @@ -89,6 +102,19 @@ public struct Frame: Codable {
extension Frame: Hashable {
}

public extension Frame {
var symbolicationTarget: SymbolicationTarget? {
guard
let uuid = binaryUUID,
let loadAddress = binaryLoadAddress
else {
return nil
}

return SymbolicationTarget(uuid: uuid, loadAddress: loadAddress, path: nil)
}
}

public class CallStack: NSObject, Codable {
/// Indicates which thread caused the crash
public var threadAttributed: Bool
Expand Down Expand Up @@ -120,7 +146,7 @@ public class CallStackTree: Codable {

#if os(iOS) || os(macOS)
@available(iOS 14.0, macOS 12.0, *)
static func from(callStackTree: MXCallStackTree) throws -> CallStackTreeProtocol {
public static func from(callStackTree: MXCallStackTree) throws -> CallStackTreeProtocol {
let data = callStackTree.jsonRepresentation()

return try from(data: data)
Expand Down
14 changes: 14 additions & 0 deletions Sources/Meter/DiagnosticPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ public class CrashMetaData: MetaDataProtocol, Codable {
self.signal = signal
}

public init(diagnostic: CrashDiagnosticProtocol) {
self.deviceType = diagnostic.metaData.deviceType
self.applicationBuildVersion = diagnostic.metaData.applicationBuildVersion
self.applicationVersion = diagnostic.applicationVersion
self.osVersion = diagnostic.metaData.osVersion
self.platformArchitecture = diagnostic.metaData.platformArchitecture
self.regionFormat = diagnostic.metaData.regionFormat
self.virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo
self.exceptionType = diagnostic.exceptionType?.intValue
self.terminationReason = diagnostic.terminationReason
self.exceptionCode = diagnostic.exceptionCode?.intValue
self.signal = diagnostic.signal?.intValue
}

public func jsonRepresentation() -> Data {
return (try? JSONEncoder().encode(self)) ?? Data()
}
Expand Down
55 changes: 55 additions & 0 deletions Sources/Meter/DlfcnSymbolicator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import BinaryImage

public class DlfcnSymbolicator {
private var pathCache: [String : String]

public init() {
self.pathCache = [:]
}

private lazy var imageMap: [UUID: BinaryImage] = {
var map: [UUID: BinaryImage] = [:]

BinaryImageEnumerateLoadedImages { image, _ in
BinaryImageEnumerateLoadCommands(image.header) { lcmd, code, stop in
switch code {
case UInt32(LC_UUID):
if let uuid = BinaryuImageUUIDFromLoadCommand(lcmd, code) {
map[uuid] = image
}

stop.pointee = true
default:
break
}
}
}

return map
}()
}

extension DlfcnSymbolicator: Symbolicator {
public func symbolicate(address: Int, in target: SymbolicationTarget) -> [SymbolInfo] {
guard let loadedImage = imageMap[target.uuid] else {
return []
}

let loadAddress = Int(bitPattern: loadedImage.header)
let relativeAddress = address - target.loadAddress
let processAddress = loadAddress + relativeAddress
let ptr = UnsafeRawPointer(bitPattern: processAddress)
var info: Dl_info = Dl_info()

guard dladdr(ptr, &info) != 0 else {
return []
}

let offset = processAddress - Int(bitPattern: info.dli_saddr)
let name = String(cString: info.dli_sname)
let symbolInfo = SymbolInfo(symbol: name, offset: offset)

return [symbolInfo]
}
}
Loading

0 comments on commit 1ff3d64

Please sign in to comment.