Skip to content

Commit ea000d6

Browse files
authored
Merge pull request #77938 from andrurogerz/swift-inspect-linux
[swift-inspect] implement basic GNU/Linux support
2 parents 0f2637c + 627f787 commit ea000d6

File tree

17 files changed

+834
-4
lines changed

17 files changed

+834
-4
lines changed

tools/swift-inspect/Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@ 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])),
2223
],
2324
swiftSettings: [.unsafeFlags(["-parse-as-library"])]),
2425
.target(name: "SwiftInspectClient"),
26+
.target(
27+
name: "SwiftInspectLinux",
28+
dependencies: ["LinuxSystemHeaders"],
29+
path: "Sources/SwiftInspectLinux",
30+
exclude: ["SystemHeaders"],
31+
cSettings: [.define("_GNU_SOURCE", to: "1")]),
32+
.systemLibrary(
33+
name: "LinuxSystemHeaders",
34+
path: "Sources/SwiftInspectLinux/SystemHeaders"),
2535
.systemLibrary(
2636
name: "SwiftInspectClientInterface"),
2737
.testTarget(

tools/swift-inspect/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ In order to build on Windows, some additional parameters must be passed to the b
1616
swift build -Xcc -I%SDKROOT%\usr\include\swift\SwiftRemoteMirror -Xlinker %SDKROOT%\usr\lib\swift\windows\x86_64\swiftRemoteMirror.lib
1717
~~~
1818

19+
#### Linux
20+
21+
In order to build on Linux, some additional parameters must be passed to the build tool to locate the necessary includes and libraries.
22+
23+
~~~
24+
swift build -Xswiftc -I$(git rev-parse --show-toplevel)/include/swift/SwiftRemoteMirror -Xlinker -lswiftRemoteMirror
25+
~~~
26+
1927
#### CMake
2028

2129
In order to build on Windows with CMake, some additional parameters must be passed to the build tool to locate the necessary Swift modules.
@@ -30,9 +38,6 @@ The following inspection operations are available currently.
3038

3139
##### All Platforms
3240

33-
dump-arrays <name-or-pid>
34-
: Print information about array objects in the target
35-
3641
dump-cache-nodes <name-or-pid>
3742
: Print the metadata cache nodes.
3843

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

53+
#### Darwin and Windows Only
54+
55+
dump-arrays <name-or-pid>
56+
: Print information about array objects in the target
57+
4858
##### Darwin Only
4959

5060
dump-concurrency <name-or-pid>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
import Foundation
14+
import LinuxSystemHeaders
15+
16+
// TODO: replace this implementation with general purpose ELF parsing support
17+
// currently private to swift/stdlib/public/Backtrace.
18+
class ElfFile {
19+
public enum ELFError: Error {
20+
case notELF64(_ filePath: String, _ description: String = "")
21+
case malformedFile(_ filePath: String, _ description: String = "")
22+
}
23+
24+
public typealias SymbolMap = [String: (start: UInt64, end: UInt64)]
25+
26+
let filePath: String
27+
let fileData: Data
28+
let ehdr: Elf64_Ehdr
29+
30+
public init(filePath: String) throws {
31+
self.filePath = filePath
32+
33+
self.fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .alwaysMapped)
34+
35+
let ident = fileData.prefix(upTo: Int(EI_NIDENT))
36+
37+
guard String(bytes: ident.prefix(Int(SELFMAG)), encoding: .utf8) == ELFMAG else {
38+
throw ELFError.notELF64(filePath, "\(ident.prefix(Int(SELFMAG))) != ELFMAG")
39+
}
40+
41+
guard ident[Int(EI_CLASS)] == ELFCLASS64 else {
42+
throw ELFError.notELF64(filePath, "\(ident[Int(EI_CLASS)]) != ELFCLASS64")
43+
}
44+
45+
let ehdrSize = MemoryLayout<Elf64_Ehdr>.size
46+
self.ehdr = fileData[0..<ehdrSize].withUnsafeBytes { $0.load(as: Elf64_Ehdr.self) }
47+
}
48+
49+
// returns a map of symbol names to their offset range in file (+ baseAddress)
50+
public func loadSymbols(baseAddress: UInt64 = 0) throws -> SymbolMap {
51+
guard let sectionCount = UInt(exactly: self.ehdr.e_shnum) else {
52+
throw ELFError.malformedFile(
53+
self.filePath, "invalid Elf64_Ehdr.e_shnum: \(self.ehdr.e_shnum)")
54+
}
55+
56+
var symbols: SymbolMap = [:]
57+
for sectionIndex in 0..<sectionCount {
58+
let shdr: Elf64_Shdr = try self.loadShdr(index: sectionIndex)
59+
guard shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM else { continue }
60+
61+
let symTableData: Data = try self.loadSection(shdr)
62+
let symTable: [Elf64_Sym] = symTableData.withUnsafeBytes {
63+
Array($0.bindMemory(to: Elf64_Sym.self))
64+
}
65+
66+
guard shdr.sh_entsize == MemoryLayout<Elf64_Sym>.size else {
67+
throw ELFError.malformedFile(self.filePath, "invalid Elf64_Shdr.sh_entsize")
68+
}
69+
70+
// the link field in the section header for a symbol table section refers
71+
// to the index of the string table section containing the symbol names
72+
guard let linkIndex = UInt(exactly: shdr.sh_link) else {
73+
throw ELFError.malformedFile(self.filePath, "invalid Elf64_Shdr.sh_link: \(shdr.sh_link)")
74+
}
75+
76+
let shdrLink: Elf64_Shdr = try self.loadShdr(index: UInt(linkIndex))
77+
guard shdrLink.sh_type == SHT_STRTAB else {
78+
throw ELFError.malformedFile(self.filePath, "linked section not SHT_STRTAB")
79+
}
80+
81+
// load the entire contents of the string table into memory
82+
let strTableData: Data = try self.loadSection(shdrLink)
83+
let strTable: [UInt8] = strTableData.withUnsafeBytes {
84+
Array($0.bindMemory(to: UInt8.self))
85+
}
86+
87+
let symCount = Int(shdr.sh_size / shdr.sh_entsize)
88+
for symIndex in 0..<symCount {
89+
let sym = symTable[symIndex]
90+
guard sym.st_shndx != SHN_UNDEF, sym.st_value != 0, sym.st_size != 0 else { continue }
91+
92+
// sym.st_name is a byte offset into the string table
93+
guard let strStart = Int(exactly: sym.st_name), strStart < strTable.count else {
94+
throw ELFError.malformedFile(self.filePath, "invalid string table offset: \(sym.st_name)")
95+
}
96+
97+
guard let strEnd = strTable[strStart...].firstIndex(of: 0),
98+
let symName = String(bytes: strTable[strStart..<strEnd], encoding: .utf8)
99+
else {
100+
throw ELFError.malformedFile(self.filePath, "invalid string @ offset \(strStart)")
101+
}
102+
103+
// rebase the symbol value on the base address provided by the caller
104+
let symStart = sym.st_value + baseAddress
105+
symbols[symName] = (start: symStart, end: symStart + sym.st_size)
106+
}
107+
}
108+
109+
return symbols
110+
}
111+
112+
// returns the Elf64_Shdr at the specified index
113+
internal func loadShdr(index: UInt) throws -> Elf64_Shdr {
114+
guard index < self.ehdr.e_shnum else {
115+
throw ELFError.malformedFile(
116+
self.filePath, "section index \(index) >= Elf64_Ehdr.e_shnum \(self.ehdr.e_shnum))")
117+
}
118+
119+
let shdrSize = MemoryLayout<Elf64_Shdr>.size
120+
guard shdrSize == self.ehdr.e_shentsize else {
121+
throw ELFError.malformedFile(self.filePath, "Elf64_Ehdr.e_shentsize != \(shdrSize)")
122+
}
123+
124+
let shdrOffset = Int(self.ehdr.e_shoff) + Int(index) * shdrSize
125+
let shdrData = self.fileData[shdrOffset..<(shdrOffset + shdrSize)]
126+
return shdrData.withUnsafeBytes { $0.load(as: Elf64_Shdr.self) }
127+
}
128+
129+
// returns all data in the specified section
130+
internal func loadSection(_ shdr: Elf64_Shdr) throws -> Data {
131+
guard let sectionSize = Int(exactly: shdr.sh_size) else {
132+
throw ELFError.malformedFile(self.filePath, "Elf64_Shdr.sh_size too large \(shdr.sh_size)")
133+
}
134+
135+
guard let fileOffset = Int(exactly: shdr.sh_offset) else {
136+
throw ELFError.malformedFile(
137+
self.filePath, "Elf64_Shdr.sh_offset too large \(shdr.sh_offset)")
138+
}
139+
140+
return self.fileData[fileOffset..<(fileOffset + sectionSize)]
141+
}
142+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
import Foundation
14+
import LinuxSystemHeaders
15+
16+
class LinkMap {
17+
public enum LinkMapError: Error {
18+
case failedLoadingAuxVec(for: pid_t)
19+
case missingAuxVecEntry(for: pid_t, _ tag: Int32)
20+
case malformedELF(for: pid_t, _ description: String)
21+
}
22+
23+
public struct Entry {
24+
let baseAddress: UInt64
25+
let moduleName: String
26+
}
27+
28+
public let entries: [Entry]
29+
30+
public init(for process: Process) throws {
31+
let auxVec = try Self.loadAuxVec(for: process.pid)
32+
guard let phdrAddr = auxVec[AT_PHDR] else {
33+
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHDR)
34+
}
35+
36+
guard let phdrSize = auxVec[AT_PHENT] else {
37+
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHENT)
38+
}
39+
40+
guard let phdrCount = auxVec[AT_PHNUM] else {
41+
throw LinkMapError.missingAuxVecEntry(for: process.pid, AT_PHNUM)
42+
}
43+
44+
guard phdrSize == MemoryLayout<Elf64_Phdr>.size else {
45+
throw LinkMapError.malformedELF(for: process.pid, "AT_PHENT invalid size: \(phdrSize)")
46+
}
47+
48+
// determine the base load address for the executable file and locate the
49+
// dynamic segment
50+
var dynamicSegment: Elf64_Phdr? = nil
51+
var baseLoadSegment: Elf64_Phdr? = nil
52+
for i in 0...phdrCount {
53+
let address: UInt64 = phdrAddr + i * phdrSize
54+
let phdr: Elf64_Phdr = try process.readStruct(address: address)
55+
56+
switch phdr.p_type {
57+
case UInt32(PT_LOAD):
58+
// chose the PT_LOAD segment with the lowest p_vaddr value, which will
59+
// typically be zero
60+
if let loadSegment = baseLoadSegment {
61+
if phdr.p_vaddr < loadSegment.p_vaddr { baseLoadSegment = phdr }
62+
} else {
63+
baseLoadSegment = phdr
64+
}
65+
66+
case UInt32(PT_DYNAMIC):
67+
guard dynamicSegment == nil else {
68+
throw LinkMapError.malformedELF(for: process.pid, "multiple PT_DYNAMIC segments found")
69+
}
70+
dynamicSegment = phdr
71+
72+
default: continue
73+
}
74+
}
75+
76+
guard let dynamicSegment = dynamicSegment else {
77+
throw LinkMapError.malformedELF(for: process.pid, "PT_DYNAMIC segment not found")
78+
}
79+
80+
guard let baseLoadSegment = baseLoadSegment else {
81+
throw LinkMapError.malformedELF(for: process.pid, "PT_LOAD segment not found")
82+
}
83+
84+
let ehdrSize = MemoryLayout<Elf64_Ehdr>.size
85+
let loadAddr: UInt64 = phdrAddr - UInt64(ehdrSize)
86+
let baseAddr: UInt64 = loadAddr - baseLoadSegment.p_vaddr
87+
let dynamicSegmentAddr: UInt64 = baseAddr + dynamicSegment.p_vaddr
88+
89+
// parse through the dynamic segment to find the location of the .debug section
90+
var rDebugEntry: Elf64_Dyn? = nil
91+
let entrySize = MemoryLayout<Elf64_Dyn>.size
92+
let dynamicEntryCount = UInt(dynamicSegment.p_memsz / UInt64(entrySize))
93+
for i in 0...dynamicEntryCount {
94+
let address: UInt64 = dynamicSegmentAddr + UInt64(i) * UInt64(entrySize)
95+
let dyn: Elf64_Dyn = try process.readStruct(address: address)
96+
if dyn.d_tag == DT_DEBUG {
97+
rDebugEntry = dyn
98+
break
99+
}
100+
}
101+
102+
guard let rDebugEntry = rDebugEntry else {
103+
throw LinkMapError.malformedELF(for: process.pid, "DT_DEBUG not found in dynamic segment")
104+
}
105+
106+
let rDebugAddr: UInt64 = rDebugEntry.d_un.d_val
107+
let rDebug: r_debug = try process.readStruct(address: rDebugAddr)
108+
109+
var entries: [Entry] = []
110+
var linkMapAddr = UInt(bitPattern: rDebug.r_map)
111+
while linkMapAddr != 0 {
112+
let linkMap: link_map = try process.readStruct(address: UInt64(linkMapAddr))
113+
let nameAddr = UInt(bitPattern: linkMap.l_name)
114+
let name = try process.readString(address: UInt64(nameAddr))
115+
entries.append(Entry(baseAddress: linkMap.l_addr, moduleName: name))
116+
117+
linkMapAddr = UInt(bitPattern: linkMap.l_next)
118+
}
119+
120+
self.entries = entries
121+
}
122+
123+
// loads the auxiliary vector for a 64-bit process
124+
static func loadAuxVec(for pid: pid_t) throws -> [Int32: UInt64] {
125+
guard let data = ProcFS.loadFile(for: pid, "auxv") else {
126+
throw LinkMapError.failedLoadingAuxVec(for: pid)
127+
}
128+
129+
return data.withUnsafeBytes {
130+
// in a 64-bit process, aux vector is an array of 8-byte pairs
131+
let count = $0.count / MemoryLayout<(UInt64, UInt64)>.stride
132+
let auxVec = Array($0.bindMemory(to: (UInt64, UInt64).self)[..<count])
133+
134+
var entries: [Int32: UInt64] = [:]
135+
for (rawTag, value) in auxVec {
136+
// the AT_ constants defined in linux/auxv.h are imported as Int32
137+
guard let tag = Int32(exactly: rawTag) else { continue }
138+
entries[tag] = UInt64(value)
139+
}
140+
141+
return entries
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)