Merge pull request #78275 from andrurogerz/swift-inspect-android

[swift-inspect] implement Android support including remote heap iteration
This commit is contained in:
Saleem Abdulrasool
2025-01-16 09:20:59 -08:00
committed by GitHub
25 changed files with 999 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.28)
project(swift-inspect
LANGUAGES CXX Swift)
LANGUAGES C CXX Swift)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
@@ -27,7 +27,17 @@ if(WIN32)
Sources/SwiftInspectClient/SwiftInspectClient.cpp)
target_link_libraries(SwiftInspectClient PRIVATE
SwiftInspectClientInterface)
elseif(LINUX)
endif()
if (ANDROID)
add_library(AndroidCLib STATIC
Sources/AndroidCLib/heap.c)
target_include_directories(AndroidCLib PUBLIC
Sources/AndroidCLib/include)
set_property(TARGET AndroidCLib PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()
if(ANDROID OR LINUX)
add_library(LinuxSystemHeaders INTERFACE)
target_include_directories(LinuxSystemHeaders INTERFACE
Sources/SwiftInspectLinux/SystemHeaders)
@@ -38,6 +48,8 @@ elseif(LINUX)
Sources/SwiftInspectLinux/MemoryMap.swift
Sources/SwiftInspectLinux/Process.swift
Sources/SwiftInspectLinux/ProcFS.swift
Sources/SwiftInspectLinux/PTrace.swift
Sources/SwiftInspectLinux/RegisterSet.swift
Sources/SwiftInspectLinux/SymbolCache.swift)
target_compile_options(SwiftInspectLinux PRIVATE
-Xcc -D_GNU_SOURCE)
@@ -52,6 +64,7 @@ add_executable(swift-inspect
Sources/swift-inspect/Operations/DumpConformanceCache.swift
Sources/swift-inspect/Operations/DumpGenericMetadata.swift
Sources/swift-inspect/Operations/DumpRawMetadata.swift
Sources/swift-inspect/AndroidRemoteProcess.swift
Sources/swift-inspect/Backtrace.swift
Sources/swift-inspect/DarwinRemoteProcess.swift
Sources/swift-inspect/LinuxRemoteProcess.swift
@@ -71,7 +84,12 @@ target_link_libraries(swift-inspect PRIVATE
if(WIN32)
target_link_libraries(swift-inspect PRIVATE
SwiftInspectClientInterface)
elseif(LINUX)
endif()
if(ANDROID)
target_link_libraries(swift-inspect PRIVATE
AndroidCLib)
endif()
if(ANDROID OR LINUX)
target_link_libraries(swift-inspect PRIVATE
SwiftInspectLinux)
endif()

View File

@@ -19,7 +19,8 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.target(name: "SwiftInspectClient", condition: .when(platforms: [.windows])),
.target(name: "SwiftInspectClientInterface", condition: .when(platforms: [.windows])),
.target(name: "SwiftInspectLinux", condition: .when(platforms: [.linux])),
.target(name: "SwiftInspectLinux", condition: .when(platforms: [.linux, .android])),
.target(name: "AndroidCLib", condition: .when(platforms: [.android])),
],
swiftSettings: [.unsafeFlags(["-parse-as-library"])]),
.target(name: "SwiftInspectClient"),
@@ -32,6 +33,11 @@ let package = Package(
.systemLibrary(
name: "LinuxSystemHeaders",
path: "Sources/SwiftInspectLinux/SystemHeaders"),
.target(
name: "AndroidCLib",
path: "Sources/AndroidCLib",
publicHeadersPath: "include",
cSettings: [.unsafeFlags(["-fPIC"])]),
.systemLibrary(
name: "SwiftInspectClientInterface"),
.testTarget(

View File

@@ -24,6 +24,27 @@ In order to build on Linux, some additional parameters must be passed to the bui
swift build -Xswiftc -I$(git rev-parse --show-toplevel)/include/swift/SwiftRemoteMirror -Xlinker -lswiftRemoteMirror
~~~
#### Android
To cross-compile swift-inspect for Android on Windows, some additional parameters must be passed to the build tool to locate the toolchain and necessary libraries.
~~~cmd
set ANDROID_ARCH=aarch64
set ANDROID_API_LEVEL=29
set ANDROID_CLANG_VERSION=17.0.2
set ANDROID_NDK_ROOT=C:\Android\android-sdk\ndk\26.3.11579264
set SDKROOT_ANDROID=%LocalAppData%\Programs\Swift\Platforms\0.0.0\Android.platform\Developer\SDKs\Android.sdk
swift build --triple %ANDROID_ARCH%-unknown-linux-android%ANDROID_API_LEVEL% ^
--sdk %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\sysroot ^
-Xswiftc -sdk -Xswiftc %SDKROOT_ANDROID% ^
-Xswiftc -sysroot -Xswiftc %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\sysroot ^
-Xswiftc -I -Xswiftc %SDKROOT_ANDROID%\usr\include ^
-Xswiftc -Xclang-linker -Xswiftc -resource-dir -Xswiftc -Xclang-linker -Xswiftc %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\%ANDROID_CLANG_VERSION% ^
-Xlinker -L%ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\%ANDROID_CLANG_VERSION%\lib\linux\%ANDROID_ARCH% ^
-Xcc -I%SDKROOT_ANDROID%\usr\include\swift\SwiftRemoteMirror ^
-Xlinker %SDKROOT_ANDROID%\usr\lib\swift\android\%ANDROID_ARCH%\libswiftRemoteMirror.so
~~~
#### CMake
In order to build on Windows with CMake, some additional parameters must be passed to the build tool to locate the necessary Swift modules.
@@ -38,6 +59,26 @@ In order to build on Linux with CMake, some additional parameters must be passed
cmake -B out -G Ninja -S . -D ArgumentParser_DIR=... -D CMAKE_Swift_FLAGS="-Xcc -I$(git rev-parse --show-toplevel)/include/swift/SwiftRemoteMirror"
~~~
In order to build for Android with CMake on Windows, some additiona parameters must be passed to the build tool to locate the necessary Swift modules.
~~~cmd
set ANDROID_ARCH=aarch64
set ANDROID_API_LEVEL=29
set ANDROID_CLANG_VERSION=17.0.2
set ANDROID_NDK_ROOT=C:\Android\android-sdk\ndk\26.3.11579264
set ANDROID_ARCH_ABI=arm64-v8a
set SDKROOT_ANDROID=%LocalAppData%\Programs\Swift\Platforms\0.0.0\Android.platform\Developer\SDKs\Android.sdk
cmake -B build -S . -G Ninja ^
-D CMAKE_BUILD_WITH_INSTALL_RPATH=YES ^
-D CMAKE_SYSTEM_NAME=Android ^
-D CMAKE_ANDROID_ARCH_ABI=%ANDROID_ARCH_ABI% ^
-D CMAKE_SYSTEM_VERSION=%ANDROID_API_LEVEL% ^
-D CMAKE_Swift_COMPILER_TARGET=%ANDROID_ARCH%-unknown-linux-android%ANDROID_API_LEVEL% ^
-D CMAKE_Swift_FLAGS="-sdk %SDKROOT_ANDROID% -L%ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\%ANDROID_CLANG_VERSION%\lib\linux\%ANDROID_ARCH% -Xclang-linker -resource-dir -Xclang-linker %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\%ANDROID_CLANG_VERSION% -Xcc -I%SDKROOT_ANDROID%\usr\include -I%SDKROOT_ANDROID%\usr\include\swift\SwiftRemoteMirror" ^
-D ArgumentParser_DIR=...
cmake --build build
~~~
Building with CMake requires a local copy of [swift-argument-parser](https://github.com/apple/swift-argument-parser) built with CMake.
The `ArumentParser_DIR=` definition must refer to the `cmake/modules` sub-directory of the swift-argument-parser build output directory.

View File

@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#include <string.h>
#include "heap.h"
/* The heap metadata buffer is interpreted as an array of 8-byte pairs. The
* first pair contains metadata describing the buffer itself: max valid index
* (e.g. size of the buffer) and next index (e.g. write cursor/position). Each
* subsequent pair describes the address and length of a heap entry in the
* remote process. A 4KiB page provides sufficient space for the header and
* 255 (address, length) pairs.
*
* ------------
* | uint64_t | max valid index (e.g. sizeof(buffer) / sizeof(uint64_t))
* ------------
* | uint64_t | next free index (starts at 2)
* ------------
* | uint64_t | heap item 1 address
* ------------
* | uint64_t | heap item 1 size
* ------------
* | uint64_t | heap item 2 address
* ------------
* | uint64_t | heap item 2 size
* ------------
* | uint64_t | ...
* ------------
* | uint64_t | ...
* ------------
* | uint64_t | heap item N address
* ------------
* | uint64_t | heap item N size
* ------------
*/
#if !__has_builtin(__builtin_debugtrap)
#error("compiler support for __builtin_debugtrap is required")
#endif
#define MAX_VALID_IDX 0
#define NEXT_FREE_IDX 1
#define HEADER_SIZE 2
#define ENTRY_SIZE 2
// Callback for malloc_iterate. Because this function is meant to be copied to
// a different process for execution, it must not make any function calls to
// ensure compiles to simple, position-independent code. It is implemented in C
// for readability/maintainability. It is placed in its own code section to
// simplify calculating its size.
__attribute__((noinline, used, section("heap_iterator")))
static void heap_iterate_callback(unsigned long base, unsigned long size, void *arg) {
volatile uint64_t *data = (uint64_t*)arg;
while (data[NEXT_FREE_IDX] >= data[MAX_VALID_IDX]) {
// SIGTRAP indicates the buffer is full and needs to be drained before more
// entries can be written.
__builtin_debugtrap();
// After the SIGTRAP, the signal handler advances the instruction pointer
// (PC) to the next instruction. Inserting a nop instruction here ensures
// the CPU has a clear, executable instruction to process, which avoids
// potential speculative execution or pipeline issues that could arise if
// the next instruction were a control transfer like a branch or jump.
__asm__ __volatile__("nop");
}
data[data[NEXT_FREE_IDX]++] = base;
data[data[NEXT_FREE_IDX]++] = size;
}
// The linker implicitly defines __start- and __stop- prefixed symbols that mark
// the start and end of user defined sections.
extern char __stop_heap_iterator[];
void* heap_iterate_callback_start() {
return (void*)heap_iterate_callback;
}
size_t heap_iterate_callback_len() {
return (uintptr_t)__stop_heap_iterator - (uintptr_t)heap_iterate_callback;
}
bool heap_iterate_metadata_init(void* data, size_t len) {
uint64_t *metadata = data;
const uint64_t max_entries = len / sizeof(uint64_t);
if (max_entries < HEADER_SIZE + ENTRY_SIZE)
return false;
memset(data, 0, len);
metadata[MAX_VALID_IDX] = max_entries;
metadata[NEXT_FREE_IDX] = HEADER_SIZE;
return true;
}
bool heap_iterate_metadata_process(
void* data, size_t len, void* callback_context, heap_iterate_entry_callback_t callback) {
uint64_t *metadata = data;
const uint64_t max_entries = len / sizeof(uint64_t);
const uint64_t end_index = metadata[NEXT_FREE_IDX];
if (metadata[MAX_VALID_IDX] != max_entries || end_index > max_entries)
return false;
for (size_t i = HEADER_SIZE; i < end_index; i += ENTRY_SIZE) {
const uint64_t base = metadata[i];
const uint64_t size = metadata[i + 1];
callback(callback_context, base, size);
}
return true;
}

View File

@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <stdbool.h>
#include <stdint.h>
#if defined(__cplusplus)
extern "C" {
#endif
// Location of the heap_iterate callback.
void* heap_iterate_callback_start();
// Size of the heap_iterate callback.
size_t heap_iterate_callback_len();
// Initialize the provided buffer to receive heap iteration metadata.
bool heap_iterate_metadata_init(void* data, size_t len);
// Callback invoked by heap_iterate_data_process for each heap entry .
typedef void (*heap_iterate_entry_callback_t)(void* context, uint64_t base, uint64_t len);
// Process all heap iteration entries in the provided buffer.
bool heap_iterate_metadata_process(
void* data, size_t len, void* callback_context, heap_iterate_entry_callback_t callback);
#if defined(__cplusplus)
}
#endif

View File

@@ -0,0 +1,4 @@
module AndroidCLib {
header "heap.h"
export *
}

View File

@@ -0,0 +1,157 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
import LinuxSystemHeaders
// Provides scoped access to a PTrace object.
public func withPTracedProcess(pid: pid_t, _ closure: (consuming PTrace) throws -> Void) throws {
let ptrace = try PTrace(pid)
try closure(ptrace)
}
public struct PTrace: ~Copyable {
enum PTraceError: Error {
case operationFailure(_ command: CInt, pid: pid_t, errno: CInt = get_errno())
case waitFailure(pid: pid_t, errno: CInt = get_errno())
case unexpectedWaitStatus(pid: pid_t, status: CInt, sigInfo: siginfo_t? = nil)
}
let pid: pid_t
// Initializing a PTrace instance attaches to the target process, waits for
// it to stop, and leaves it in a stopped state. The caller may resume the
// process by calling cont().
// NOTE: clients must use withPTracedProcess instead of direct initialization.
fileprivate init(_ pid: pid_t) throws {
guard ptrace_attach(pid) != -1 else {
throw PTraceError.operationFailure(PTRACE_ATTACH, pid: pid)
}
while true {
var status: CInt = 0
let result = waitpid(pid, &status, 0)
if result == -1 {
if get_errno() == EINTR { continue }
throw PTraceError.waitFailure(pid: pid)
}
precondition(pid == result, "waitpid returned unexpected value \(result)")
if wIfStopped(status) { break }
}
self.pid = pid
}
deinit { _ = ptrace_detach(self.pid) }
public func cont() throws {
if ptrace_cont(self.pid) == -1 {
throw PTraceError.operationFailure(PTRACE_CONT, pid: self.pid)
}
}
public func interrupt() throws {
if ptrace_interrupt(self.pid) == -1 {
throw PTraceError.operationFailure(PTRACE_INTERRUPT, pid: self.pid)
}
}
public func getSigInfo() throws -> siginfo_t {
var sigInfo = siginfo_t()
if ptrace_getsiginfo(self.pid, &sigInfo) == -1 {
throw PTraceError.operationFailure(PTRACE_GETSIGINFO, pid: self.pid)
}
return sigInfo
}
public func pokeData(addr: UInt64, value: UInt64) throws {
if ptrace_pokedata(self.pid, UInt(addr), UInt(value)) == -1 {
throw PTraceError.operationFailure(PTRACE_POKEDATA, pid: self.pid)
}
}
public func getRegSet() throws -> RegisterSet {
var regSet = RegisterSet()
try withUnsafeMutableBytes(of: &regSet) {
var vec = iovec(iov_base: $0.baseAddress!, iov_len: MemoryLayout<RegisterSet>.size)
if ptrace_getregset(self.pid, NT_PRSTATUS, &vec) == -1 {
throw PTraceError.operationFailure(PTRACE_GETREGSET, pid: self.pid)
}
}
return regSet
}
public func setRegSet(regSet: RegisterSet) throws {
var regSetCopy = regSet
try withUnsafeMutableBytes(of: &regSetCopy) {
var vec = iovec(iov_base: $0.baseAddress!, iov_len: MemoryLayout<RegisterSet>.size)
if ptrace_setregset(self.pid, NT_PRSTATUS, &vec) == -1 {
throw PTraceError.operationFailure(PTRACE_SETREGSET, pid: self.pid)
}
}
}
// Call the function at the specified address in the attached process with the
// provided argument array. The optional callback is invoked when the process
// is stopped with a SIGTRAP signal. In this case, the caller is responsible
// for taking action on the signal.
public func jump(
to address: UInt64, with args: [UInt64] = [],
_ callback: ((borrowing PTrace) throws -> Void)? = nil
) throws -> UInt64 {
let origRegs = try self.getRegSet()
defer { try? self.setRegSet(regSet: origRegs) }
// Set the return address to 0. This forces the function to return to 0 on
// completion, resulting in a SIGSEGV with address 0 which will interrupt
// the process and notify us (the tracer) via waitpid(). At that point, we
// will restore the original state and continue the process.
let returnAddr: UInt64 = 0
var newRegs = try origRegs.setupCall(self, to: address, with: args, returnTo: returnAddr)
try self.setRegSet(regSet: newRegs)
try self.cont()
var status: CInt = 0
while true {
let result = waitpid(self.pid, &status, 0)
guard result != -1 else {
if get_errno() == EINTR { continue }
throw PTraceError.waitFailure(pid: self.pid)
}
precondition(self.pid == result, "waitpid returned unexpected value \(result)")
guard wIfStopped(status) && !wIfExited(status) && !wIfSignaled(status) else {
throw PTraceError.unexpectedWaitStatus(pid: self.pid, status: status)
}
guard wStopSig(status) == SIGTRAP, let callback = callback else { break }
// give the caller the opportunity to handle SIGTRAP
try callback(self)
}
let sigInfo = try self.getSigInfo()
newRegs = try self.getRegSet()
guard wStopSig(status) == SIGSEGV, siginfo_si_addr(sigInfo) == nil else {
throw PTraceError.unexpectedWaitStatus(pid: self.pid, status: status, sigInfo: sigInfo)
}
return UInt64(newRegs.returnValue())
}
}

View File

@@ -13,9 +13,25 @@
import Foundation
import LinuxSystemHeaders
// The Android version of iovec defineds iov_len as __kernel_size_t, while the
// standard Linux definition is size_t. This extension makes the difference
// easier to deal with.
extension iovec {
public init<T: BinaryInteger>(iov_base: UnsafeMutableRawPointer?, iov_len: T) {
self.init()
self.iov_base = iov_base
#if os(Android)
self.iov_len = __kernel_size_t(iov_len)
#else
self.iov_len = size_t(iov_len)
#endif
}
}
public class Process {
public enum ProcessError: Error {
case processVmReadFailure(pid: pid_t, address: UInt64, size: UInt64)
case processVmWriteFailure(pid: pid_t, address: UInt64, size: UInt64)
case malformedString(address: UInt64)
}
@@ -69,19 +85,46 @@ public class Process {
// read an array of type T elements from the target process
public func readArray<T>(address: UInt64, upToCount: UInt) throws -> [T] {
guard upToCount > 0 else { return [] }
let maxSize = upToCount * UInt(MemoryLayout<T>.stride)
let array: [T] = Array(unsafeUninitializedCapacity: Int(upToCount)) { buffer, initCount in
var local = iovec(iov_base: buffer.baseAddress!, iov_len: Int(maxSize))
var local = iovec(iov_base: buffer.baseAddress!, iov_len: maxSize)
var remote = iovec(
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(address)), iov_len: Int(maxSize))
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(address)), iov_len: maxSize)
let bytesRead = process_vm_readv(self.pid, &local, 1, &remote, 1, 0)
initCount = bytesRead / MemoryLayout<T>.stride
}
guard array.count > 0 else {
throw ProcessError.processVmReadFailure(pid: self.pid, address: address, size: UInt64(maxSize))
throw ProcessError.processVmReadFailure(
pid: self.pid, address: address, size: UInt64(maxSize))
}
return array
}
// simple wrapper around process_vm_readv
public func readMem(remoteAddr: UInt64, localAddr: UnsafeRawPointer, len: UInt) throws {
var local = iovec(iov_base: UnsafeMutableRawPointer(mutating: localAddr), iov_len: len)
var remote = iovec(
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(remoteAddr)), iov_len: len)
let bytesRead = process_vm_readv(self.pid, &local, 1, &remote, 1, 0)
guard bytesRead == len else {
throw ProcessError.processVmReadFailure(pid: self.pid, address: remoteAddr, size: UInt64(len))
}
}
// simple wrapper around process_vm_writev
public func writeMem(remoteAddr: UInt64, localAddr: UnsafeRawPointer, len: UInt) throws {
var local = iovec(iov_base: UnsafeMutableRawPointer(mutating: localAddr), iov_len: len)
var remote = iovec(
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(remoteAddr)), iov_len: len)
let bytesWritten = process_vm_writev(self.pid, &local, 1, &remote, 1, 0)
guard bytesWritten == len else {
throw ProcessError.processVmWriteFailure(
pid: self.pid, address: remoteAddr, size: UInt64(len))
}
}
}

