Skip to content

[swift-inspect] implement basic GNU/Linux support #77938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ee5d15
[linux] swift-inspect support for Linux
andrurogerz Dec 3, 2024
049af2e
[linux] add Linux build instructions to README.txt
andrurogerz Dec 3, 2024
59d39a1
[linux] exclude dump-array support
andrurogerz Dec 3, 2024
4906ed9
[linux] define _GNU_SOURCE for process_vm_readv
andrurogerz Dec 3, 2024
6dc7c32
address some code review comments
andrurogerz Dec 4, 2024
31460f6
specify -Xlinker instead of -Xswiftc in README.md
andrurogerz Dec 4, 2024
aa6bfab
PR feedback: fix typo in README.md
andrurogerz Dec 4, 2024
9e5f0ca
PR feedback: update copyright to 2024
andrurogerz Dec 4, 2024
3b0e5bb
consistent use of /proc
andrurogerz Dec 4, 2024
671db08
PR feedback: use precondition in place of assert
andrurogerz Dec 4, 2024
5a277d4
PR feedback: short-circuit return on readArray of 0 items
andrurogerz Dec 4, 2024
48cd587
PR feedback: add Process.readRawString to get byte count in GetString…
andrurogerz Dec 4, 2024
6e466c4
minimize ELF parsing support to ELF64 only
andrurogerz Dec 4, 2024
e702be9
PR feedback: rework error definitions
andrurogerz Dec 4, 2024
278f90c
PR feedback: fix typo
andrurogerz Dec 4, 2024
f669299
properly override the Free property in LinuxRemoteProcess
andrurogerz Dec 5, 2024
61f6b0e
PR Feedback: make ProcFS an enum instead of a class
andrurogerz Dec 5, 2024
3aef500
formatting fixes and missed copyright date updates
andrurogerz Dec 10, 2024
b7dff42
PR feedback
andrurogerz Dec 16, 2024
b121882
use memory-mapped IO instead of file reads
andrurogerz Dec 16, 2024
627f787
PR feedback in ElfFile.init
andrurogerz Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tools/swift-inspect/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ 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])),
],
swiftSettings: [.unsafeFlags(["-parse-as-library"])]),
.target(name: "SwiftInspectClient"),
.target(
name: "SwiftInspectLinux",
dependencies: ["LinuxSystemHeaders"],
path: "Sources/SwiftInspectLinux",
exclude: ["SystemHeaders"],
cSettings: [.define("_GNU_SOURCE", to: "1")]),
.systemLibrary(
name: "LinuxSystemHeaders",
path: "Sources/SwiftInspectLinux/SystemHeaders"),
.systemLibrary(
name: "SwiftInspectClientInterface"),
.testTarget(
Expand Down
16 changes: 13 additions & 3 deletions tools/swift-inspect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ In order to build on Windows, some additional parameters must be passed to the b
swift build -Xcc -I%SDKROOT%\usr\include\swift\SwiftRemoteMirror -Xlinker %SDKROOT%\usr\lib\swift\windows\x86_64\swiftRemoteMirror.lib
~~~

#### Linux

In order to build on Linux, some additional parameters must be passed to the build tool to locate the necessary includes and libraries.

~~~
swift build -Xswiftc -I$(git rev-parse --show-toplevel)/include/swift/SwiftRemoteMirror -Xlinker -lswiftRemoteMirror
~~~

#### 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.
Expand All @@ -30,9 +38,6 @@ The following inspection operations are available currently.

##### All Platforms

dump-arrays <name-or-pid>
: Print information about array objects in the target

dump-cache-nodes <name-or-pid>
: Print the metadata cache nodes.

Expand All @@ -45,6 +50,11 @@ dump-generic-metadata <name-or-pid> [--backtrace] [--backtrace-long]
dump-raw-metadata <name-or-pid> [--backtrace] [--backtrace-long]
: Print metadata allocations.

#### Darwin and Windows Only

dump-arrays <name-or-pid>
: Print information about array objects in the target

##### Darwin Only

dump-concurrency <name-or-pid>
Expand Down
142 changes: 142 additions & 0 deletions tools/swift-inspect/Sources/SwiftInspectLinux/ElfFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// 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

// TODO: replace this implementation with general purpose ELF parsing support
// currently private to swift/stdlib/public/Backtrace.
class ElfFile {
public enum ELFError: Error {
case notELF64(_ filePath: String, _ description: String = "")
case malformedFile(_ filePath: String, _ description: String = "")
}

public typealias SymbolMap = [String: (start: UInt64, end: UInt64)]

let filePath: String
let fileData: Data
let ehdr: Elf64_Ehdr

public init(filePath: String) throws {
self.filePath = filePath

self.fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .alwaysMapped)

let ident = fileData.prefix(upTo: Int(EI_NIDENT))

guard String(bytes: ident.prefix(Int(SELFMAG)), encoding: .utf8) == ELFMAG else {
throw ELFError.notELF64(filePath, "\(ident.prefix(Int(SELFMAG))) != ELFMAG")
}

guard ident[Int(EI_CLASS)] == ELFCLASS64 else {
throw ELFError.notELF64(filePath, "\(ident[Int(EI_CLASS)]) != ELFCLASS64")
}

let ehdrSize = MemoryLayout<Elf64_Ehdr>.size
self.ehdr = fileData[0..<ehdrSize].withUnsafeBytes { $0.load(as: Elf64_Ehdr.self) }
}

// returns a map of symbol names to their offset range in file (+ baseAddress)
public func loadSymbols(baseAddress: UInt64 = 0) throws -> SymbolMap {
guard let sectionCount = UInt(exactly: self.ehdr.e_shnum) else {
throw ELFError.malformedFile(
self.filePath, "invalid Elf64_Ehdr.e_shnum: \(self.ehdr.e_shnum)")
}

var symbols: SymbolMap = [:]
for sectionIndex in 0..<sectionCount {
let shdr: Elf64_Shdr = try self.loadShdr(index: sectionIndex)
guard shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM else { continue }

let symTableData: Data = try self.loadSection(shdr)
let symTable: [Elf64_Sym] = symTableData.withUnsafeBytes {
Array($0.bindMemory(to: Elf64_Sym.self))
}

guard shdr.sh_entsize == MemoryLayout<Elf64_Sym>.size else {
throw ELFError.malformedFile(self.filePath, "invalid Elf64_Shdr.sh_entsize")
}

// the link field in the section header for a symbol table section refers
// to the index of the string table section containing the symbol names
guard let linkIndex = UInt(exactly: shdr.sh_link) else {
throw ELFError.malformedFile(self.filePath, "invalid Elf64_Shdr.sh_link: \(shdr.sh_link)")
}

let shdrLink: Elf64_Shdr = try self.loadShdr(index: UInt(linkIndex))
guard shdrLink.sh_type == SHT_STRTAB else {
throw ELFError.malformedFile(self.filePath, "linked section not SHT_STRTAB")
}

// load the entire contents of the string table into memory
let strTableData: Data = try self.loadSection(shdrLink)
let strTable: [UInt8] = strTableData.withUnsafeBytes {
Array($0.bindMemory(to: UInt8.self))
}

let symCount = Int(shdr.sh_size / shdr.sh_entsize)
for symIndex in 0..<symCount {
let sym = symTable[symIndex]
guard sym.st_shndx != SHN_UNDEF, sym.st_value != 0, sym.st_size != 0 else { continue }

// sym.st_name is a byte offset into the string table
guard let strStart = Int(exactly: sym.st_name), strStart < strTable.count else {
throw ELFError.malformedFile(self.filePath, "invalid string table offset: \(sym.st_name)")
}

guard let strEnd = strTable[strStart...].firstIndex(of: 0),
let symName = String(bytes: strTable[strStart..<strEnd], encoding: .utf8)
else {
throw ELFError.malformedFile(self.filePath, "invalid string @ offset \(strStart)")
}

// rebase the symbol value on the base address provided by the caller
let symStart = sym.st_value + baseAddress
symbols[symName] = (start: symStart, end: symStart + sym.st_size)
}
}

return symbols
}

// returns the Elf64_Shdr at the specified index
internal func loadShdr(index: UInt) throws -> Elf64_Shdr {
guard index < self.ehdr.e_shnum else {
throw ELFError.malformedFile(
self.filePath, "section index \(index) >= Elf64_Ehdr.e_shnum \(self.ehdr.e_shnum))")
}

let shdrSize = MemoryLayout<Elf64_Shdr>.size
guard shdrSize == self.ehdr.e_shentsize else {
throw ELFError.malformedFile(self.filePath, "Elf64_Ehdr.e_shentsize != \(shdrSize)")
}

let shdrOffset = Int(self.ehdr.e_shoff) + Int(index) * shdrSize
let shdrData = self.fileData[shdrOffset..<(shdrOffset + shdrSize)]
return shdrData.withUnsafeBytes { $0.load(as: Elf64_Shdr.self) }
}

// returns all data in the specified section
internal func loadSection(_ shdr: Elf64_Shdr) throws -> Data {
guard let sectionSize = Int(exactly: shdr.sh_size) else {
throw ELFError.malformedFile(self.filePath, "Elf64_Shdr.sh_size too large \(shdr.sh_size)")
}

guard let fileOffset = Int(exactly: shdr.sh_offset) else {
throw ELFError.malformedFile(
self.filePath, "Elf64_Shdr.sh_offset too large \(shdr.sh_offset)")
}

return self.fileData[fileOffset..<(fileOffset + sectionSize)]
}
}
144 changes: 144 additions & 0 deletions tools/swift-inspect/Sources/SwiftInspectLinux/LinkMap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//===----------------------------------------------------------------------===//
//
// 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

