Skip to content

Commit

Permalink
Merge pull request #123 from benfred/os_thread_activity
Browse files Browse the repository at this point in the history
Get thread activity from the OS
  • Loading branch information
benfred authored Jul 7, 2019
2 parents cc6cbf4 + 3ef940a commit 67c6861
Show file tree
Hide file tree
Showing 17 changed files with 783 additions and 345 deletions.
6 changes: 3 additions & 3 deletions remoteprocess/examples/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn get_backtrace(pid: remoteprocess::Pid) -> Result<(), remoteprocess::Error> {
// Create a stack unwind object, and use it to get the stack for each thread
let unwinder = process.unwinder()?;
for thread in process.threads()?.iter() {
println!("Thread 0x{:0x} - {}", thread.id()?, if thread.active()? { "running" } else { "idle" });
println!("Thread {} - {}", thread.id()?, if thread.active()? { "running" } else { "idle" });

// lock the thread to get a consistent snapshot (unwinding will fail otherwise)
// Note: the thread will appear idle when locked, so wee are calling
Expand All @@ -24,8 +24,8 @@ fn get_backtrace(pid: remoteprocess::Pid) -> Result<(), remoteprocess::Error> {

// Lookup the current stack frame containing a filename/function/linenumber etc
// for the current address
unwinder.symbolicate(ip, &mut |sf| {
println!("{}", sf);
unwinder.symbolicate(ip, true, &mut |sf| {
println!("\t{}", sf);
})?;
}
}
Expand Down
2 changes: 1 addition & 1 deletion remoteprocess/src/dylib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ macro_rules! dlsym {
static $name: self::dylib::Symbol<unsafe extern fn($($t),*) -> $ret> =
self::dylib::Symbol {
name: concat!(stringify!($name), "\0"),
addr: ::std::sync::atomic::ATOMIC_USIZE_INIT,
addr: ::std::sync::atomic::AtomicUsize::new(0),
_marker: ::std::marker::PhantomData,
};
)*)
Expand Down
7 changes: 3 additions & 4 deletions remoteprocess/src/linux/gimli_unwinder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use proc_maps;

use gimli::{EhFrame, BaseAddresses, Pointer, NativeEndian, EhFrameHdr};
use goblin::elf::program_header::*;
use gimli;

use gimli::EndianRcSlice;
type RcReader = EndianRcSlice<NativeEndian>;
Expand Down Expand Up @@ -166,11 +165,11 @@ impl Unwinder {
Ok(Cursor{registers: thread.registers()?, parent: self, initial_frame: true})
}

pub fn symbolicate(&self, addr: u64, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
pub fn symbolicate(&self, addr: u64, line_info: bool, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
let binary = match self.get_binary(addr) {
Some(binary) => binary,
None => {
return Err(gimli::Error::NoUnwindInfoForAddress.into());
return Err(Error::NoBinaryForAddress(addr));
}
};
if binary.filename != "[vdso]" {
Expand All @@ -180,7 +179,7 @@ impl Unwinder {
*symbols = Some(SymbolData::new(&binary.filename, binary.offset));
}
match symbols.as_ref() {
Some(Ok(symbols)) => symbols.symbolicate(addr, callback),
Some(Ok(symbols)) => symbols.symbolicate(addr, line_info, callback),
_ => {
// we probably failed to load the symbols (maybe goblin v0.15 dependency causing error
// in gimli/object crate). Rather than fail add a stub
Expand Down
13 changes: 8 additions & 5 deletions remoteprocess/src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ pub mod libunwind;
mod gimli_unwinder;
#[cfg(unwind)]
mod symbolication;
use libc::{c_void, pid_t};
use libc::pid_t;
#[cfg(unwind)]
use libc::c_void;

use nix::{self, sys::wait, sys::ptrace, {sched::{setns, CloneFlags}}};
use std::io::Read;
Expand All @@ -25,6 +27,7 @@ pub use self::libunwind::{LibUnwind};
use read_process_memory::{CopyAddress};

pub type Pid = pid_t;
pub type Tid = pid_t;

pub struct Process {
pub pid: Pid,
Expand Down Expand Up @@ -106,8 +109,8 @@ impl super::ProcessMemory for Process {
}

impl Thread {
pub fn new(threadid: i32) -> Thread{
Thread{tid: nix::unistd::Pid::from_raw(threadid)}
pub fn new(threadid: i32) -> Result<Thread, Error> {
Ok(Thread{tid: nix::unistd::Pid::from_raw(threadid)})
}

pub fn lock(&self) -> Result<ThreadLock, Error> {
Expand All @@ -128,8 +131,8 @@ impl Thread {
}
}

pub fn id(&self) -> Result<u64, Error> {
Ok(self.tid.as_raw() as u64)
pub fn id(&self) -> Result<Tid, Error> {
Ok(self.tid.as_raw())
}

pub fn active(&self) -> Result<bool, Error> {
Expand Down
51 changes: 28 additions & 23 deletions remoteprocess/src/linux/symbolication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,36 +51,41 @@ impl SymbolData {
Ok(SymbolData{ctx, offset, dynamic_symbols, symbols, filename: filename.to_owned()})
}

pub fn symbolicate(&self, addr: u64, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
pub fn symbolicate(&self, addr: u64, line_info: bool, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
let mut ret = StackFrame{line:None, filename: None, function: None, addr, module: self.filename.clone()};

// get the address before relocations
let offset = addr - self.offset;
let mut has_debug_info = false;

// addr2line0.8 uses an older version of gimli (0.0.19) than we are using here (0.0.21),
// this means we can't use the type of the error returned ourselves here since the
// type alias is private. hack by re-mapping the error
let error_handler = |e| Error::Other(format!("addr2line error: {:?}", e));

// if we have debugging info, get the appropiate stack frames for the adresss
let mut frames = self.ctx.find_frames(offset).map_err(error_handler)?;
while let Some(frame) = frames.next().map_err(error_handler)? {
has_debug_info = true;
if let Some(func) = frame.function {
ret.function = Some(func.raw_name().map_err(error_handler)?.to_string());
}
if let Some(loc) = frame.location {
ret.line = loc.line;
if let Some(file) = loc.file.as_ref() {
ret.filename = Some(file.to_string());

// if we are being asked for line information, sue gimli addr2line to look up the debug info
// (this is slow, and not necessary all the time which is why we are skipping)
if line_info {
let mut has_debug_info = false;

// addr2line0.8 uses an older version of gimli (0.0.19) than we are using here (0.0.21),
// this means we can't use the type of the error returned ourselves here since the
// type alias is private. hack by re-mapping the error
let error_handler = |e| Error::Other(format!("addr2line error: {:?}", e));

// if we have debugging info, get the appropiate stack frames for the adresss
let mut frames = self.ctx.find_frames(offset).map_err(error_handler)?;
while let Some(frame) = frames.next().map_err(error_handler)? {
has_debug_info = true;
if let Some(func) = frame.function {
ret.function = Some(func.raw_name().map_err(error_handler)?.to_string());
}
if let Some(loc) = frame.location {
ret.line = loc.line;
if let Some(file) = loc.file.as_ref() {
ret.filename = Some(file.to_string());
}
}
callback(&ret);
}
callback(&ret);
}

if has_debug_info {
return Ok(())
if has_debug_info {
return Ok(())
}
}

// otherwise try getting the function name from the symbols
Expand Down
13 changes: 11 additions & 2 deletions remoteprocess/src/osx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub use self::unwinder::Unwinder;
use libproc::libproc::proc_pid::{pidpath, pidinfo, PIDInfo, PidInfoFlavor};

pub type Pid = pid_t;
pub type Tid = u32;

pub struct Process {
pub pid: Pid,
Expand All @@ -35,7 +36,7 @@ pub struct Process {

#[derive(Eq, PartialEq, Hash, Copy, Clone)]
pub struct Thread {
pub tid: u32
pub tid: Tid
}

impl Process {
Expand Down Expand Up @@ -95,7 +96,15 @@ use self::mach_thread_bindings::{thread_info, thread_basic_info, thread_identifi


impl Thread {
pub fn id(&self) -> Result<u64, Error> {
pub fn new(tid: Tid) -> Result<Thread, Error> {
Ok(Thread{tid})
}

pub fn id(&self) -> Result<Tid, Error> {
Ok(self.tid)
}

pub fn thread_handle(&self) -> Result<u64, Error> {
let thread_id = self.get_thread_identifier_info()?;
Ok(thread_id.thread_handle)
}
Expand Down
2 changes: 1 addition & 1 deletion remoteprocess/src/osx/unwinder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl Unwinder {
Ok(Cursor{registers: thread.registers()?, parent: self, initial_frame: true})
}

pub fn symbolicate(&self, addr: u64, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
pub fn symbolicate(&self, addr: u64, _line_info: bool, callback: &mut FnMut(&StackFrame)) -> Result<(), Error> {
// Get the symbols for the current address
let symbol = unsafe { self.cs.resolve(addr) };

Expand Down
115 changes: 115 additions & 0 deletions remoteprocess/src/windows/generate_syscall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
""" Generates the syscall.rs code """
import argparse
import os
import sys
import json

# We need to map to major/minor/build to the names used in the syscall table
# https://www.gaijin.at/en/infos/windows-version-numbers
OS_VERSION_LOOKUP = {
("Windows Vista", "SP0"): (6, 0, 6000),
("Windows Vista", "SP1"): (6, 0, 6001),
("Windows Vista", "SP2"): (6, 0, 6002),
("Windows 7", "SP0"): (6, 1, 7600),
("Windows 7", "SP1"): (6, 1, 7601),
("Windows 8", "8.0"): (6, 2, 9200),
("Windows 8", "8.1"): (6, 3, 9600),
("Windows 10", "1507"): (10, 0, 10240),
("Windows 10", "1511"): (10, 0, 10586),
("Windows 10", "1607"): (10, 0, 14393),
("Windows 10", "1703"): (10, 0, 15063),
("Windows 10", "1709"): (10, 0, 16299),
("Windows 10", "1803"): (10, 0, 17134),
("Windows 10", "1809"): (10, 0, 17763),
("Windows 10", "1903"): (10, 0, 18362),
}

# I'm not supporting anything prior to vista here
UNSUPPORTED_OS = {"Windows XP", "Windows Server 2003"}

# these seem to have the same NT syscall as the consumer versions
SERVER_OS = {"Windows Server 2008", "Windows Server 2012"}

# how different are the syscall tables here?
OS_SERVER_LOOKUP = {
("Windows Server 2008", "SP0"): (6, 0, 6001),
("Windows Server 2008", "SP2"): (6, 0, 6002),
("Windows Server 2008", "R2"): (6, 1, 7600),
("Windows Server 2012", "SP0"): (6, 2, 9200),
("Windows Server 2012", "R2"): (6, 3, 9600),
("Windows Server 2008", "R2 SP1"): (6, 1, 7601),
}


def generate(inputpath, arch="x64", namefilter=lambda s: not s.startswith("NtWait")):
nt = json.load(open(os.path.join(inputpath, arch, "json", "nt-per-system.json")))
win32k = json.load(open(os.path.join(inputpath, arch, "json", "win32k-per-system.json")))
outputpath = os.path.dirname(os.path.realpath(__file__))
outputfilename = os.path.join(outputpath, f"syscalls_{arch}.rs")

syscallnames = set()
matches = []

for ((osname, osversion), (major, minor, build)) in OS_VERSION_LOOKUP.items():
syscalls = nt[osname][osversion]
syscalls.update(win32k[osname][osversion])
for syscallname, syscallnumber in syscalls.items():
if namefilter and namefilter(syscallname):
continue

matches.append((major, minor, build, syscallnumber, syscallname))
syscallnames.add(syscallname)

matches.sort()
syscallnames = sorted(syscallnames)

print(outputfilename)
with open(outputfilename, "w") as o:
o.write(f"// Automatically generated by '{__file__}' - do not edit\n")
if namefilter:
o.write("// Note that this is a subset of the syscalls, run again with no namefilter\n")
o.write("// for the complete list (which takes forever to compile)\n")
o.write("\n")
o.write("#[allow(non_camel_case_types)]\n\n")

o.write("pub enum Syscall {\n")
for syscall in syscallnames:
o.write(f" {syscall},\n")
o.write("}\n\n")

o.write("pub fn lookup_syscall(major: u32, minor: u32, build: u32, syscall: u32)")
o.write(" -> Option<Syscall> {\n")
o.write(" match (major, minor, build, syscall) {\n")
for major, minor, build, syscallnumber, syscallname in matches:
o.write(f" ({major}, {minor}, {build}, {syscallnumber})")
o.write(f" => Some(Syscall::{syscallname}),\n")
o.write(" _ => None\n")
o.write(" }\n")
o.write("}\n\n")


if __name__ == "__main__":

if sys.platform.startswith("win"):
default_syscall_path = os.path.join(os.getenv("userprofile"), "code", "windows-syscalls")
else:
default_syscall_path = os.path.join(os.getenv("HOME"), "code", "windows-syscalls")

parser = argparse.ArgumentParser(
description="generates syscall.rs", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"--syscallpath",
type=str,
default=default_syscall_path,
dest="syscallpath",
help="path to syscall repo https://github.com/j00ru/windows-syscalls",
)
args = parser.parse_args()

if not os.path.isdir(args.syscallpath):
print(f"Directory '{args.syscallpath}' doesn't exist!")
print("Pass a valid path to the j00ru/windows-syscalls in with --syscallpath <pathname>")
sys.exit(1)

generate(args.syscallpath)
Loading

0 comments on commit 67c6861

Please sign in to comment.