View File

@@ -0,0 +1,94 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
import LinuxSystemHeaders
#if arch(arm64)
public typealias RegisterSet = user_pt_regs
extension RegisterSet {
public static var trapInstructionSize: UInt { return 4 } // brk #0x0
public func setupCall(
_ ptrace: borrowing PTrace, to funcAddr: UInt64, with args: [UInt64],
returnTo returnAddr: UInt64
) throws -> RegisterSet {
// The first 8 arguments are passed in regsters. Any additional arguments
// must be pushed on the stack, which is not implemented.
precondition(args.count <= 8)
var registers = self
registers.regs.0 = args.count > 0 ? args[0] : 0
registers.regs.1 = args.count > 1 ? args[1] : 0
registers.regs.2 = args.count > 2 ? args[2] : 0
registers.regs.3 = args.count > 3 ? args[3] : 0
registers.regs.4 = args.count > 4 ? args[4] : 0
registers.regs.5 = args.count > 5 ? args[5] : 0
registers.regs.6 = args.count > 6 ? args[6] : 0
registers.regs.7 = args.count > 7 ? args[7] : 0
registers.pc = funcAddr
registers.regs.30 = returnAddr // link register (x30)
return registers
}
public func returnValue() -> UInt64 {
return self.regs.0
}
public mutating func step(_ bytes: UInt) {
self.pc += UInt64(bytes)
}
}
#elseif arch(x86_64)
public typealias RegisterSet = pt_regs
extension RegisterSet {
public static var trapInstructionSize: UInt { return 1 } // int3
public func setupCall(
_ ptrace: borrowing PTrace, to funcAddr: UInt64, with args: [UInt64],
returnTo returnAddr: UInt64
) throws -> RegisterSet {
// The first 6 arguments are passed in registers. Any additional arguments
// must be pushed on the stack, which is not implemented.
precondition(args.count <= 6)
var registers = self
registers.rdi = UInt(args.count > 0 ? args[0] : 0)
registers.rsi = UInt(args.count > 1 ? args[1] : 0)
registers.rdx = UInt(args.count > 2 ? args[2] : 0)
registers.rcx = UInt(args.count > 3 ? args[3] : 0)
registers.r8 = UInt(args.count > 4 ? args[4] : 0)
registers.r9 = UInt(args.count > 5 ? args[5] : 0)
registers.rip = UInt(funcAddr)
registers.rax = 0 // rax is the number of args in a va_args function
// push the return address onto the stack
registers.rsp -= UInt(MemoryLayout<UInt64>.size)
try ptrace.pokeData(addr: UInt64(registers.rsp), value: returnAddr)
return registers
}
public func returnValue() -> UInt64 {
return UInt64(self.rax)
}
public mutating func step(_ bytes: UInt) {
self.rip += UInt(bytes)
}
}
#else
#error("Only arm64 and x86_64 architectures are supported")
#endif

