-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[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
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 049af2e
[linux] add Linux build instructions to README.txt
andrurogerz 59d39a1
[linux] exclude dump-array support
andrurogerz 4906ed9
[linux] define _GNU_SOURCE for process_vm_readv
andrurogerz 6dc7c32
address some code review comments
andrurogerz 31460f6
specify -Xlinker instead of -Xswiftc in README.md
andrurogerz aa6bfab
PR feedback: fix typo in README.md
andrurogerz 9e5f0ca
PR feedback: update copyright to 2024
andrurogerz 3b0e5bb
consistent use of /proc
andrurogerz 671db08
PR feedback: use precondition in place of assert
andrurogerz 5a277d4
PR feedback: short-circuit return on readArray of 0 items
andrurogerz 48cd587
PR feedback: add Process.readRawString to get byte count in GetString…
andrurogerz 6e466c4
minimize ELF parsing support to ELF64 only
andrurogerz e702be9
PR feedback: rework error definitions
andrurogerz 278f90c
PR feedback: fix typo
andrurogerz f669299
properly override the Free property in LinuxRemoteProcess
andrurogerz 61f6b0e
PR Feedback: make ProcFS an enum instead of a class
andrurogerz 3aef500
formatting fixes and missed copyright date updates
andrurogerz b7dff42
PR feedback
andrurogerz b121882
use memory-mapped IO instead of file reads
andrurogerz 627f787
PR feedback in ElfFile.init
andrurogerz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
tools/swift-inspect/Sources/SwiftInspectLinux/ElfFile.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "") | ||
} | ||
andrurogerz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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
144
tools/swift-inspect/Sources/SwiftInspectLinux/LinkMap.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.