FBDeviceControl
FBDeviceControl
is the macOS Framework that implements all functionality associated with iOS Devices within idb
. It can be used independently of idb
as it is a standalone Framework.
This page contains information about the implementation details of how iOS Device access works from macOS.
MobileDevice.framework
β
MobileDevice.framework
is a System Framework for macOS. It's default location is at /System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice
. This is a Private Framework, there is no Apple-provided documentation for it. Most of it's API is of the CoreFoundation style, which means that most of it's surface is made up of plain C symbols with CF
objects passed in or out. As such, this makes integration into an Objective-C Framework (like FBDeviceControl
) relitavely simple, since CF
objects are often Toll-Free Bridged to Objective-C.
At a first glance, this Framework is difficult to deal with since Private APIs are typically easier to work with if they export Objective-C classes. However, most of the usual patterns around how CoreFoundation
APIs do apply. MobileDevice.framework
is used extensively across Apple's macOS Applications that interact with iOS Devices (Finder/iTunes, Xcode, Accessibility Inspector, Apple Configurator 2, Photos). If an Application uses an iOS Device on macOS, it is extremely likely to leverage MobileDevice.framework
.
This wealth of "clients" of MobileDevice.framework
has made it possible to understand how each of the API calls work in terms of their inputs and outputs. As a result, there's a header that defines all of the calls that FBDeviceControl
uses.
FBDeviceControl
aims to use these APIs as far as possible, MobileDevice.framework
is assumed to be the "canonical" way to interact with iOS Devices on macOS. The libimobiledevice
project is essentially a re-implementation of MobileDevice.framework
that runs on different host operating systems. There's also a range of other projects that use MobileDevice.framework
including SDMMobileDevice
, MobileDevice
and pymobiledevice
and more.
There are some exceptions to the level of support for iOS Device operations within MobileDevice.framework
; some functionality is provided via Xcode itself. This means that some device operations will be fully functional without Xcode installed (and xcode-select
'ed), but some functionality is dependent on the presence of an Xcode on the host. Typically the functionality that requires Xcode, is only available through Xcode to begin with. FBDeviceControl
will attempt to defer the loading of Xcode specific functionality until it is used in order to prevent failure for when Xcode is not required. For example if iOS Device listing is only used on a host without Xcode, the device listing functionality will still work.
AMDevice
β
AMDevice
is a CF type defined in MobileDevice.framework
. It is essentially the object that represents a single iOS Device attached to the host. All functions on an AMDevice
start with the prefix AMDevice
and typically take an AMDevice
as the first argument. An AMDevice
will only be present if the device is both booted and attached to the host. It cannot be in DFU or Restore mode.
To operate on an AMDevice
, the phone must "trust" the host. You might recognize this as the "Trust Dialog" that appears when connecting an iOS Device to a Mac. Most calls will fail if this trust exchange has not been performed. The Mac maintains a local store of the cryptographic components that are used as part of this trust exchange. This local store means that an iOS Device will not constantly require trust to be authorized every time that the iOS Device is connected to the same host. If this process did not take place, then it would be completely infeasible for iOS Devices to be used in a Continuous Integration environment.
Since this is such an important component of how to interact with iOS Devices, it is backed by an Objective-C FBAMDevice
class.
The process of discovering devices is asynchronous, which means that fetching the list of AMDevice
s at a snapshot in time is going to be unreliable. FBDeviceControl
instead uses an API within MobileDevice.framework
for recieving an AMDevice
instance every time there is a state change in the availability of AMDevice
instances. This property is also true of Apple's tools that build on top of MobileDevice.framework
; xcodebuild
has a -destination-timeout
parameter and Apple Configurator's cfgutil
has a --timeout
parameter since device discovery is delivered asynchronously. You might never notice this in Xcode's "Devices and Simulators" window, but it is still there too.
AMRestorableDevice
β
AMRestorableDevice
is also part of MobileDevice.framework
, it represents any iOS Device that is attached to the host and powered on. This includes iOS Devices that are booting, booted or in DFU/Restore mode.
This API is extremely limited and only exists to describe devices and perform some actions that are only relevant for devices that are not yet in a booted state. This is used to move a booted device to a DFU/Restore state and back out again. It is useful for understanding why a device that appears to be connected may not be represented by an AMDevice
instance.
This is why FBDeviceControl
can represent a single FBDevice
instance to be backed either or both of an AMDevice
/AMRestorableDevice
, for the sake of full observability into connected iOS Devices. FBDevice
instances will also fail appropriately, for instance when attempting to install an Application to an iOS Device that is in DFU/Restore mode.
AMDServiceConnection
β
This is another CoreFoundation type that represents a connection to a "lockdown service". In order to get basically anything done on an iOS Device, a connection to service running on the iOS Device is required. There are a huge range of these services for a variety of cases. For example com.apple.syslog_relay
is a lockdown service that is used for relaying system logs from the iOS Device to the attached host. You might have seen this used in practice within the "Simulators and Devices" window within Xcode. Connections are created via the AMDeviceSecureStartService
call to an AMDevice
, returning an AMDServiceConnection
type.
There are a number of function calls relevant to this type, dealing with sending and recieving binary data over the connection as if it was a file descriptor. AMDServiceConnection
can optionally contain a "secure context", which is cryptographic information required for sending data over a TLS'd connection. This is why it is important to use AMDServiceConnection(Send|Recieve)
instead of raw read
/write
syscalls, sending unencrypted information over an encrypted transport will mean that the recieving side is incapable of reading the same data. The presence of a "secure context" can be detected at runtime by inspecting the connection value. In more recent iOS versions, some services start requiring usage of encrypted IO so detection and usage of these calls is very important.
It is important to stress that this types is just a "Transport" rather than a "Protocol". Each "lockdown service" may have it's own very different binary protocol for sending and recieving data. In the simple case of com.apple.syslog_relay
, the service just repeatedly sends text over the connection. Other protocols, for instance those used by Instruments are far more complicated. There is no single Protocol that is used by all lockdown services.
There is one exception to this, the "Plist Protocol". This is implemented in AMDServiceConnection(Send|Recieve)Message
calls. This is common across a range of services, such as the screenshot service and SpringBoard service. It's essentially wiring a length header followed by a binary plist, this is used on both the send and receive sides.
AFC
: "Apple File Connection"β
AFC
is also common throughout the MobileDevice.framework
API. This is a set of APIs for file manipulation on an iOS Devices. It is another example of a protocol that wraps an AMDServiceConnection
transport. It has a number of functions for dealing with reading, writing, listing directories, moving, copying and deleting files. These operations are performed on the AFCConnection
type.
On non-jailbroken devices, there is no way of getting an AFC
connection for the entire root filesystem of the Device. Instead, there are different lockdown services corresponding to various containers or sandboxes within the iPhone's operating system. Access to Photos/Media (com.apple.afc
), Application Sandboxes (AMDeviceCreateHouseArrestService
) and crash logs (com.apple.crashreportcopymobile
) are all examples of AFC
services.
Developer Disk Imagesβ
Whilst iOS by default has a number of different lockdown services provided within the base iOS image, not all of the functionality that is available within Xcode is implemented from services in the base OS. In order to augment the iOS Device with additional functionality to an attached macOS host, Xcode bundles a "Developer Disk Image".
This is a regular .dmg
file, which can be opened on macOS. Within this disk image is a number of executables, libraries and plist
s describing the lockdown services that get added to the iOS Device upon mounting them on the device. FBDeviceControl
provides an API for manipulating the disk image manipulation functions in MobileDevice.framework
. It is not possible to mount arbitrary disk image on an iOS Device as every disk image and it's binaries are signed by Apple, only disk images that have genuine Apple signing are allowed to be mounted. There's some evidence within MobileDevice.framework
to suggest that Apple uses these Disk Images internally for developing iOS itself.
The "Developer Disk Image"s may be different for each major/minor iOS verion. This is presumably because the lockdown services interact with APIs on the device that can change in iOS versions. The services that are contained within the Developer Disk Image are associated with functionality that is specific to Xcode, Instruments and Accessibility Inspector. The usage of and availability of these services can change over Xcode versions, so there may be differences in the implementation of clients depending on the protocol version of the service that the client is communicating with.
Manipulating Disk Imagesβ
Any FBDeviceControl
functionality that depends on a service provided by a "Developer Disk Image" will implicitly search for and subsequently mount the most appropriate disk image for the attached iOS device. idb
also provides ways of manually managing the mounting of these disk images through the "disk image container" of file commands:
# List all of the available disk images that are present within the current Xcode, as well as the current mounted image/s (if present).
# No images are mounted as the "mounted" path is empty.
$ idb file ls --disk-images .
15.0/DeveloperDiskImage.dmg
12.1/DeveloperDiskImage.dmg
10.0/DeveloperDiskImage.dmg
9.2/DeveloperDiskImage.dmg
9.1/DeveloperDiskImage.dmg
14.2/DeveloperDiskImage.dmg
13.4/DeveloperDiskImage.dmg
11.3/DeveloperDiskImage.dmg
13.0/DeveloperDiskImage.dmg
12.2/DeveloperDiskImage.dmg
10.1/DeveloperDiskImage.dmg
13.1/DeveloperDiskImage.dmg
11.0/DeveloperDiskImage.dmg
14.3/DeveloperDiskImage.dmg
13.5/DeveloperDiskImage.dmg
11.4/DeveloperDiskImage.dmg
12.3/DeveloperDiskImage.dmg
10.2/DeveloperDiskImage.dmg
14.0/DeveloperDiskImage.dmg
13.2/DeveloperDiskImage.dmg
11.1/DeveloperDiskImage.dmg
14.4/DeveloperDiskImage.dmg
13.6/DeveloperDiskImage.dmg
12.0/DeveloperDiskImage.dmg
14.5/DeveloperDiskImage.dmg
12.4/DeveloperDiskImage.dmg
10.3/DeveloperDiskImage.dmg
14.1/DeveloperDiskImage.dmg
11.2/DeveloperDiskImage.dmg
13.3/DeveloperDiskImage.dmg
9.0/DeveloperDiskImage.dmg
9.3/DeveloperDiskImage.dmg
13.7/DeveloperDiskImage.dmg
$ idb file ls --disk-images mounted
# Mounting for the current iOS Version of the attached device (iOS 15.0 succeeds).
$ idb file mv --disk-images 15.0/DeveloperDiskImage.dmg mounted
# Now we can see that the image is mounted.
$ idb file ls --disk-images mounted
15.0/DeveloperDiskImage.dmg
# Unmount the disk image by rm'ing it. The disk image is kept intact on the host, but is unmounted from the device.
$ idb file rm --disk-images mounted/15.0/DeveloperDiskImage.dmg
$ idb file ls --disk-images mounted
Instruments Serviceβ
The "Instruments Service", which is a service within the "Developer Disk Image" is a very important one with respect to iOS Device automation. Since Instruments.app
and the instruments
commandline offers a lot of functionality for launching and profiling Applications and iOS Devices, it is integral to tasks such as app launching and process listing on iOS Devices.
The client-side implementation of this protocol is provided via DTXConnectionServices
, with a provisional re-implementation within FBDeviceControl
. There are more details about the makeup of this protocol within the ios_instruments_client
project.
Video Encodingβ
One of the key features of FBDeviceControl
is the ability to stream the iPhone's screen to the host over USB. You might be familiar with this within QuickTime's "Screen Recording" feature, where you can record video from a connected iOS Device to an .mp4
file on your Mac.
Access to this is provided via AVFoundation
as a "Capture Device". Usage of "Capture Devices" on macOS also requires that the hosting process (The process using FBDeviceControl
) has system-level permissions for this on more recent versions of macOS. With a Capture Device for the iOS Device screen, it is then possible to create a "Capture Session" with the device. A "Capture Session" can thent be established, with relevant configuration so that frames are received in the most optimal format for the consumer. FBDeviceControl
receives frame samples from the device and these samples are either re-encoded or not before being passed on to a stream of data.
FBDeviceControl
supports writing to an mp4
video file or as a stream of encoded data. For streams of data, these can be passed to other Applications for cases like webRTC or HTTP Live-Streaming.
Debugging Applicationsβ
idb
(via FBDeviceControl
) has support for launching a debugserver
on an iOS Device. This enables debugging Apps on iOS Devices, without having to run via Xcode. There are some important considerations in terms of what is possible:
- Only
lldb
may be used as the debugger client. - Only "Developer Signed" Applications may be debugged. Debugging Enterprise signed or App Store Apps is not supported or allowed by iOS.
- Attaching to an already-launched Application is not supported or allowed by iOS. Debugged Applications must be launched by the debugger.
Debugging an app on an iOS Device isn't quite as simple as an lldb
one-liner. However, the only process that changes is how to start a debugging session. The process for a device is as follows:
- The Application to debug is installed on the iOS Device. A copy of the Application is also present/saved on the host attached to the device. The copy of the App is required on the host as this is needed for
lldb
to be able to symbolicate symbols within the debugged app. - A Developer Disk Image for the current device is mounted. This is required as the
debugserver
lockdown service is contained within the Developer Disk Image. - The
debugserver
service is started via lockdown. This results in a bi-directional socket stream thatlldb
can talk to. This is a remote debugging server, using the gdb protocol. - The
debugserver
service must be made accessible tolldb
. This is done by wrapping thedebugserver
connection within a socket, so that it can be connected to outside of the process that obtained thedebugserver
socket. lldb
is started and then connects to thedebugserver
via the exposed socket. A series of commands are sent tolldb
so that it understands how to connect to the remote debugserver and how to talk to it.lldb
can now launch and debug the application.
A "debuggable app"β
Aside from the previously-mentioned requirement that an App is only debuggable if it is "Developer Signed", there is an additional contraint around App debuggability; a copy of the Application must be persisted on the host attached to the device. This is required so that lldb
has a local copy of the Application available to it for the purpose of symbolication.
idb
can do this via the --make-debuggable
flag upon an idb install
:
# Install an app, also persisting it to disk so that idb can later find it when launching a debugserver
$ idb install --make-debuggable /path/to/SomeApp.app
You can also confirm whether an app is debuggable or not in the output of idb list-apps
:
$ idb list-apps | grep SomeApp
com.foo.SomeApp | SomeApp | user_development | no archs available | Unknown | Debuggable | pid=None
If the application is listed as "Debuggable" then a debugserver
can be started against it.
Starting a debugserver
β
A debugserver
can then be started for any "Debuggable" App. This is done with the idb debugserver
command.
# Start the debugserver, a list of bootstrapping commands are returned on stdout
$ idb debugserver start com.foo.SomeApp
platform select remote-ios --sysroot '/Users/Someone/Library/Developer/Xcode/iOS DeviceSupport/15.2.1 (19C63) arm64e/Symbols'
target create '/private/tmp/idb-applications/com.foo.SomeApp/SomeApp.app'
script lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec("/private/var/containers/Bundle/Application/64B4DDB3-1049-458C-AA0B-F56AF4DCF0E0/SomeApp.app"))
process connect connect://localhost:10881
The bootstrapping commands returned can then be used to initialize a debug session via lldb
.
Bootstrapping via lldb
β
# Start lldb and pass the bootrapping commands from above
$ lldb
(lldb) platform select remote-ios --sysroot '/Users/Someone/Library/Developer/Xcode/iOS DeviceSupport/15.2.1 (19C63) arm64e/Symbols'
Platform: remote-ios
Connected: no
SDK Path: "/Users/Someone/Library/Developer/Xcode/iOS DeviceSupport/15.2.1 (19C63) arm64e/Symbols"
(lldb) target create '/private/tmp/idb-applications/com.foo.SomeApp/SomeApp.app'
script lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec("/private/var/containers/Bundle/Application/64B4DDB3-1049-458C-AA0B-F56AF4DCF0E0/SomeApp.app"))Current executable set to '/private/tmp/idb-applications/com.foo.SomeApp/SomeApp.app' (arm64).
(lldb) script lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec("/private/var/containers/Bundle/Application/64B4DDB3-1049-458C-AA0B-F56AF4DCF0E0/SomeApp.app"))
True
(lldb) process connect connect://localhost:10881
(lldb) r
Process 10416 launched: '/private/tmp/idb-applications/com.foo.SomeApp/SomeApp.app/SomeApp' (arm64)
Upon process connect
completing, the lldb
will then be attached to the remote debugserver
. The app will in a ready state, but not launched, this can be done with the run
command in lldb
that will launch the app.
At this point lldb
can be used as a regular debugger, there's some good documentation about how to use lldb
via it's command prompt in lldb
's documenation. One important note is that you can only have one debug session with a launched debugserver. To start a new debug session after process connect
, the debugserver needs to be re-started via idb debugserver stop
and subsequently running idb debugserver start
again.