View File

@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <errno.h>
static inline
int get_errno() {
return errno;
}

View File

@@ -0,0 +1,15 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <sys/mman.h>

View File

@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <signal.h>
#include <sys/ptrace.h>
#include <sys/uio.h>
#include <linux/ptrace.h>
static inline
int ptrace_retry(int op, pid_t pid, void *addr, void *data) {
int retries = 3;
int result;
do {
result = ptrace(op, pid, addr, data);
} while (result < 0 && (errno == EAGAIN || errno == EBUSY) && retries-- > 0);
return result;
}
static inline
int ptrace_attach(pid_t pid) {
return ptrace_retry(PTRACE_ATTACH, pid, 0, 0);
}
static inline
int ptrace_detach(pid_t pid) {
return ptrace_retry(PTRACE_DETACH, pid, 0, 0);
}
static inline
int ptrace_cont(pid_t pid) {
return ptrace_retry(PTRACE_CONT, pid, 0, 0);
}
static inline
int ptrace_interrupt(pid_t pid) {
return ptrace_retry(PTRACE_INTERRUPT, pid, 0, 0);
}
static inline
int ptrace_getsiginfo(pid_t pid, siginfo_t *siginfo) {
return ptrace_retry(PTRACE_GETSIGINFO, pid, 0, siginfo);
}
static inline
int ptrace_pokedata(pid_t pid, unsigned long addr, unsigned long value) {
return ptrace_retry(PTRACE_POKEDATA, pid, (void*)(uintptr_t)addr, (void*)(uintptr_t)value);
}
static inline
int ptrace_getregset(pid_t pid, int type, struct iovec *regs) {
return ptrace_retry(PTRACE_GETREGSET, pid, (void*)(uintptr_t)type, regs);
}
static inline
int ptrace_setregset(pid_t pid, int type, struct iovec *regs) {
return ptrace_retry(PTRACE_SETREGSET, pid, (void*)(uintptr_t)type, regs);
}

