Skip to content

Commit f981f35

Browse files
committed
[swift-inspect] full Android implementation including heap iteration
1 parent d2801ec commit f981f35

File tree

19 files changed

+661
-5
lines changed

19 files changed

+661
-5
lines changed

tools/swift-inspect/Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ let package = Package(
1919
.product(name: "ArgumentParser", package: "swift-argument-parser"),
2020
.target(name: "SwiftInspectClient", condition: .when(platforms: [.windows])),
2121
.target(name: "SwiftInspectClientInterface", condition: .when(platforms: [.windows])),
22-
.target(name: "SwiftInspectLinux", condition: .when(platforms: [.linux])),
22+
.target(name: "SwiftInspectLinux", condition: .when(platforms: [.linux, .android])),
23+
.target(name: "AndroidCLib", condition: .when(platforms: [.android])),
2324
],
2425
swiftSettings: [.unsafeFlags(["-parse-as-library"])]),
2526
.target(name: "SwiftInspectClient"),
@@ -32,6 +33,10 @@ let package = Package(
3233
.systemLibrary(
3334
name: "LinuxSystemHeaders",
3435
path: "Sources/SwiftInspectLinux/SystemHeaders"),
36+
.target(
37+
name: "AndroidCLib",
38+
path: "Sources/AndroidCLib",
39+
publicHeadersPath: "include"),
3540
.systemLibrary(
3641
name: "SwiftInspectClientInterface"),
3742
.testTarget(

tools/swift-inspect/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ In order to build on Windows with CMake, some additional parameters must be pass
3232
cmake -B out -G Ninja -S . -D ArgumentParser_DIR=... -D CMAKE_Swift_FLAGS="-Xcc -I%SDKROOT%\usr\include\swift\SwiftRemoteMirror"
3333
~~~
3434

35+
#### Android
36+
37+
To cross-compile swift-inspect for Android, some additional parameters must be passed to the build tool to locate the toolchain and necessary libraries.
38+
39+
~~~
40+
set ANDROID_ARCH=aarch64
41+
set ANDROID_API_LEVEL=29
42+
set ANDROID_NDK_ROOT=C:\Android\android-sdk\ndk\26.3.11579264
43+
set SWIFT_ANDROID_SDK_ROOT=C:\Users\Andrew\AppData\Local\Programs\Swift\Platforms\0.0.0\Android.platform\Developer\SDKs\Android.sdk
44+
swift build --triple %ANDROID_ARCH%-unknown-linux-android%ANDROID_API_LEVEL% `
45+
--sdk %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\sysroot `
46+
-Xswiftc -sdk -Xswiftc %SWIFT_ANDROID_SDK_ROOT% `
47+
-Xswiftc -sysroot -Xswiftc %ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\sysroot `
48+
-Xswiftc -I -Xswiftc %SWIFT_ANDROID_SDK_ROOT%\usr\include `
49+
-Xlinker -L%ANDROID_NDK_ROOT%\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\17.0.2\lib\linux\%ANDROID_ARCH% `
50+
-Xcc -I%SWIFT_ANDROID_SDK_ROOT%\usr\include\swift\SwiftRemoteMirror `
51+
-Xlinker %SWIFT_ANDROID_SDK_ROOT%\usr\lib\swift\android\%ANDROID_ARCH%\libswiftRemoteMirror.so
52+
~~~
53+
3554
### Using
3655

3756
The following inspection operations are available currently.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#include "heap.h"
14+
15+
#if defined(__aarch64__) || defined(__ARM64__) || defined(_M_ARM64)
16+
#define DEBUG_BREAK() asm("brk #0x0; nop")
17+
#elif defined(_M_X64) || defined(__amd64__) || defined(__x86_64__) || defined(_M_AMD64)
18+
#define DEBUG_BREAK() asm("int3; nop")
19+
#else
20+
#error("only aarch64 and x86_64 are supported")
21+
#endif
22+
23+
/* We allocate a buffer in the remote process that it populates with metadata
24+
* describing each heap entry it enumerates. We then read the contents of the
25+
* buffer, and individual heap entry contents, with process_vm_readv.
26+
*
27+
* The buffer is interpreted as an array of 8-byte pairs. The first pair
28+
* contains metadata describing the buffer itself: max valid index (e.g. size of
29+
* the buffer) and next index (e.g. write cursor/position). Each subsequent pair
30+
* describes the address and length of a heap entry in the remote process.
31+
*
32+
* ------------
33+
* | uint64_t | max valid index (e.g. sizeof(buffer) / sizeof(uint64_t))
34+
* ------------
35+
* | uint64_t | next free index (starts at 2)
36+
* ------------
37+
* | uint64_t | heap item 1 address
38+
* ------------
39+
* | uint64_t | heap item 1 size
40+
* ------------
41+
* | uint64_t | heap item 2 address
42+
* ------------
43+
* | uint64_t | heap item 2 size
44+
* ------------
45+
* | uint64_t | ...
46+
* ------------
47+
* | uint64_t | ...
48+
* ------------
49+
* | uint64_t | heap item N address
50+
* ------------
51+
* | uint64_t | heap item N size
52+
* ------------
53+
*/
54+
55+
// NOTE: this function cannot call any other functions and must only use
56+
// relative branches. We could inline assembly instead, but C is more reabable
57+
// and maintainable.
58+
static void remote_callback_start(unsigned long base, unsigned long size, void *arg) {
59+
volatile uint64_t *data = (uint64_t*)arg;
60+
while (data[HEAP_ITERATE_DATA_NEXT_FREE_IDX] >= data[HEAP_ITERATE_DATA_MAX_VALID_IDX]) {
61+
DEBUG_BREAK();
62+
}
63+
data[data[HEAP_ITERATE_DATA_NEXT_FREE_IDX]++] = base;
64+
data[data[HEAP_ITERATE_DATA_NEXT_FREE_IDX]++] = size;
65+
}
66+
67+
// NOTE: this function is here to mark the end of remote_callback_start and is never
68+
// called.
69+
static void remote_callback_end() {}
70+
71+
void* heap_callback_start() {
72+
return (void*)remote_callback_start;
73+
}
74+
75+
size_t heap_callback_len() {
76+
return (size_t)(remote_callback_end - remote_callback_start);
77+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#pragma once
14+
15+
#include <stdbool.h>
16+
#include <stdint.h>
17+
#include <unistd.h>
18+
19+
#if defined(__cplusplus)
20+
extern "C" {
21+
#endif
22+
23+
#define HEAP_ITERATE_DATA_MAX_VALID_IDX 0
24+
#define HEAP_ITERATE_DATA_NEXT_FREE_IDX 1
25+
#define HEAP_ITERATE_DATA_HEADER_SIZE 2
26+
#define HEAP_ITERATE_DATA_ENTRY_SIZE 2
27+
28+
typedef void (*heap_iterate_callback_t)(void* context, uint64_t base, uint64_t len);
29+
bool heap_iterate(pid_t pid, void* callback_context, heap_iterate_callback_t callback);
30+
31+
void* heap_callback_start();
32+
size_t heap_callback_len();
33+
34+
#if defined(__cplusplus)
35+
}
36+
#endif
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import Foundation
2+
import LinuxSystemHeaders
3+
4+
public class PTrace {
5+
enum Error: Swift.Error {
6+
case PTraceFailure(_ command: Int32, pid: pid_t, errno: Int32 = get_errno())
7+
case WaitFailure(pid: pid_t, errno: Int32 = get_errno())
8+
case IllegalArgument(description: String)
9+
case UnexpectedWaitStatus(pid: pid_t, status: Int32, sigInfo: siginfo_t? = nil)
10+
}
11+
12+
let pid: pid_t
13+
14+
public init(process pid: pid_t) throws {
15+
if ptrace_attach(pid) == -1 { throw Error.PTraceFailure(PTRACE_ATTACH, pid: pid) }
16+
17+
while true {
18+
var status: Int32 = 0
19+
let result = waitpid(pid, &status, 0)
20+
if result == -1 {
21+
if get_errno() == EINTR { continue }
22+
throw Error.WaitFailure(pid: pid)
23+
}
24+
25+
if result == pid && wIfStopped(status) { break }
26+
}
27+
28+
self.pid = pid
29+
}
30+
31+
deinit { ptrace_detach(self.pid) }
32+
33+
public func cont() throws {
34+
if ptrace_continue(self.pid) == -1 { throw Error.PTraceFailure(PTRACE_CONT, pid: self.pid) }
35+
}
36+
37+
public func getSigInfo() throws -> siginfo_t {
38+
var sigInfo = siginfo_t()
39+
if ptrace_getsiginfo(self.pid, &sigInfo) == -1 {
40+
throw Error.PTraceFailure(PTRACE_GETSIGINFO, pid: self.pid)
41+
}
42+
return sigInfo
43+
}
44+
45+
public func pokeData(addr: UInt64, value: UInt64) throws {
46+
if ptrace_pokedata(self.pid, UInt(addr), UInt(value)) == -1 {
47+
throw Error.PTraceFailure(PTRACE_POKEDATA, pid: self.pid)
48+
}
49+
}
50+
51+
public func getRegSet() throws -> RegisterSet {
52+
var regSet = RegisterSet()
53+
try withUnsafeMutableBytes(of: &regSet) {
54+
var vec = iovec(iov_base: $0.baseAddress!, iov_len: MemoryLayout<RegisterSet>.size)
55+
if ptrace_getregset(self.pid, NT_PRSTATUS, &vec) == -1 {
56+
throw Error.PTraceFailure(PTRACE_GETREGSET, pid: self.pid)
57+
}
58+
}
59+
return regSet
60+
}
61+
62+
public func setRegSet(regSet: RegisterSet) throws {
63+
var regSetCopy = regSet
64+
try withUnsafeMutableBytes(of: &regSetCopy) {
65+
var vec = iovec(iov_base: $0.baseAddress!, iov_len: MemoryLayout<RegisterSet>.size)
66+
if ptrace_setregset(self.pid, NT_PRSTATUS, &vec) == -1 {
67+
throw Error.PTraceFailure(PTRACE_SETREGSET, pid: self.pid)
68+
}
69+
}
70+
}
71+
72+
public func callRemoteFunction(
73+
at address: UInt64, with args: [UInt64] = [], onTrap callback: (() throws -> Void)? = nil
74+
) throws -> UInt64 {
75+
76+
guard args.count <= 6 else {
77+
throw Error.IllegalArgument(description: "max of 6 arguments allowed")
78+
}
79+
80+
let origRegs = try self.getRegSet()
81+
defer { try? self.setRegSet(regSet: origRegs) }
82+
83+
// Set the return address to 0. This forces the function to return to 0 on
84+
// completion, resulting in a SIGSEGV with address 0 which will interrupt
85+
// the process and notify us (the tracer) via waitpid(). At that point, we
86+
// will restore the original state and continue the process.
87+
let returnAddr: UInt64 = 0
88+
89+
var newRegs = try origRegs.setupCall(self, to: address, with: args, returnTo: returnAddr)
90+
try self.setRegSet(regSet: newRegs)
91+
try self.cont()
92+
93+
var status: Int32 = 0
94+
while true {
95+
let result = waitpid(self.pid, &status, 0)
96+
if result == -1 {
97+
if get_errno() == EINTR { continue }
98+
throw Error.WaitFailure(pid: self.pid)
99+
}
100+
101+
if wIfExited(status) || wIfSignaled(status) {
102+
throw Error.UnexpectedWaitStatus(pid: self.pid, status: status)
103+
}
104+
105+
if wIfStopped(status) {
106+
guard wStopSig(status) == SIGTRAP, let callback = callback else { break }
107+
108+
// give the caller the opportunity to handle SIGTRAP
109+
try callback()
110+
try self.cont()
111+
continue
112+
}
113+
}
114+
115+
let sigInfo = try self.getSigInfo()
116+
newRegs = try self.getRegSet()
117+
118+
guard wStopSig(status) == SIGSEGV, siginfo_si_addr(sigInfo) == nil else {
119+
print("WSTOPSIG(status):\(wStopSig(status)), si_addr:\(siginfo_si_addr(sigInfo)!)")
120+
throw Error.UnexpectedWaitStatus(pid: self.pid, status: status, sigInfo: sigInfo)
121+
}
122+
123+
return UInt64(newRegs.returnValue())
124+
}
125+
}

tools/swift-inspect/Sources/SwiftInspectLinux/Process.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,25 @@
1313
import Foundation
1414
import LinuxSystemHeaders
1515

16+
// The Android version of iovec defineds iov_len as __kernel_size_t, while the
17+
// standard Linux definition is size_t. This extension makes the difference
18+
// easier to deal with.
19+
public extension iovec {
20+
init<T: BinaryInteger>(iov_base: UnsafeMutableRawPointer?, iov_len: T) {
21+
self.init()
22+
self.iov_base = iov_base
23+
#if os(Android)
24+
self.iov_len = __kernel_size_t(iov_len)
25+
#else
26+
self.iov_len = size_t(iov_len)
27+
#endif
28+
}
29+
}
30+
1631
public class Process {
1732
public enum ProcessError: Error {
1833
case processVmReadFailure(pid: pid_t, address: UInt64, size: UInt64)
34+
case processVmWriteFailure(pid: pid_t, address: UInt64, size: UInt64)
1935
case malformedString(address: UInt64)
2036
}
2137

@@ -69,11 +85,12 @@ public class Process {
6985
// read an array of type T elements from the target process
7086
public func readArray<T>(address: UInt64, upToCount: UInt) throws -> [T] {
7187
guard upToCount > 0 else { return [] }
88+
7289
let maxSize = upToCount * UInt(MemoryLayout<T>.stride)
7390
let array: [T] = Array(unsafeUninitializedCapacity: Int(upToCount)) { buffer, initCount in
74-
var local = iovec(iov_base: buffer.baseAddress!, iov_len: Int(maxSize))
91+
var local = iovec(iov_base: buffer.baseAddress!, iov_len: maxSize)
7592
var remote = iovec(
76-
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(address)), iov_len: Int(maxSize))
93+
iov_base: UnsafeMutableRawPointer(bitPattern: UInt(address)), iov_len: maxSize)
7794
let bytesRead = process_vm_readv(self.pid, &local, 1, &remote, 1, 0)
7895
initCount = bytesRead / MemoryLayout<T>.stride
7996
}
@@ -84,4 +101,14 @@ public class Process {
84101

85102
return array
86103
}
104+
105+
public func writeMem(remoteAddr: UInt64, localAddr: UnsafeRawPointer, len: UInt) throws {
106+
var local = iovec(iov_base: UnsafeMutableRawPointer(mutating: localAddr), iov_len: len)
107+
var remote = iovec(iov_base: UnsafeMutableRawPointer(bitPattern: UInt(remoteAddr)), iov_len: len)
108+
109+
let bytesWritten = process_vm_writev(self.pid, &local, 1, &remote, 1, 0)
110+
guard bytesWritten == len else {
111+
throw ProcessError.processVmWriteFailure(pid: self.pid, address: remoteAddr, size: UInt64(len))
112+
}
113+
}
87114
}

0 commit comments

Comments
 (0)