FBDeviceControl/Commands/FBDeviceDebuggerCommands.m (94 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBDeviceDebuggerCommands.h"
#import "FBDevice+Private.h"
#import "FBDevice.h"
#import "FBDeviceControlError.h"
#import "FBDeviceDebugServer.h"
/*
Much of the implementation here comes from:
- DTDeviceKitBase which provides implementations of functions for calling AMDevice calls. This is used to establish the 'debugserver' socket, which is then consumed by lldb itself.
- DVTFoundation calls out to the DebuggerLLDB.ideplugin plugin, which provides implementations of lldb debugger clients.
- DebuggerLLDB.ideplugin is the plugin/framework responsible for calling the underlying debugger, there are different objc class implementations depending on what is being debugged.
- These implementations are backed by interfaces to the SBDebugger class (https://lldb.llvm.org/python_api/lldb.SBDebugger.html)
- 'LLDBRPCDebugger' is the class responsible for debugging over an RPC interface, this is used for debugging iOS Devices, since it is running against a remote debugserver on the iOS device, forwarded over a socket on the host. This is backed by the lldb_rpc:SBDebugger class within the lldb codebase.
- DebuggerLLDB uses a combination of calls to the C++ LLDB API and executing command strings here. The bulk of the implementation is in ` -[DBGLLDBLauncher _doRegularDebugWithTarget:usingDebugServer:errTargetString:outError:]`.
- It is possible to trace (using dtrace) the commands that Xcode runs to start a debug session, by observing the 'HandleCommand:' method on the Objc class that wraps SBDebugger.
- To trace the stacks of the command strings that are executed: `sudo dtrace -n 'objc$target:*:*HandleCommand*:entry { ustack(); }' -p XCODE_PID``
- To trace the command strings that are executed: `sudo dtrace -n 'objc$target:*:*HandleCommand*:entry { printf("HandleCommand = %s\n", copyinstr(arg2)); }' -p XCODE_PID``
- To trace stacks of all API calls: `sudo dtrace -n 'objc$target:LLDBRPCDebugger:*:entry { ustack(); }' -p XCODE_PID`
- It is also possible to use lldb's internal logging to see the API calls that it is making. This is done by configuring lldb via adding a line in ~/.lldbinit (e.g `log enable -v -f /tmp/lldb.log lldb api`)
*/
@interface FBDeviceDebuggerCommands ()
@property (nonatomic, weak, readonly) FBDevice *device;
@end
@implementation FBDeviceDebuggerCommands
#pragma mark Public
+ (instancetype)commandsWithTarget:(FBDevice *)target
{
return [[self alloc] initWithDevice:target];
}
- (instancetype)initWithDevice:(FBDevice *)device
{
self = [super init];
if (!self) {
return nil;
}
_device = device;
return self;
}
#pragma mark FBDebuggerCommands Implementation
- (FBFuture<id<FBDebugServer>> *)launchDebugServerForHostApplication:(FBBundleDescriptor *)application port:(in_port_t)port
{
return [[self
lldbBootstrapCommandsForApplicationAtPath:application.path port:port]
onQueue:self.device.workQueue fmap:^(NSArray<NSString *> *commands) {
return [FBDeviceDebugServer debugServerForServiceConnection:[self connectToDebugServer] port:port lldbBootstrapCommands:commands queue:self.device.workQueue logger:self.device.logger];
}];
}
#pragma mark Public
- (FBFutureContext<FBAMDServiceConnection *> *)connectToDebugServer
{
return [[self.device
ensureDeveloperDiskImageIsMounted]
onQueue:self.device.workQueue pushTeardown:^(FBDeveloperDiskImage *diskImage) {
// Xcode 12 and after uses a different service name for the debugserver.
return [self.device startService:(diskImage.xcodeVersion.majorVersion >= 12 ? @"com.apple.debugserver.DVTSecureSocketProxy" : @"com.apple.debugserver")];
}];
}
#pragma mark Private
- (FBFuture<FBBundleDescriptor *> *)applicationBundleForPath:(NSString *)path
{
return [FBFuture resolveValue:^(NSError **error) {
return [FBBundleDescriptor bundleFromPath:path error:error];
}];
}
- (FBFuture<NSArray<NSString *> *> *)lldbBootstrapCommandsForApplicationAtPath:(NSString *)path port:(in_port_t)port
{
return [[self
applicationBundleForPath:path]
onQueue:self.device.workQueue fmap:^(FBBundleDescriptor *bundle) {
return [FBFuture futureWithFutures:@[
[self platformSelectCommand],
[FBDeviceDebuggerCommands localTargetForApplicationAtPath:path],
[self remoteTargetForBundleID:bundle.identifier],
[FBDeviceDebuggerCommands processConnectForPort:port],
]];
}];
}
- (FBFuture<NSString *> *)platformSelectCommand
{
FBDevice *device = self.device;
id<FBControlCoreLogger> logger = self.device.logger;
return [FBFuture
onQueue:self.device.asyncQueue resolveValue:^(NSError **error) {
NSError *innerError = nil;
NSString *developerSymbolsPath = [FBDeveloperDiskImage pathForDeveloperSymbols:device.buildVersion logger:logger error:&innerError];
NSString *platformSelectCommand = @"platform select remote-ios";
if (!developerSymbolsPath) {
[logger logFormat:@"Failed to get developer symbols for %@, no symbolication of system libraries will occur. To fix ensure developer symbols are downloaded from the device using the 'Devices and Simulators' tool within Xcode: %@", device, innerError];
return platformSelectCommand;
}
return [platformSelectCommand stringByAppendingFormat:@" --sysroot '%@'", developerSymbolsPath];
}];
}
+ (FBFuture<NSString *> *)localTargetForApplicationAtPath:(NSString *)path
{
return [FBFuture futureWithResult:[NSString stringWithFormat:@"target create '%@'", path]];
}
- (FBFuture<NSString *> *)remoteTargetForBundleID:(NSString *)bundleID
{
return [[self.device
installedApplicationWithBundleID:bundleID]
onQueue:self.device.asyncQueue map:^(FBInstalledApplication *installedApplication) {
return [NSString stringWithFormat:@"script lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec(\"%@\"))", installedApplication.bundle.path];
}];
}
+ (FBFuture<NSString *> *)processConnectForPort:(in_port_t)port
{
return [FBFuture futureWithResult:[NSString stringWithFormat:@"process connect connect://localhost:%d", port]];
}
@end