View File

@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <signal.h>
static inline
void* siginfo_si_addr(siginfo_t siginfo) {
return siginfo.si_addr;
}

View File

@@ -0,0 +1,15 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <unistd.h>

View File

@@ -0,0 +1,36 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#pragma once
#include <stdbool.h>
#include <sys/wait.h>
static inline
bool wIfStopped(int status) {
return WIFSTOPPED(status) != 0;
}
static inline
bool wIfExited(int status) {
return WIFEXITED(status) != 0;
}
static inline
bool wIfSignaled(int status) {
return WIFSIGNALED(status) != 0;
}
static inline
int wStopSig(int status) {
return WSTOPSIG(status);
}

View File

@@ -1,7 +1,13 @@
module LinuxSystemHeaders {
header "auxvec.h"
header "elf.h"
header "link.h"
header "uio.h"
header "_auxvec.h"
header "_elf.h"
header "_errno.h"
header "_link.h"
header "_mman.h"
header "_ptrace.h"
header "_signal.h"
header "_uio.h"
header "_unistd.h"
header "_wait.h"
export *
}

View File

@@ -0,0 +1,247 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if os(Android)
import AndroidCLib
import Foundation
import LinuxSystemHeaders
import SwiftInspectLinux
import SwiftRemoteMirror
extension MemoryMap.Entry {
public func isHeapRegion() -> Bool {
guard let name = self.pathname else { return false }
// The heap region naming convention is found in AOSP's libmemunreachable at
// android/platform/system/memory/libmemunreachable/MemUnreachable.cpp.
if name == "[anon:libc_malloc]" { return true }
if name.hasPrefix("[anon:scudo:") { return true }
if name.hasPrefix("[anon:GWP-ASan") { return true }
return false
}
}
internal final class AndroidRemoteProcess: LinuxRemoteProcess {
enum RemoteProcessError: Error {
case missingSymbol(_ name: String)
case heapIterationFailed
}
struct RemoteSymbol {
let addr: UInt64?
let name: String
init(_ name: String, _ symbolCache: SymbolCache) {
self.name = name
if let symbolRange = symbolCache.address(of: name) {
self.addr = symbolRange.start
} else {
self.addr = nil
}
}
}
// We call mmap/munmap in the remote process to alloc/free memory for our own
// use without impacting existing allocations in the remote process.
lazy var mmapSymbol: RemoteSymbol = RemoteSymbol("mmap", self.symbolCache)
lazy var munmapSymbol: RemoteSymbol = RemoteSymbol("munmap", self.symbolCache)
// We call malloc_iterate in the remote process to enumerate all items in the
// remote process' heap. We use malloc_disable/malloc_enable to ensure no
// malloc/free requests can race with malloc_iterate.
lazy var mallocDisableSymbol: RemoteSymbol = RemoteSymbol("malloc_disable", self.symbolCache)
lazy var mallocEnableSymbol: RemoteSymbol = RemoteSymbol("malloc_enable", self.symbolCache)
lazy var mallocIterateSymbol: RemoteSymbol = RemoteSymbol("malloc_iterate", self.symbolCache)
// Linux and Android have no supported method to enumerate allocations in the
// heap of a remote process. Android does, however, support the malloc_iterate
// API, which enumerates allocations in the current process. We leverage this
// API by invoking it in the remote process with ptrace and using simple IPC
// (SIGTRAP and process_vm_readv and process_vm_writev) to fetch the results.
override internal func iterateHeap(_ body: (swift_addr_t, UInt64) -> Void) {
var regionCount = 0
var allocCount = 0
do {
try withPTracedProcess(pid: self.processIdentifier) { ptrace in
for entry in self.memoryMap.entries {
// Limiting malloc_iterate calls to only memory regions that are known
// to contain heap allocations is not strictly necessary but it does
// significantly improve the speed of heap iteration.
guard entry.isHeapRegion() else { continue }
// collect all of the allocations in this heap region
let allocations: [(base: swift_addr_t, len: UInt64)]
allocations = try self.iterateHeapRegion(ptrace, region: entry)
regionCount += 1
allocCount += allocations.count
// process all of the collected allocations
for alloc in allocations { body(alloc.base, alloc.len) }
}
}
} catch {
print("failed iterating remote heap: \(error)")
return
}
if regionCount == 0 {
// This condition most likely indicates the MemoryMap.Entry.isHeapRegion
// filtering is needs to be modified to support a new heap region naming
// convention in a newer Android version.
print("WARNING: no heap regions found")
print("swift-inspect may need to be updated for a newer Android version")
} else if allocCount == 0 {
print("WARNING: no heap items enumerated")
}
}
// Iterate a single heap region in the remote process and return an array
// of (base, len) pairs describing each heap allocation in the region.
internal func iterateHeapRegion(_ ptrace: borrowing PTrace, region: MemoryMap.Entry) throws -> [(
base: swift_addr_t, len: UInt64
)] {
// Allocate a page-sized buffer in the remote process that malloc_iterate
// will populaate with metadata describing each heap entry it enumerates.
let dataLen = sysconf(Int32(_SC_PAGESIZE))
let remoteDataAddr = try self.mmapRemote(
ptrace, len: dataLen, prot: PROT_READ | PROT_WRITE, flags: MAP_ANON | MAP_PRIVATE)
defer {
_ = try? self.munmapRemote(ptrace, addr: remoteDataAddr, len: dataLen)
}
// Allocate and inialize a local buffer that will be used to copy metadata
// to/from the target process.
let buffer = UnsafeMutableRawPointer.allocate(
byteCount: dataLen, alignment: MemoryLayout<UInt64>.alignment)
defer { buffer.deallocate() }
guard heap_iterate_metadata_init(buffer, dataLen) else {
throw RemoteProcessError.heapIterationFailed
}
try self.process.writeMem(remoteAddr: remoteDataAddr, localAddr: buffer, len: UInt(dataLen))
// Allocate an rwx region to hold the malloc_iterate callback that will be
// executed in the remote process.
let codeLen = heap_iterate_callback_len()
let remoteCodeAddr = try mmapRemote(
ptrace, len: codeLen, prot: PROT_READ | PROT_WRITE | PROT_EXEC, flags: MAP_ANON | MAP_PRIVATE)
defer {
_ = try? self.munmapRemote(ptrace, addr: remoteCodeAddr, len: codeLen)
}
// Copy the malloc_iterate callback implementation to the remote process.
let codeStart = heap_iterate_callback_start()!
try self.process.writeMem(
remoteAddr: remoteCodeAddr, localAddr: codeStart, len: UInt(codeLen))
guard let mallocIterateAddr = self.mallocIterateSymbol.addr else {
throw RemoteProcessError.missingSymbol(self.mallocIterateSymbol.name)
}
// Disable malloc/free while enumerating the region to get a consistent
// snapshot of existing allocations.
try self.mallocDisableRemote(ptrace)
defer {
_ = try? self.mallocEnableRemote(ptrace)
}
// Collects (base, len) pairs describing each heap allocation in the remote
// process.
var allocations: [(base: swift_addr_t, len: UInt64)] = []
let regionLen = region.endAddr - region.startAddr
let args = [region.startAddr, regionLen, remoteCodeAddr, remoteDataAddr]
_ = try ptrace.jump(to: mallocIterateAddr, with: args) { ptrace in
// This callback is invoked when a SIGTRAP is encountered in the remote
// process. In this context, this signal indicates there is no more room
// in the allocated metadata region (see AndroidCLib/heap.c).
// Immediately read the heap metadata from the remote process, skip past
// the trap/break instruction, and resume the remote process.
try self.process.readMem(remoteAddr: remoteDataAddr, localAddr: buffer, len: UInt(dataLen))
allocations.append(contentsOf: try self.processHeapMetadata(buffer: buffer, len: dataLen))
guard heap_iterate_metadata_init(buffer, dataLen) else {
throw RemoteProcessError.heapIterationFailed
}
try self.process.writeMem(remoteAddr: remoteDataAddr, localAddr: buffer, len: UInt(dataLen))
var regs = try ptrace.getRegSet()
regs.step(RegisterSet.trapInstructionSize)
try ptrace.setRegSet(regSet: regs)
try ptrace.cont()
}
try self.process.readMem(remoteAddr: remoteDataAddr, localAddr: buffer, len: UInt(dataLen))
allocations.append(contentsOf: try self.processHeapMetadata(buffer: buffer, len: dataLen))
return allocations
}
// Process heap metadata generated by our malloc_iterate callback in the
// remote process and return an array of (base, len) pairs describing each
// heap allocation.
internal func processHeapMetadata(buffer: UnsafeMutableRawPointer, len: Int) throws -> [(
base: UInt64, len: UInt64
)] {
let callback: @convention(c) (UnsafeMutableRawPointer?, UInt64, UInt64) -> Void = {
let allocationsPointer = $0!.assumingMemoryBound(to: [(UInt64, UInt64)].self)
allocationsPointer.pointee.append(($1, $2))
}
var allocations: [(UInt64, UInt64)] = []
try withUnsafeMutablePointer(to: &allocations) {
let context = UnsafeMutableRawPointer($0)
if !heap_iterate_metadata_process(buffer, Int(len), context, callback) {
throw RemoteProcessError.heapIterationFailed
}
}
return allocations
}
// call mmap in the remote process with the provided arguments
internal func mmapRemote(_ ptrace: borrowing PTrace, len: Int, prot: Int32, flags: Int32) throws
-> UInt64
{
guard let sym = self.mmapSymbol.addr else {
throw RemoteProcessError.missingSymbol(self.mmapSymbol.name)
}
let args = [0, UInt64(len), UInt64(prot), UInt64(flags)]
return try ptrace.jump(to: sym, with: args)
}
// call munmap in the remote process with the provdied arguments
internal func munmapRemote(_ ptrace: borrowing PTrace, addr: UInt64, len: Int) throws -> UInt64 {
guard let sym = self.munmapSymbol.addr else {
throw RemoteProcessError.missingSymbol(self.munmapSymbol.name)
}
let args: [UInt64] = [addr, UInt64(len)]
return try ptrace.jump(to: sym, with: args)
}
// call malloc_disable in the remote process
internal func mallocDisableRemote(_ ptrace: borrowing PTrace) throws {
guard let sym = self.mallocDisableSymbol.addr else {
throw RemoteProcessError.missingSymbol(self.mallocDisableSymbol.name)
}
_ = try ptrace.jump(to: sym)
}
// call malloc_enable in the remote process
internal func mallocEnableRemote(_ ptrace: borrowing PTrace) throws {
guard let sym = self.mallocEnableSymbol.addr else {
throw RemoteProcessError.missingSymbol(self.mallocEnableSymbol.name)
}
_ = try ptrace.jump(to: sym)
}
}
#endif