class LinkMap {
public enum LinkMapError: Error {
case failedLoadingAuxVec(for: pid_t)
case missingAuxVecEntry(for: pid_t, _ tag: Int32)
case malformedELF(for: pid_t, _ description: String)
}

public struct Entry {
let baseAddress: UInt64
let moduleName: String
}

public let entries: [Entry]

public init(for process: Process) throws {
let auxVec = try Self.loadAuxVec(for: process.pid)
guard let phdrAddr = auxVec[AT_PHDR] else {
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHDR)
}

guard let phdrSize = auxVec[AT_PHENT] else {
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHENT)
}

guard let phdrCount = auxVec[AT_PHNUM] else {
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHNUM)
}

guard phdrSize == MemoryLayout<Elf64_Phdr>.size else {
throw LinkMapError.malformedELF(for: process.pid, "AT_PHENT invalid size: \(phdrSize)")
}

// determine the base load address for the executable file and locate the
// dynamic segment
var dynamicSegment: Elf64_Phdr? = nil
var baseLoadSegment: Elf64_Phdr? = nil
for i in 0...phdrCount {
let address: UInt64 = phdrAddr + i * phdrSize
let phdr: Elf64_Phdr = try process.readStruct(address: address)

switch phdr.p_type {
case UInt32(PT_LOAD):
// chose the PT_LOAD segment with the lowest p_vaddr value, which will
// typically be zero
if let loadSegment = baseLoadSegment {
if phdr.p_vaddr < loadSegment.p_vaddr { baseLoadSegment = phdr }
} else {
baseLoadSegment = phdr
}

case UInt32(PT_DYNAMIC):
guard dynamicSegment == nil else {
throw LinkMapError.malformedELF(for: process.pid, "multiple PT_DYNAMIC segments found")
}
dynamicSegment = phdr

default: continue
}
}