View File

@@ -10,7 +10,7 @@
//
//===----------------------------------------------------------------------===//
#if os(Linux)
#if os(Linux) || os(Android)
import Foundation
import SwiftInspectLinux
import SwiftRemoteMirror

View File

@@ -168,6 +168,31 @@ internal func getRemoteProcess(processId: ProcessIdentifier,
return LinuxRemoteProcess(processId: processId)
}
#elseif os(Android)
import Foundation
internal typealias ProcessIdentifier = AndroidRemoteProcess.ProcessIdentifier
internal func process(matching: String) -> ProcessIdentifier? {
guard let processId = AndroidRemoteProcess.ProcessIdentifier(matching) else {
return nil
}
let procfsPath = "/proc/\(processId)"
var isDirectory: Bool = false
guard FileManager.default.fileExists(atPath: procfsPath, isDirectory: &isDirectory)
&& isDirectory else {
return nil
}
return processId
}
internal func getRemoteProcess(processId: ProcessIdentifier,
options: UniversalOptions) -> (any RemoteProcess)? {
return AndroidRemoteProcess(processId: processId)
}
#else
#error("Unsupported platform")
#endif

View File

@@ -22,7 +22,14 @@ extension DefaultStringInterpolation {
enum Std {
struct File: TextOutputStream {
var underlying: UnsafeMutablePointer<FILE>
#if os(Android)
typealias File = OpaquePointer
#else
typealias File = UnsafeMutablePointer<FILE>
#endif
var underlying: File
mutating func write(_ string: String) {
fputs(string, underlying)
@@ -34,4 +41,4 @@ enum Std {
internal func disableStdErrBuffer() {
setvbuf(stderr, nil, Int32(_IONBF), 0)
}
}

View File

@@ -136,7 +136,7 @@ internal struct SwiftInspect: ParsableCommand {
DumpArrays.self,
DumpConcurrency.self,
]
#elseif os(Windows)
#elseif os(Windows) || os(Android)
static let subcommands: [ParsableCommand.Type] = [
DumpConformanceCache.self,
DumpRawMetadata.self,