guard let dynamicSegment = dynamicSegment else {
throw LinkMapError.malformedELF(for: process.pid, "PT_DYNAMIC segment not found")
}

guard let baseLoadSegment = baseLoadSegment else {
throw LinkMapError.malformedELF(for: process.pid, "PT_LOAD segment not found")
}

let ehdrSize = MemoryLayout<Elf64_Ehdr>.size
let loadAddr: UInt64 = phdrAddr - UInt64(ehdrSize)
let baseAddr: UInt64 = loadAddr - baseLoadSegment.p_vaddr
let dynamicSegmentAddr: UInt64 = baseAddr + dynamicSegment.p_vaddr

// parse through the dynamic segment to find the location of the .debug section
var rDebugEntry: Elf64_Dyn? = nil
let entrySize = MemoryLayout<Elf64_Dyn>.size
let dynamicEntryCount = UInt(dynamicSegment.p_memsz / UInt64(entrySize))
for i in 0...dynamicEntryCount {
let address: UInt64 = dynamicSegmentAddr + UInt64(i) * UInt64(entrySize)
let dyn: Elf64_Dyn = try process.readStruct(address: address)
if dyn.d_tag == DT_DEBUG {
rDebugEntry = dyn
break
}
}

guard let rDebugEntry = rDebugEntry else {
throw LinkMapError.malformedELF(for: process.pid, "DT_DEBUG not found in dynamic segment")
}

let rDebugAddr: UInt64 = rDebugEntry.d_un.d_val
let rDebug: r_debug = try process.readStruct(address: rDebugAddr)

var entries: [Entry] = []
var linkMapAddr = UInt(bitPattern: rDebug.r_map)
while linkMapAddr != 0 {
let linkMap: link_map = try process.readStruct(address: UInt64(linkMapAddr))
let nameAddr = UInt(bitPattern: linkMap.l_name)
let name = try process.readString(address: UInt64(nameAddr))
entries.append(Entry(baseAddress: linkMap.l_addr, moduleName: name))

linkMapAddr = UInt(bitPattern: linkMap.l_next)
}

self.entries = entries
}

// loads the auxiliary vector for a 64-bit process
static func loadAuxVec(for pid: pid_t) throws -> [Int32: UInt64] {
guard let data = ProcFS.loadFile(for: pid, "auxv") else {
throw LinkMapError.failedLoadingAuxVec(for: pid)
}

return data.withUnsafeBytes {
// in a 64-bit process, aux vector is an array of 8-byte pairs
let count = $0.count / MemoryLayout<(UInt64, UInt64)>.stride
let auxVec = Array($0.bindMemory(to: (UInt64, UInt64).self)[..<count])

var entries: [Int32: UInt64] = [:]
for (rawTag, value) in auxVec {
// the AT_ constants defined in linux/auxv.h are imported as Int32
guard let tag = Int32(exactly: rawTag) else { continue }
entries[tag] = UInt64(value)
}

return entries
}
}
}
Loading