pykdebugparser 1.4.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/PKG-INFO +1 -2
- pykdebugparser-1.6.0/pykdebugparser/highlight.py +66 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/pykdebugparser.py +70 -8
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/bsd.py +114 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/dyld.py +119 -17
- pykdebugparser-1.6.0/pykdebugparser/trace_handlers/fsystem.py +64 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/mach.py +129 -1
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/trace.py +18 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/traces_parser.py +13 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/PKG-INFO +1 -2
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/SOURCES.txt +2 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/requires.txt +0 -1
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pyproject.toml +1 -2
- pykdebugparser-1.6.0/tests/test_highlight.py +52 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_pykdebugparser.py +80 -0
- pykdebugparser-1.4.0/pykdebugparser/trace_handlers/fsystem.py +0 -22
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/LICENSE +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/README.md +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/__init__.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/__main__.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/callstacks_parser.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/kd_buf_parser.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/kevent.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/os_log_event.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace.codes +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_codes.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/__init__.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/perf.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/turnstile.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/dependency_links.txt +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/entry_points.txt +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/top_level.txt +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/setup.cfg +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_kd_buf_parser.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_kevent.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_os_log_event.py +0 -0
- {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_trace_codes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykdebugparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Python parser for kdebug events
|
|
5
5
|
Author: Matan Perelman
|
|
6
6
|
Author-email: matan1008@gmail.com
|
|
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
|
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
41
|
Requires-Dist: bytestruct>=0.1.0
|
|
42
|
-
Requires-Dist: pygments
|
|
43
42
|
Requires-Dist: click
|
|
44
43
|
Requires-Dist: termcolor
|
|
45
44
|
Provides-Extra: testing
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Syntax highlighting of trace lines, such as `open("/etc/hosts", O_RDONLY), fd: 3`.
|
|
2
|
+
|
|
3
|
+
Lines are colored with the terminal's 16 basic colors, so they follow its theme:
|
|
4
|
+
|
|
5
|
+
- the call or event name at the start, bold;
|
|
6
|
+
- strings (paths, symbols, ...), green;
|
|
7
|
+
- numbers, cyan;
|
|
8
|
+
- constants (flags, enums, signals: `O_RDONLY`, `KERN_SUCCESS`, `SIGKILL`), yellow;
|
|
9
|
+
- an error (`errno: ENOENT(2)`, a `KERN_*` failure), red;
|
|
10
|
+
- the labels of results and fields (`fd:`, `count:`), dim.
|
|
11
|
+
|
|
12
|
+
A trace is mostly the same calls with the same arguments, so the most recent lines are cached.
|
|
13
|
+
"""
|
|
14
|
+
import re
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
|
|
17
|
+
_RESET = '\x1b[0m'
|
|
18
|
+
_BOLD = '\x1b[1m'
|
|
19
|
+
_DIM = '\x1b[2m'
|
|
20
|
+
_RED = '\x1b[31m'
|
|
21
|
+
_GREEN = '\x1b[32m'
|
|
22
|
+
_YELLOW = '\x1b[33m'
|
|
23
|
+
_CYAN = '\x1b[36m'
|
|
24
|
+
|
|
25
|
+
_TOKEN = re.compile(
|
|
26
|
+
r'(?P<string>"[^"]*")'
|
|
27
|
+
r'|(?P<number>-?0[xX][0-9a-fA-F]+|-?\d+)'
|
|
28
|
+
r'|(?P<label>[A-Za-z_][\w ]*?:)(?= |$)'
|
|
29
|
+
r'|(?P<name>[A-Za-z_]\w*)'
|
|
30
|
+
)
|
|
31
|
+
# A constant is all capitals, digits and underscores, with at least one capital.
|
|
32
|
+
_CONSTANT = re.compile(r'[A-Z][A-Z0-9_]*')
|
|
33
|
+
# `KERN_SUCCESS` is a result, not an error.
|
|
34
|
+
_SUCCESS = {'KERN_SUCCESS'}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=1 << 14)
|
|
38
|
+
def highlight(text: str) -> str:
|
|
39
|
+
"""`text` with terminal colors (SGR escape sequences); the text itself is unchanged."""
|
|
40
|
+
out = []
|
|
41
|
+
pos = 0
|
|
42
|
+
error = False # inside the value of an `errno:` result
|
|
43
|
+
first = True
|
|
44
|
+
for match in _TOKEN.finditer(text):
|
|
45
|
+
if match.start() > pos:
|
|
46
|
+
out.append(text[pos:match.start()])
|
|
47
|
+
pos = match.end()
|
|
48
|
+
kind = match.lastgroup
|
|
49
|
+
value = match.group()
|
|
50
|
+
if kind == 'string':
|
|
51
|
+
out.append(_GREEN + value + _RESET)
|
|
52
|
+
elif kind == 'number':
|
|
53
|
+
out.append((_RED if error else _CYAN) + value + _RESET)
|
|
54
|
+
elif kind == 'label':
|
|
55
|
+
error = value == 'errno:'
|
|
56
|
+
out.append((_RED if error else _DIM) + value + _RESET)
|
|
57
|
+
elif first:
|
|
58
|
+
out.append(_BOLD + value + _RESET)
|
|
59
|
+
elif _CONSTANT.fullmatch(value):
|
|
60
|
+
failed = error or (value.startswith('KERN_') and value not in _SUCCESS)
|
|
61
|
+
out.append((_RED if failed else _YELLOW) + value + _RESET)
|
|
62
|
+
else:
|
|
63
|
+
out.append(value)
|
|
64
|
+
first = False
|
|
65
|
+
out.append(text[pos:])
|
|
66
|
+
return ''.join(out)
|
|
@@ -1,25 +1,53 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
2
4
|
import io
|
|
3
5
|
import math
|
|
4
6
|
|
|
5
|
-
from pygments import highlight, lexers, formatters
|
|
6
7
|
from termcolor import colored
|
|
7
8
|
|
|
8
9
|
from pykdebugparser.callstacks_parser import CallstacksParser
|
|
10
|
+
from pykdebugparser.highlight import highlight
|
|
9
11
|
from pykdebugparser.kd_buf_parser import KdBufParser
|
|
10
12
|
from pykdebugparser.kevent import DgbFuncQual
|
|
11
13
|
from pykdebugparser.trace_codes import default_trace_codes
|
|
14
|
+
from pykdebugparser.trace_handlers import UnknownEnumValue
|
|
15
|
+
from pykdebugparser.trace_handlers.trace import TRACE_LOST_EVENTS
|
|
12
16
|
from pykdebugparser.traces_parser import TracesParser
|
|
13
17
|
from pykdebugparser.os_log_event import OsLogEvent
|
|
14
18
|
|
|
15
|
-
c_lexer = lexers.CLexer()
|
|
16
|
-
color_formatter = formatters.TerminalTrueColorFormatter(style='stata-dark')
|
|
17
19
|
|
|
18
20
|
DBG_TRACE = 7
|
|
19
21
|
DBG_FSYSTEM = 3
|
|
20
22
|
DBG_BSD = 4
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
def _fields(trace) -> dict:
|
|
26
|
+
"""A trace's parsed values (its dataclass fields but the raw kevents), as JSON-compatible values."""
|
|
27
|
+
return {field.name: _jsonable(getattr(trace, field.name)) for field in dataclasses.fields(trace)
|
|
28
|
+
if field.name != 'ktraces'}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _jsonable(value):
|
|
32
|
+
if isinstance(value, (bool, str)) or value is None:
|
|
33
|
+
return value
|
|
34
|
+
if isinstance(value, UnknownEnumValue):
|
|
35
|
+
return int(value)
|
|
36
|
+
if isinstance(value, Enum):
|
|
37
|
+
return value.name
|
|
38
|
+
if isinstance(value, (int, float)):
|
|
39
|
+
return value
|
|
40
|
+
if isinstance(value, (bytes, bytearray)):
|
|
41
|
+
return value.hex()
|
|
42
|
+
if isinstance(value, (list, tuple)):
|
|
43
|
+
return [_jsonable(item) for item in value]
|
|
44
|
+
if isinstance(value, dict):
|
|
45
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
46
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
47
|
+
return _fields(value)
|
|
48
|
+
return str(value)
|
|
49
|
+
|
|
50
|
+
|
|
23
51
|
class PyKdebugParser:
|
|
24
52
|
def __init__(self):
|
|
25
53
|
self.filter_tid = None
|
|
@@ -93,11 +121,39 @@ class PyKdebugParser:
|
|
|
93
121
|
if self.filter_process is not None:
|
|
94
122
|
trace_generator = filter(self._filter_process_callback, trace_generator)
|
|
95
123
|
if add_trace_class:
|
|
96
|
-
|
|
124
|
+
# Lost events stay: they tell the reader the trace has a gap.
|
|
125
|
+
trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_TRACE
|
|
126
|
+
or t.ktraces[0].eventid == TRACE_LOST_EVENTS, trace_generator)
|
|
97
127
|
if add_fs_class:
|
|
98
128
|
trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_FSYSTEM, trace_generator)
|
|
99
129
|
return trace_generator
|
|
100
130
|
|
|
131
|
+
def dict_traces(self, kdebug: io.IOBase, trace_codes=None):
|
|
132
|
+
"""The traces as JSON-compatible dicts (see `trace_dict`)."""
|
|
133
|
+
trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
|
|
134
|
+
return map(lambda t: self.trace_dict(t, trace_codes_map), self.traces(kdebug, trace_codes_map))
|
|
135
|
+
|
|
136
|
+
def trace_dict(self, trace, trace_codes=None) -> dict:
|
|
137
|
+
"""
|
|
138
|
+
A trace as JSON-compatible data: when (`timestamp` in mach ticks, and `time` in ISO 8601 when the device's time
|
|
139
|
+
information is set), where (`tid`, `pid`, `process`), what (`event`, the trace code name of its first kevent,
|
|
140
|
+
and `fields`, the parsed values) and `text`, as `formatted_traces` prints it.
|
|
141
|
+
"""
|
|
142
|
+
trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
|
|
143
|
+
first = trace.ktraces[0]
|
|
144
|
+
pid = self.threads_pids.get(first.tid)
|
|
145
|
+
posix_time = self._posix_time(first.timestamp)
|
|
146
|
+
return {
|
|
147
|
+
'timestamp': first.timestamp,
|
|
148
|
+
'time': None if posix_time is None else datetime.fromtimestamp(posix_time, tz=self.timezone).isoformat(),
|
|
149
|
+
'tid': first.tid,
|
|
150
|
+
'pid': pid,
|
|
151
|
+
'process': self.pids_names.get(pid, '') if pid is not None else '',
|
|
152
|
+
'event': trace_codes_map.get(first.eventid, hex(first.eventid)),
|
|
153
|
+
'fields': _fields(trace),
|
|
154
|
+
'text': str(trace),
|
|
155
|
+
}
|
|
156
|
+
|
|
101
157
|
def formatted_traces(self, kdebug: io.IOBase, trace_codes=None):
|
|
102
158
|
return map(lambda t: self._format_trace(t), self.traces(kdebug, trace_codes))
|
|
103
159
|
|
|
@@ -127,13 +183,19 @@ class PyKdebugParser:
|
|
|
127
183
|
process_name = self.pids_names.get(pid, '')
|
|
128
184
|
return self.filter_process == str(pid) or self.filter_process == process_name
|
|
129
185
|
|
|
130
|
-
def
|
|
186
|
+
def _posix_time(self, timestamp):
|
|
187
|
+
"""A kevent timestamp (mach ticks) as POSIX time, or None without the device's time information."""
|
|
131
188
|
if None in (self.mach_absolute_time, self.numer, self.denom, self.usecs_since_epoch, self.timezone):
|
|
132
|
-
return
|
|
189
|
+
return None
|
|
133
190
|
offset_usec = (
|
|
134
191
|
((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
|
|
135
192
|
)
|
|
136
|
-
|
|
193
|
+
return (self.usecs_since_epoch + offset_usec) / 1000000
|
|
194
|
+
|
|
195
|
+
def _format_timestamp(self, timestamp):
|
|
196
|
+
posix_time = self._posix_time(timestamp)
|
|
197
|
+
if posix_time is None:
|
|
198
|
+
return str(timestamp) + ' '
|
|
137
199
|
# Split to seconds and microseconds exactly like `datetime.fromtimestamp` does (round half to even), so
|
|
138
200
|
# the date and time part can be formatted only once per second.
|
|
139
201
|
fraction, seconds = math.modf(posix_time)
|
|
@@ -187,7 +249,7 @@ class PyKdebugParser:
|
|
|
187
249
|
formatted_data += f'{self._format_process(tid):<34}'
|
|
188
250
|
event_rep = str(trace)
|
|
189
251
|
if self.color:
|
|
190
|
-
event_rep = highlight(event_rep
|
|
252
|
+
event_rep = highlight(event_rep)
|
|
191
253
|
|
|
192
254
|
return formatted_data + event_rep
|
|
193
255
|
|
|
@@ -378,11 +378,18 @@ def serialize_stat_flags(flags: int) -> List[StatFlags]:
|
|
|
378
378
|
return stat_flags
|
|
379
379
|
|
|
380
380
|
|
|
381
|
+
# bsd/sys/errno.h: codes the kernel uses internally, which a syscall end event can carry.
|
|
382
|
+
KERNEL_ERRNOS = {-1: 'ERESTART', -2: 'EJUSTRETURN'}
|
|
383
|
+
|
|
384
|
+
|
|
381
385
|
def serialize_result(end_event, success_name='', fmt=lambda x: x) -> str:
|
|
382
386
|
error_code = end_event.values[0]
|
|
383
387
|
res = end_event.values[1]
|
|
388
|
+
signed = ctypes.c_int64(error_code).value
|
|
384
389
|
if error_code in errno.errorcode:
|
|
385
390
|
err = f'errno: {errno.errorcode[error_code]}({error_code})'
|
|
391
|
+
elif signed in KERNEL_ERRNOS:
|
|
392
|
+
err = f'errno: {KERNEL_ERRNOS[signed]}({signed})'
|
|
386
393
|
else:
|
|
387
394
|
err = f'errno: {error_code}'
|
|
388
395
|
success = f'{success_name}: {fmt(res)}' if success_name else ''
|
|
@@ -2823,6 +2830,55 @@ class BscGeteuid:
|
|
|
2823
2830
|
return f'geteuid(), uid: {self.uid}'
|
|
2824
2831
|
|
|
2825
2832
|
|
|
2833
|
+
# Darwin's signal numbers, which differ from Linux's.
|
|
2834
|
+
DARWIN_SIGNALS = (None, 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGEMT', 'SIGFPE', 'SIGKILL',
|
|
2835
|
+
'SIGBUS', 'SIGSEGV', 'SIGSYS', 'SIGPIPE', 'SIGALRM', 'SIGTERM', 'SIGURG', 'SIGSTOP', 'SIGTSTP',
|
|
2836
|
+
'SIGCONT', 'SIGCHLD', 'SIGTTIN', 'SIGTTOU', 'SIGIO', 'SIGXCPU', 'SIGXFSZ', 'SIGVTALRM', 'SIGPROF',
|
|
2837
|
+
'SIGWINCH', 'SIGINFO', 'SIGUSR1', 'SIGUSR2')
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
def format_wait_status(status: int) -> str:
|
|
2841
|
+
"""Describe a wait status (`W_EXITCODE(code, signal)`) as its exit code or terminating signal."""
|
|
2842
|
+
signal = status & 0x7f
|
|
2843
|
+
if not signal:
|
|
2844
|
+
return f'exit code: {(status >> 8) & 0xff}'
|
|
2845
|
+
name = DARWIN_SIGNALS[signal] if signal < len(DARWIN_SIGNALS) else str(signal)
|
|
2846
|
+
return f'signal: {name}' + (', core dumped' if status & 0x80 else '')
|
|
2847
|
+
|
|
2848
|
+
|
|
2849
|
+
@dataclass
|
|
2850
|
+
class ProcExit:
|
|
2851
|
+
"""A process exiting (`proc_exit`). An exec also ends the process image it replaces: xnu gives the new image a new
|
|
2852
|
+
proc and exits the old one, with the same pid and status 0."""
|
|
2853
|
+
ktraces: List
|
|
2854
|
+
pid: int
|
|
2855
|
+
status: int
|
|
2856
|
+
|
|
2857
|
+
def __str__(self):
|
|
2858
|
+
return f'proc_exit, pid: {self.pid}, {format_wait_status(self.status)}'
|
|
2859
|
+
|
|
2860
|
+
|
|
2861
|
+
@dataclass
|
|
2862
|
+
class BscExit:
|
|
2863
|
+
ktraces: List
|
|
2864
|
+
rval: int
|
|
2865
|
+
|
|
2866
|
+
def __str__(self):
|
|
2867
|
+
return f'exit({self.rval})'
|
|
2868
|
+
|
|
2869
|
+
|
|
2870
|
+
@dataclass
|
|
2871
|
+
class BscBsdthreadTerminate:
|
|
2872
|
+
ktraces: List
|
|
2873
|
+
stackaddr: int
|
|
2874
|
+
freesize: int
|
|
2875
|
+
port: int
|
|
2876
|
+
sem: int
|
|
2877
|
+
|
|
2878
|
+
def __str__(self):
|
|
2879
|
+
return f'bsdthread_terminate({hex(self.stackaddr)}, {hex(self.freesize)}, {self.port}, {self.sem})'
|
|
2880
|
+
|
|
2881
|
+
|
|
2826
2882
|
@dataclass
|
|
2827
2883
|
class BscWait4:
|
|
2828
2884
|
ktraces: List
|
|
@@ -2970,6 +3026,35 @@ class BscSync:
|
|
|
2970
3026
|
return 'sync()'
|
|
2971
3027
|
|
|
2972
3028
|
|
|
3029
|
+
@dataclass
|
|
3030
|
+
class BscPthreadKill:
|
|
3031
|
+
ktraces: List
|
|
3032
|
+
thread_port: int
|
|
3033
|
+
sig: int
|
|
3034
|
+
result: str
|
|
3035
|
+
|
|
3036
|
+
def __str__(self):
|
|
3037
|
+
rep = f'pthread_kill({self.thread_port}, {self.sig})'
|
|
3038
|
+
if self.result:
|
|
3039
|
+
rep += f', {self.result}'
|
|
3040
|
+
return rep
|
|
3041
|
+
|
|
3042
|
+
|
|
3043
|
+
@dataclass
|
|
3044
|
+
class BscSigreturn:
|
|
3045
|
+
ktraces: List
|
|
3046
|
+
uctx: int
|
|
3047
|
+
infostyle: int
|
|
3048
|
+
token: int
|
|
3049
|
+
result: str
|
|
3050
|
+
|
|
3051
|
+
def __str__(self):
|
|
3052
|
+
rep = f'sigreturn({hex(self.uctx)}, {self.infostyle}, {hex(self.token)})'
|
|
3053
|
+
if self.result:
|
|
3054
|
+
rep += f', {self.result}'
|
|
3055
|
+
return rep
|
|
3056
|
+
|
|
3057
|
+
|
|
2973
3058
|
@dataclass
|
|
2974
3059
|
class BscKill:
|
|
2975
3060
|
ktraces: List
|
|
@@ -5384,6 +5469,20 @@ def handle_geteuid(parser, events):
|
|
|
5384
5469
|
return BscGeteuid(events, events[-1].values[1])
|
|
5385
5470
|
|
|
5386
5471
|
|
|
5472
|
+
def handle_proc_exit(parser, events):
|
|
5473
|
+
args = events[0].values
|
|
5474
|
+
return ProcExit(events, args[0], ctypes.c_int32(args[1]).value)
|
|
5475
|
+
|
|
5476
|
+
|
|
5477
|
+
def handle_exit(parser, events):
|
|
5478
|
+
return BscExit(events, ctypes.c_int32(events[0].values[0]).value)
|
|
5479
|
+
|
|
5480
|
+
|
|
5481
|
+
def handle_bsdthread_terminate(parser, events):
|
|
5482
|
+
args = events[0].values
|
|
5483
|
+
return BscBsdthreadTerminate(events, args[0], args[1], args[2] & 0xffffffff, args[3] & 0xffffffff)
|
|
5484
|
+
|
|
5485
|
+
|
|
5387
5486
|
def handle_wait4(parser, events, no_cancel=False):
|
|
5388
5487
|
args = events[0].values
|
|
5389
5488
|
return BscWait4(events, args[0], args[1], args[2], args[3], serialize_result(events[-1], 'pid'), no_cancel)
|
|
@@ -5438,6 +5537,16 @@ def handle_sync(parser, events):
|
|
|
5438
5537
|
return BscSync(events)
|
|
5439
5538
|
|
|
5440
5539
|
|
|
5540
|
+
def handle_pthread_kill(parser, events):
|
|
5541
|
+
args = events[0].values
|
|
5542
|
+
return BscPthreadKill(events, args[0] & 0xffffffff, args[1], serialize_result(events[-1]))
|
|
5543
|
+
|
|
5544
|
+
|
|
5545
|
+
def handle_sigreturn(parser, events):
|
|
5546
|
+
args = events[0].values
|
|
5547
|
+
return BscSigreturn(events, args[0], args[1], args[2], serialize_result(events[-1]))
|
|
5548
|
+
|
|
5549
|
+
|
|
5441
5550
|
def handle_kill(parser, events):
|
|
5442
5551
|
return BscKill(events, events[0].values[0], events[0].values[1], serialize_result(events[-1]))
|
|
5443
5552
|
|
|
@@ -7133,6 +7242,8 @@ handlers = {
|
|
|
7133
7242
|
'BSC_fchflags': handle_fchflags,
|
|
7134
7243
|
'BSC_sync': handle_sync,
|
|
7135
7244
|
'BSC_kill': handle_kill,
|
|
7245
|
+
'BSC_pthread_kill': handle_pthread_kill,
|
|
7246
|
+
'BSC_sigreturn': handle_sigreturn,
|
|
7136
7247
|
'BSC_getppid': handle_getppid,
|
|
7137
7248
|
'BSC_sys_dup': handle_sys_dup,
|
|
7138
7249
|
'BSC_pipe': handle_pipe,
|
|
@@ -7472,6 +7583,9 @@ handlers = {
|
|
|
7472
7583
|
'BSC_sys_crossarch_trap': handle_sys_crossarch_trap,
|
|
7473
7584
|
'BSC_map_with_linking_np': handle_map_with_linking_np,
|
|
7474
7585
|
'BSC_wait4': handle_wait4,
|
|
7586
|
+
'proc_exit': handle_proc_exit,
|
|
7587
|
+
'BSC_exit': handle_exit,
|
|
7588
|
+
'BSC_bsdthread_terminate': handle_bsdthread_terminate,
|
|
7475
7589
|
'BSC_mknodat': handle_mknodat,
|
|
7476
7590
|
'BSC_mkfifoat': handle_mkfifoat,
|
|
7477
7591
|
'BSC_freadlink': handle_freadlink,
|
|
@@ -18,6 +18,10 @@ def to_rtld_flags(flags: int):
|
|
|
18
18
|
return [r for r in RtldFlag if r.value & flags]
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _image(path: str) -> str:
|
|
22
|
+
return f', image: "{path}"' if path else ''
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
@dataclass
|
|
22
26
|
class DyldUuidMapA:
|
|
23
27
|
ktraces: List
|
|
@@ -34,9 +38,11 @@ class DyldUuidMapB:
|
|
|
34
38
|
ktraces: List
|
|
35
39
|
fid_objno: int
|
|
36
40
|
fid_generation: int
|
|
41
|
+
path: str = ''
|
|
37
42
|
|
|
38
43
|
def __str__(self):
|
|
39
|
-
|
|
44
|
+
rep = f'DYLD_uuid_map_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
|
|
45
|
+
return rep + f', path: "{self.path}"' if self.path else rep
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
@dataclass
|
|
@@ -55,9 +61,11 @@ class DyldUuidUnmapB:
|
|
|
55
61
|
ktraces: List
|
|
56
62
|
fid_objno: int
|
|
57
63
|
fid_generation: int
|
|
64
|
+
path: str = ''
|
|
58
65
|
|
|
59
66
|
def __str__(self):
|
|
60
|
-
|
|
67
|
+
rep = f'DYLD_uuid_unmap_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
|
|
68
|
+
return rep + f', path: "{self.path}"' if self.path else rep
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
@dataclass
|
|
@@ -77,9 +85,11 @@ class DyldUuidSharedCacheB:
|
|
|
77
85
|
ktraces: List
|
|
78
86
|
fid_objno: int
|
|
79
87
|
fid_generation: int
|
|
88
|
+
path: str = ''
|
|
80
89
|
|
|
81
90
|
def __str__(self):
|
|
82
|
-
|
|
91
|
+
rep = f'DYLD_uuid_shared_cache_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
|
|
92
|
+
return rep + f', path: "{self.path}"' if self.path else rep
|
|
83
93
|
|
|
84
94
|
|
|
85
95
|
@dataclass
|
|
@@ -106,9 +116,53 @@ class DyldFuncForAddImage:
|
|
|
106
116
|
ktraces: List
|
|
107
117
|
addr: int
|
|
108
118
|
func: int
|
|
119
|
+
image: str = ''
|
|
120
|
+
|
|
121
|
+
def __str__(self):
|
|
122
|
+
return f'DBG_DYLD_TIMING_FUNC_FOR_ADD_IMAGE, addr: {hex(self.addr)}{_image(self.image)}, func: {hex(self.func)}'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class DyldFuncForRemoveImage:
|
|
127
|
+
ktraces: List
|
|
128
|
+
addr: int
|
|
129
|
+
func: int
|
|
130
|
+
image: str = ''
|
|
131
|
+
|
|
132
|
+
def __str__(self):
|
|
133
|
+
return (f'DBG_DYLD_TIMING_FUNC_FOR_REMOVE_IMAGE, addr: {hex(self.addr)}{_image(self.image)}, '
|
|
134
|
+
f'func: {hex(self.func)}')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class DyldStaticInitializer:
|
|
139
|
+
ktraces: List
|
|
140
|
+
mh: int
|
|
141
|
+
func: int
|
|
142
|
+
image: str = ''
|
|
143
|
+
|
|
144
|
+
def __str__(self):
|
|
145
|
+
return f'DBG_DYLD_TIMING_STATIC_INITIALIZER, mh: {hex(self.mh)}{_image(self.image)}, func: {hex(self.func)}'
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class DyldObjcInit:
|
|
150
|
+
ktraces: List
|
|
151
|
+
mh: int
|
|
152
|
+
image: str = ''
|
|
153
|
+
|
|
154
|
+
def __str__(self):
|
|
155
|
+
return f'DBG_DYLD_TIMING_OBJC_INIT, mh: {hex(self.mh)}{_image(self.image)}'
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class DyldTiming:
|
|
160
|
+
"""A dyld phase traced without arguments (applying fixups, mapping ObjC images, ...)."""
|
|
161
|
+
ktraces: List
|
|
162
|
+
name: str
|
|
109
163
|
|
|
110
164
|
def __str__(self):
|
|
111
|
-
return
|
|
165
|
+
return self.name
|
|
112
166
|
|
|
113
167
|
|
|
114
168
|
@dataclass
|
|
@@ -171,34 +225,55 @@ class Dladdr:
|
|
|
171
225
|
return f'dladdr({hex(self.addr)}), ret: {self.ret}'
|
|
172
226
|
|
|
173
227
|
|
|
174
|
-
|
|
228
|
+
# dyld traces each image (un)load as an `_a` event with its UUID and load address, followed on the same thread by a
|
|
229
|
+
# `_b` event with the id of its path string. The parser keeps the loaded images per process, so the events that only
|
|
230
|
+
# carry a mach header (initializers, ObjC init, ...) can name their image.
|
|
231
|
+
|
|
232
|
+
def _image_path(parser, events, mh):
|
|
233
|
+
return parser.dyld_images.get((parser.threads_pids.get(events[0].tid, -1), mh), '')
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _map_a(parser, events, cls):
|
|
175
237
|
args = events[0].values
|
|
176
|
-
|
|
238
|
+
parser.dyld_pending_load_addr[events[0].tid] = args[2]
|
|
239
|
+
return cls(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _map_b(parser, events, cls, loaded):
|
|
243
|
+
args = events[0].values
|
|
244
|
+
path = parser.global_strings.get(args[1], '') if args[1] else ''
|
|
245
|
+
load_addr = parser.dyld_pending_load_addr.pop(events[0].tid, None)
|
|
246
|
+
if load_addr is not None:
|
|
247
|
+
key = (parser.threads_pids.get(events[0].tid, -1), load_addr)
|
|
248
|
+
if loaded and path:
|
|
249
|
+
parser.dyld_images[key] = path
|
|
250
|
+
elif not loaded:
|
|
251
|
+
parser.dyld_images.pop(key, None)
|
|
252
|
+
return cls(events, args[0] & 0xffffffff, args[0] >> 32, path)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def handle_uuid_map_a(parser, events):
|
|
256
|
+
return _map_a(parser, events, DyldUuidMapA)
|
|
177
257
|
|
|
178
258
|
|
|
179
259
|
def handle_uuid_map_b(parser, events):
|
|
180
|
-
|
|
181
|
-
return DyldUuidMapB(events, arg & 0xffffffff, arg >> 32)
|
|
260
|
+
return _map_b(parser, events, DyldUuidMapB, loaded=True)
|
|
182
261
|
|
|
183
262
|
|
|
184
263
|
def handle_uuid_unmap_a(parser, events):
|
|
185
|
-
|
|
186
|
-
return DyldUuidUnmapA(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
|
|
264
|
+
return _map_a(parser, events, DyldUuidUnmapA)
|
|
187
265
|
|
|
188
266
|
|
|
189
267
|
def handle_uuid_unmap_b(parser, events):
|
|
190
|
-
|
|
191
|
-
return DyldUuidUnmapB(events, arg & 0xffffffff, arg >> 32)
|
|
268
|
+
return _map_b(parser, events, DyldUuidUnmapB, loaded=False)
|
|
192
269
|
|
|
193
270
|
|
|
194
271
|
def handle_uuid_shared_cache_a(parser, events):
|
|
195
|
-
|
|
196
|
-
return DyldUuidSharedCacheA(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
|
|
272
|
+
return _map_a(parser, events, DyldUuidSharedCacheA)
|
|
197
273
|
|
|
198
274
|
|
|
199
275
|
def handle_uuid_shared_cache_b(parser, events):
|
|
200
|
-
|
|
201
|
-
return DyldUuidSharedCacheB(events, arg & 0xffffffff, arg >> 32)
|
|
276
|
+
return _map_b(parser, events, DyldUuidSharedCacheB, loaded=True)
|
|
202
277
|
|
|
203
278
|
|
|
204
279
|
def handle_timing_launch_executable(parser, events):
|
|
@@ -217,7 +292,26 @@ def handle_timing_map_image(parser, events):
|
|
|
217
292
|
|
|
218
293
|
def handle_timing_func_for_add_image(parser, events):
|
|
219
294
|
args = events[0].values
|
|
220
|
-
return DyldFuncForAddImage(events, args[1], args[2])
|
|
295
|
+
return DyldFuncForAddImage(events, args[1], args[2], _image_path(parser, events, args[1]))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def handle_timing_func_for_remove_image(parser, events):
|
|
299
|
+
args = events[0].values
|
|
300
|
+
return DyldFuncForRemoveImage(events, args[1], args[2], _image_path(parser, events, args[1]))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def handle_timing_static_initializer(parser, events):
|
|
304
|
+
args = events[0].values
|
|
305
|
+
return DyldStaticInitializer(events, args[1], args[2], _image_path(parser, events, args[1]))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def handle_timing_objc_init(parser, events):
|
|
309
|
+
args = events[0].values
|
|
310
|
+
return DyldObjcInit(events, args[1], _image_path(parser, events, args[1]))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def handle_timing(parser, events):
|
|
314
|
+
return DyldTiming(events, parser.trace_codes[events[0].eventid])
|
|
221
315
|
|
|
222
316
|
|
|
223
317
|
def handle_timing_bootstrap_start(parser, events):
|
|
@@ -257,6 +351,14 @@ handlers = {
|
|
|
257
351
|
'DBG_DYLD_TIMING_LAUNCH_EXECUTABLE': handle_timing_launch_executable,
|
|
258
352
|
'DBG_DYLD_TIMING_MAP_IMAGE': handle_timing_map_image,
|
|
259
353
|
'DBG_DYLD_TIMING_FUNC_FOR_ADD_IMAGE': handle_timing_func_for_add_image,
|
|
354
|
+
'DBG_DYLD_TIMING_FUNC_FOR_REMOVE_IMAGE': handle_timing_func_for_remove_image,
|
|
355
|
+
'DBG_DYLD_TIMING_STATIC_INITIALIZER': handle_timing_static_initializer,
|
|
356
|
+
'DBG_DYLD_TIMING_OBJC_INIT': handle_timing_objc_init,
|
|
357
|
+
'DBG_DYLD_TIMING_OBJC_MAP': handle_timing,
|
|
358
|
+
'DBG_DYLD_TIMING_APPLY_FIXUPS': handle_timing,
|
|
359
|
+
'DBG_DYLD_TIMING_ATTACH_CODESIGNATURE': handle_timing,
|
|
360
|
+
'DBG_DYLD_TIMING_BUILD_CLOSURE': handle_timing,
|
|
361
|
+
'DBG_DYLD_REMOTE_IMAGE_NOTIFIER': handle_timing,
|
|
260
362
|
'DBG_DYLD_TIMING_BOOTSTRAP_START': handle_timing_bootstrap_start,
|
|
261
363
|
'DBG_DYLD_TIMING_DLOPEN': handle_timing_dlopen,
|
|
262
364
|
'DBG_DYLD_TIMING_DLOPEN_PREFLIGHT': handle_timing_dlopen_preflight,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from pykdebugparser.trace_codes import default_trace_codes
|
|
5
|
+
|
|
6
|
+
DBG_DKRW = 0x302
|
|
7
|
+
# bsd/sys/kdebug.h: the code of a disk I/O event is these flags (`P_RdMetaAsync`, `P_WrDataDone`, ...).
|
|
8
|
+
DKIO_DONE = 0x01
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class VfsLookup:
|
|
13
|
+
ktraces: List
|
|
14
|
+
path: str
|
|
15
|
+
vnode_id: int
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return f'lookup("{self.path}"), vnode id: {self.vnode_id}'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class DiskIo:
|
|
23
|
+
"""A disk I/O, from its issue (device, block, size) to its completion (bytes not transferred, error)."""
|
|
24
|
+
ktraces: List
|
|
25
|
+
name: str
|
|
26
|
+
dev: Optional[int]
|
|
27
|
+
blkno: Optional[int]
|
|
28
|
+
size: Optional[int]
|
|
29
|
+
resid: int
|
|
30
|
+
error: int
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
rep = self.name
|
|
34
|
+
if self.dev is not None:
|
|
35
|
+
rep += f', dev: {self.dev >> 24},{self.dev & 0xffffff}, blkno: {hex(self.blkno)}, size: {self.size}'
|
|
36
|
+
return rep + f', resid: {self.resid}, error: {self.error}'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def handle_vfs_lookup(parser, events):
|
|
40
|
+
node = parser.parse_vnode(events)
|
|
41
|
+
return VfsLookup(events, node.path, node.vnode_id)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def handle_disk_io(parser, events):
|
|
45
|
+
# xnu traces the issue as (buf, dev, blkno, size) and the completion, often on another thread, as
|
|
46
|
+
# (buf, vnode, resid, error); the buf address pairs them. Print one line when the I/O completes.
|
|
47
|
+
event = events[0]
|
|
48
|
+
buf = event.values[0]
|
|
49
|
+
if not (event.eventid >> 2) & DKIO_DONE:
|
|
50
|
+
parser.pending_disk_io[buf] = event
|
|
51
|
+
return None
|
|
52
|
+
start = parser.pending_disk_io.pop(buf, None)
|
|
53
|
+
resid, error = event.values[2], event.values[3]
|
|
54
|
+
if start is None:
|
|
55
|
+
return DiskIo(events, parser.trace_codes[event.eventid].removesuffix('Done'), None, None, None, resid, error)
|
|
56
|
+
args = start.values
|
|
57
|
+
return DiskIo([start, event], parser.trace_codes[start.eventid], args[1] & 0xffffffff, args[2], args[3], resid,
|
|
58
|
+
error)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
handlers = {
|
|
62
|
+
'VFS_LOOKUP': handle_vfs_lookup,
|
|
63
|
+
}
|
|
64
|
+
handlers.update({name: handle_disk_io for eventid, name in default_trace_codes().items() if eventid >> 16 == DBG_DKRW})
|
|
@@ -2,10 +2,14 @@ import ctypes
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from functools import partial
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Optional
|
|
6
6
|
|
|
7
|
+
from pykdebugparser.trace_codes import default_trace_codes
|
|
7
8
|
from pykdebugparser.trace_handlers import parse_enum
|
|
8
9
|
|
|
10
|
+
# bsd/sys/kdebug.h: (DBG_MACH << 8) | DBG_MACH_EXCP_SYNC_ARM
|
|
11
|
+
DBG_MACH_EXCP_SYNC_ARM = 0x103
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
class AsynchronousSystemTrapsReason(Enum):
|
|
11
15
|
AST_NONE = 0x00
|
|
@@ -59,10 +63,13 @@ class ExceptionSyndromeRegisterClass(Enum):
|
|
|
59
63
|
ESR_EC_TRAP_SIMD_FP = 0x07
|
|
60
64
|
ESR_EC_PTRAUTH_INSTR_TRAP = 0x09
|
|
61
65
|
ESR_EC_MCRR_MRRC_CP14_TRAP = 0x0c
|
|
66
|
+
ESR_EC_BTI_FAIL = 0x0d
|
|
62
67
|
ESR_EC_ILLEGAL_INSTR_SET = 0x0e
|
|
63
68
|
ESR_EC_SVC_32 = 0x11
|
|
64
69
|
ESR_EC_SVC_64 = 0x15
|
|
70
|
+
ESR_EC_HVC_64 = 0x16
|
|
65
71
|
ESR_EC_MSR_TRAP = 0x18
|
|
72
|
+
ESR_EC_PAC_FAIL = 0x1c
|
|
66
73
|
ESR_EC_IABORT_EL0 = 0x20
|
|
67
74
|
ESR_EC_IABORT_EL1 = 0x21
|
|
68
75
|
ESR_EC_PC_ALIGN = 0x22
|
|
@@ -281,6 +288,25 @@ class KernelUncategorizedExcArm:
|
|
|
281
288
|
return f'KernelUncategorizedExcArm, class: {esr_class}, far: {hex(self.far)}, pc: {hex(self.pc)}'
|
|
282
289
|
|
|
283
290
|
|
|
291
|
+
@dataclass
|
|
292
|
+
class ArmException:
|
|
293
|
+
"""An arm64 synchronous exception (`sleh.c`): its ESR, fault address and PC, for the classes without their own
|
|
294
|
+
trace class (breakpoints, `brk` traps, pointer authentication failures, alignment faults, ...)."""
|
|
295
|
+
ktraces: List
|
|
296
|
+
name: str
|
|
297
|
+
esr: int
|
|
298
|
+
far: int
|
|
299
|
+
pc: int
|
|
300
|
+
|
|
301
|
+
def __str__(self):
|
|
302
|
+
esr_class = self.esr >> 26
|
|
303
|
+
try:
|
|
304
|
+
esr_class = ExceptionSyndromeRegisterClass(esr_class).name
|
|
305
|
+
except ValueError:
|
|
306
|
+
pass
|
|
307
|
+
return f'{self.name}, class: {esr_class}, far: {hex(self.far)}, pc: {hex(self.pc)}'
|
|
308
|
+
|
|
309
|
+
|
|
284
310
|
@dataclass
|
|
285
311
|
class KernelDataAbortSameElExcArm:
|
|
286
312
|
ktraces: List
|
|
@@ -594,6 +620,58 @@ class SemaphoreTimedwait:
|
|
|
594
620
|
return f'semaphore_timedwait({hex(self.wait_name)}, {self.sec}, {self.nsec}), result: {self.result.name}'
|
|
595
621
|
|
|
596
622
|
|
|
623
|
+
@dataclass
|
|
624
|
+
class TaskForPid:
|
|
625
|
+
ktraces: List
|
|
626
|
+
target_tport: int
|
|
627
|
+
pid: int
|
|
628
|
+
t: int
|
|
629
|
+
result: KernReturn
|
|
630
|
+
|
|
631
|
+
def __str__(self):
|
|
632
|
+
return f'task_for_pid({self.target_tport}, {self.pid}, {hex(self.t)}), result: {self.result.name}'
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@dataclass
|
|
636
|
+
class TaskNameForPid:
|
|
637
|
+
ktraces: List
|
|
638
|
+
target_tport: int
|
|
639
|
+
pid: int
|
|
640
|
+
tn: int
|
|
641
|
+
result: KernReturn
|
|
642
|
+
|
|
643
|
+
def __str__(self):
|
|
644
|
+
return f'task_name_for_pid({self.target_tport}, {self.pid}, {hex(self.tn)}), result: {self.result.name}'
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@dataclass
|
|
648
|
+
class PidForTask:
|
|
649
|
+
ktraces: List
|
|
650
|
+
task: int
|
|
651
|
+
pid_addr: int
|
|
652
|
+
result: KernReturn
|
|
653
|
+
|
|
654
|
+
def __str__(self):
|
|
655
|
+
return f'pid_for_task({self.task}, {hex(self.pid_addr)}), result: {self.result.name}'
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@dataclass
|
|
659
|
+
class TaskSuspension:
|
|
660
|
+
"""A task or thread suspended (by `task_suspend`, a debugger, ...) or resumed (osfmk `DBG_MACH_SUSPENSION`)."""
|
|
661
|
+
ktraces: List
|
|
662
|
+
name: str
|
|
663
|
+
target: str
|
|
664
|
+
target_id: int
|
|
665
|
+
user_stop_count: Optional[int] = None
|
|
666
|
+
pidsuspended: Optional[int] = None
|
|
667
|
+
|
|
668
|
+
def __str__(self):
|
|
669
|
+
rep = f'{self.name}, {self.target}: {self.target_id}'
|
|
670
|
+
if self.user_stop_count is not None:
|
|
671
|
+
rep += f', user_stop_count: {self.user_stop_count}, pidsuspended: {bool(self.pidsuspended)}'
|
|
672
|
+
return rep
|
|
673
|
+
|
|
674
|
+
|
|
597
675
|
@dataclass
|
|
598
676
|
class MachPortGetAttributes:
|
|
599
677
|
ktraces: List
|
|
@@ -1017,6 +1095,11 @@ def handle_user_instr_abort_lower_el_exc_arm(parser, events):
|
|
|
1017
1095
|
return UserInstrAbortLowerElExcArm(events, *events[0].values[:3])
|
|
1018
1096
|
|
|
1019
1097
|
|
|
1098
|
+
def handle_arm_exception(parser, events):
|
|
1099
|
+
args = events[0].values
|
|
1100
|
+
return ArmException(events, parser.trace_codes[events[0].eventid], args[0], args[1], args[2])
|
|
1101
|
+
|
|
1102
|
+
|
|
1020
1103
|
def handle_user_data_abort_lower_el_exc_arm(parser, events):
|
|
1021
1104
|
args = events[0].values
|
|
1022
1105
|
return UserDataAbortLowerElExcArm(events, args[0], args[1], args[2])
|
|
@@ -1057,6 +1140,41 @@ def handle_msc_mach_vm_map_trap(parser, events):
|
|
|
1057
1140
|
return MachVmMap(events, *events[0].values)
|
|
1058
1141
|
|
|
1059
1142
|
|
|
1143
|
+
def handle_msc_task_for_pid(parser, events):
|
|
1144
|
+
args = events[0].values
|
|
1145
|
+
return TaskForPid(events, args[0] & 0xffffffff, ctypes.c_int32(args[1]).value, args[2],
|
|
1146
|
+
parse_enum(KernReturn, events[-1].values[0]))
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def handle_msc_task_name_for_pid(parser, events):
|
|
1150
|
+
args = events[0].values
|
|
1151
|
+
return TaskNameForPid(events, args[0] & 0xffffffff, ctypes.c_int32(args[1]).value, args[2],
|
|
1152
|
+
parse_enum(KernReturn, events[-1].values[0]))
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def handle_msc_pid_for_task(parser, events):
|
|
1156
|
+
args = events[0].values
|
|
1157
|
+
return PidForTask(events, args[0] & 0xffffffff, args[1], parse_enum(KernReturn, events[-1].values[0]))
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def handle_task_suspend(parser, events):
|
|
1161
|
+
args = events[0].values
|
|
1162
|
+
return TaskSuspension(events, 'task_suspend', 'pid', args[0], args[1], args[2])
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def handle_task_resume(parser, events):
|
|
1166
|
+
return TaskSuspension(events, 'task_resume', 'pid', events[0].values[0])
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def handle_thread_suspend(parser, events):
|
|
1170
|
+
args = events[0].values
|
|
1171
|
+
return TaskSuspension(events, 'thread_suspend', 'tid', args[0], args[1], args[2])
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def handle_thread_resume(parser, events):
|
|
1175
|
+
return TaskSuspension(events, 'thread_resume', 'tid', events[0].values[0])
|
|
1176
|
+
|
|
1177
|
+
|
|
1060
1178
|
def handle_msc_mach_port_allocate_trap(parser, events):
|
|
1061
1179
|
args = events[0].values
|
|
1062
1180
|
return MachPortAllocate(events, args[0], parse_enum(MachPortRight, args[1]), args[2])
|
|
@@ -1306,6 +1424,13 @@ handlers = {
|
|
|
1306
1424
|
'MSC_semaphore_signal_trap': handle_msc_semaphore_signal_trap,
|
|
1307
1425
|
'MSC_semaphore_wait_trap': handle_msc_semaphore_wait_trap,
|
|
1308
1426
|
'MSC_semaphore_timedwait_trap': handle_msc_semaphore_timedwait_trap,
|
|
1427
|
+
'MSC_task_for_pid': handle_msc_task_for_pid,
|
|
1428
|
+
'MSC_task_name_for_pid': handle_msc_task_name_for_pid,
|
|
1429
|
+
'MSC_pid_for_task': handle_msc_pid_for_task,
|
|
1430
|
+
'MACH_task_suspend': handle_task_suspend,
|
|
1431
|
+
'MACH_task_resume': handle_task_resume,
|
|
1432
|
+
'MACH_thread_suspend': handle_thread_suspend,
|
|
1433
|
+
'MACH_thread_resume': handle_thread_resume,
|
|
1309
1434
|
'MSC_mach_port_get_attributes_trap': handle_msc_mach_port_get_attributes_trap,
|
|
1310
1435
|
'MSC_mach_port_guard_trap': handle_msc_mach_port_guard_trap,
|
|
1311
1436
|
'MSC_mach_port_unguard_trap': handle_msc_mach_port_unguard_trap,
|
|
@@ -1341,3 +1466,6 @@ handlers = {
|
|
|
1341
1466
|
'SCHED_CLUTCH_CPU_THREAD_SELECT': handle_sched_clutch_cpu_thread_select,
|
|
1342
1467
|
'SCHED_CLUTCH_TG_BUCKET_PRI': handle_sched_clutch_tg_bucket_pri,
|
|
1343
1468
|
}
|
|
1469
|
+
# Every other arm64 synchronous exception class shares the same arguments.
|
|
1470
|
+
handlers.update({name: handle_arm_exception for eventid, name in default_trace_codes().items()
|
|
1471
|
+
if eventid >> 16 == DBG_MACH_EXCP_SYNC_ARM and name.endswith('_Exc_ARM') and name not in handlers})
|
|
@@ -189,6 +189,23 @@ def handle_trace_string_threadname_prev(parser, events):
|
|
|
189
189
|
return event
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
# bsd/sys/kdebug.h
|
|
193
|
+
TRACE_LOST_EVENTS = 0x7020008
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class TraceLostEvents:
|
|
198
|
+
"""The kernel's trace buffer filled up and dropped events before this point."""
|
|
199
|
+
ktraces: List
|
|
200
|
+
|
|
201
|
+
def __str__(self):
|
|
202
|
+
return 'TRACE_LOST_EVENTS: the kernel dropped trace events here'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def handle_trace_lost_events(parser, events):
|
|
206
|
+
return TraceLostEvents(events)
|
|
207
|
+
|
|
208
|
+
|
|
192
209
|
handlers = {
|
|
193
210
|
'TRACE_DATA_NEWTHREAD': handle_trace_data_newthread,
|
|
194
211
|
'TRACE_DATA_EXEC': handle_trace_data_exec,
|
|
@@ -200,4 +217,5 @@ handlers = {
|
|
|
200
217
|
'TRACE_STRING_PROC_EXIT': handle_trace_string_proc_exit,
|
|
201
218
|
'TRACE_STRING_THREADNAME': handle_trace_string_threadname,
|
|
202
219
|
'TRACE_STRING_THREADNAME_PREV': handle_trace_string_threadname_prev,
|
|
220
|
+
'TRACE_LOST_EVENTS': handle_trace_lost_events,
|
|
203
221
|
}
|
|
@@ -29,6 +29,9 @@ class RawTrace:
|
|
|
29
29
|
return rep
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
NO_RETURN_CALLS = ('BSC_exit', 'BSC_bsdthread_terminate')
|
|
33
|
+
|
|
34
|
+
|
|
32
35
|
class TracesParser:
|
|
33
36
|
def __init__(self, trace_codes_map, threads_pids, pids_names):
|
|
34
37
|
self.trace_codes = trace_codes_map
|
|
@@ -46,6 +49,13 @@ class TracesParser:
|
|
|
46
49
|
}
|
|
47
50
|
self.last_data_newthread = None
|
|
48
51
|
self.last_data_exec = None
|
|
52
|
+
# Calls that never return, parsed from their start event.
|
|
53
|
+
self.no_return_eventids = {eventid for eventid, name in trace_codes_map.items() if name in NO_RETURN_CALLS}
|
|
54
|
+
# Disk I/Os issued and not completed yet, by their buf address.
|
|
55
|
+
self.pending_disk_io = {}
|
|
56
|
+
# Images dyld loaded, by (pid, load address), and the load address of each thread's last `DYLD_uuid_*_a` event.
|
|
57
|
+
self.dyld_images = {}
|
|
58
|
+
self.dyld_pending_load_addr = {}
|
|
49
59
|
# Event ids to show as `RawTrace` when they have no handler.
|
|
50
60
|
self.raw_eventids = set()
|
|
51
61
|
self.handlers = {}
|
|
@@ -109,6 +119,9 @@ class TracesParser:
|
|
|
109
119
|
return list(self.vnode_generator([e for e in events if self.trace_codes.get(e.eventid) == 'VFS_LOOKUP']))
|
|
110
120
|
|
|
111
121
|
def _feed_start_event(self, event, state):
|
|
122
|
+
if event.eventid in self.no_return_eventids:
|
|
123
|
+
# The thread or process ends inside the call, so its end is never traced.
|
|
124
|
+
return self._feed_single_event(event, state)
|
|
112
125
|
tid_state = state.get(event.tid)
|
|
113
126
|
if tid_state is None:
|
|
114
127
|
# New tid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykdebugparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Python parser for kdebug events
|
|
5
5
|
Author: Matan Perelman
|
|
6
6
|
Author-email: matan1008@gmail.com
|
|
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
|
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
41
|
Requires-Dist: bytestruct>=0.1.0
|
|
42
|
-
Requires-Dist: pygments
|
|
43
42
|
Requires-Dist: click
|
|
44
43
|
Requires-Dist: termcolor
|
|
45
44
|
Provides-Extra: testing
|
|
@@ -5,6 +5,7 @@ setup.cfg
|
|
|
5
5
|
pykdebugparser/__init__.py
|
|
6
6
|
pykdebugparser/__main__.py
|
|
7
7
|
pykdebugparser/callstacks_parser.py
|
|
8
|
+
pykdebugparser/highlight.py
|
|
8
9
|
pykdebugparser/kd_buf_parser.py
|
|
9
10
|
pykdebugparser/kevent.py
|
|
10
11
|
pykdebugparser/os_log_event.py
|
|
@@ -26,6 +27,7 @@ pykdebugparser/trace_handlers/mach.py
|
|
|
26
27
|
pykdebugparser/trace_handlers/perf.py
|
|
27
28
|
pykdebugparser/trace_handlers/trace.py
|
|
28
29
|
pykdebugparser/trace_handlers/turnstile.py
|
|
30
|
+
tests/test_highlight.py
|
|
29
31
|
tests/test_kd_buf_parser.py
|
|
30
32
|
tests/test_kevent.py
|
|
31
33
|
tests/test_os_log_event.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pykdebugparser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.0"
|
|
8
8
|
description = "Python parser for kdebug events"
|
|
9
9
|
readme.file = "README.md"
|
|
10
10
|
readme.content-type = "text/markdown"
|
|
@@ -25,7 +25,6 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"bytestruct>=0.1.0",
|
|
28
|
-
"pygments",
|
|
29
28
|
"click",
|
|
30
29
|
"termcolor",
|
|
31
30
|
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pykdebugparser.highlight import highlight
|
|
6
|
+
|
|
7
|
+
_SGR = re.compile(r'\x1b\[[0-9;]*m')
|
|
8
|
+
|
|
9
|
+
BOLD, DIM, RESET = '\x1b[1m', '\x1b[2m', '\x1b[0m'
|
|
10
|
+
RED, GREEN, YELLOW, CYAN = '\x1b[31m', '\x1b[32m', '\x1b[33m', '\x1b[36m'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _colored(color, text):
|
|
14
|
+
return color + text + RESET
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize('line, expected', [
|
|
18
|
+
('open("/etc/hosts", O_RDONLY | O_CLOEXEC), fd: 3',
|
|
19
|
+
f'{_colored(BOLD, "open")}({_colored(GREEN, chr(34) + "/etc/hosts" + chr(34))}, {_colored(YELLOW, "O_RDONLY")} | '
|
|
20
|
+
f'{_colored(YELLOW, "O_CLOEXEC")}), {_colored(DIM, "fd:")} {_colored(CYAN, "3")}'),
|
|
21
|
+
('stat64("/x"), errno: ENOENT(2)',
|
|
22
|
+
f'{_colored(BOLD, "stat64")}({_colored(GREEN, chr(34) + "/x" + chr(34))}), {_colored(RED, "errno:")} '
|
|
23
|
+
f'{_colored(RED, "ENOENT")}({_colored(RED, "2")})'),
|
|
24
|
+
('task_for_pid(515, 97997, 0x16bd572fc), result: KERN_SUCCESS',
|
|
25
|
+
f'{_colored(BOLD, "task_for_pid")}({_colored(CYAN, "515")}, {_colored(CYAN, "97997")}, '
|
|
26
|
+
f'{_colored(CYAN, "0x16bd572fc")}), {_colored(DIM, "result:")} {_colored(YELLOW, "KERN_SUCCESS")}'),
|
|
27
|
+
('mach_port_mod_refs(1, 0x0, MACH_PORT_RIGHT_SEND, 1), result: KERN_INVALID_RIGHT',
|
|
28
|
+
f'{_colored(BOLD, "mach_port_mod_refs")}({_colored(CYAN, "1")}, {_colored(CYAN, "0x0")}, '
|
|
29
|
+
f'{_colored(YELLOW, "MACH_PORT_RIGHT_SEND")}, {_colored(CYAN, "1")}), {_colored(DIM, "result:")} '
|
|
30
|
+
f'{_colored(RED, "KERN_INVALID_RIGHT")}'),
|
|
31
|
+
('lookup("private/var/db"), vnode id: 42',
|
|
32
|
+
f'{_colored(BOLD, "lookup")}({_colored(GREEN, chr(34) + "private/var/db" + chr(34))}), '
|
|
33
|
+
f'{_colored(DIM, "vnode id:")} {_colored(CYAN, "42")}'),
|
|
34
|
+
('sigreturn(0x1, 30, 0x2), errno: EJUSTRETURN(-2)',
|
|
35
|
+
f'{_colored(BOLD, "sigreturn")}({_colored(CYAN, "0x1")}, {_colored(CYAN, "30")}, {_colored(CYAN, "0x2")}), '
|
|
36
|
+
f'{_colored(RED, "errno:")} {_colored(RED, "EJUSTRETURN")}({_colored(RED, "-2")})'),
|
|
37
|
+
('DBG_DYLD_TIMING_OBJC_MAP', _colored(BOLD, 'DBG_DYLD_TIMING_OBJC_MAP')),
|
|
38
|
+
])
|
|
39
|
+
def test_highlight(line, expected):
|
|
40
|
+
assert highlight(line) == expected
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize('line', [
|
|
44
|
+
'open("/etc/hosts", O_RDONLY), fd: 3',
|
|
45
|
+
"ioctl(8, 0xc020699d /* _IOC(IOC_IN | IOC_OUT, 'i', 157, 32) */, 0x16d3cbea0)",
|
|
46
|
+
'TRACE_LOST_EVENTS: the kernel dropped trace events here',
|
|
47
|
+
'stat64("/a b/{c}: d", 0x1), count: 0x10',
|
|
48
|
+
'unterminated "string',
|
|
49
|
+
'',
|
|
50
|
+
])
|
|
51
|
+
def test_highlight_keeps_the_text(line):
|
|
52
|
+
assert _SGR.sub('', highlight(line)) == line
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime, timedelta, timezone
|
|
2
|
+
import json
|
|
2
3
|
from io import BytesIO
|
|
3
4
|
import struct
|
|
4
5
|
|
|
@@ -164,3 +165,82 @@ def test_traces_filter_eventid_without_handler():
|
|
|
164
165
|
assert len(traces) == 2
|
|
165
166
|
assert traces[0].endswith('MACH_SCHED_THREAD_SELECT(0x0, 0x0, 0x0, 0x0), end: (0x1d3, 0x0, 0x0, 0x4)')
|
|
166
167
|
assert traces[1].endswith('MACH_PSET_AVG_EXEC_TIME(0x0, 0x3, 0x0, 0x0)')
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_traces_keep_lost_events_when_filtered():
|
|
171
|
+
# The parser adds the TRACE class to filtered traces for its own bookkeeping and hides it again, except for lost
|
|
172
|
+
# events, which tell the reader the trace has a gap.
|
|
173
|
+
events = [
|
|
174
|
+
_kd_buf(1000, 0x7020008, (1, 0, 0, 0)), # TRACE_LOST_EVENTS
|
|
175
|
+
_kd_buf(1001, 0x1400140, (0, 3, 0, 0)), # MACH_PSET_AVG_EXEC_TIME
|
|
176
|
+
]
|
|
177
|
+
events_buf = RAW_VERSION2_BYTES + b'\x00' * 0x11c + b''.join(events)
|
|
178
|
+
parser = _traces_parser()
|
|
179
|
+
parser.filter_class = [1]
|
|
180
|
+
traces = list(parser.formatted_traces(BytesIO(events_buf)))
|
|
181
|
+
assert len(traces) == 1
|
|
182
|
+
assert traces[0].endswith('TRACE_LOST_EVENTS: the kernel dropped trace events here')
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_dict_traces():
|
|
186
|
+
events = [
|
|
187
|
+
_kd_buf(1000, 0x40c0014 | 1, (0x16d2c2b80, 0, 0, 0)), # open start
|
|
188
|
+
_kd_buf(2000, 0x40c0014 | 2, (2, 0, 0, 0)), # open end: ENOENT
|
|
189
|
+
]
|
|
190
|
+
events_buf = RAW_VERSION2_BYTES + b'\x00' * 0x11c + b''.join(events)
|
|
191
|
+
parser = _traces_parser()
|
|
192
|
+
(unknown_process,) = parser.dict_traces(BytesIO(events_buf))
|
|
193
|
+
assert (unknown_process['pid'], unknown_process['process']) == (None, '')
|
|
194
|
+
# The thread's process, once the trace has seen it.
|
|
195
|
+
(parsed,) = parser.traces(BytesIO(events_buf))
|
|
196
|
+
parser.threads_pids[100] = 42
|
|
197
|
+
parser.pids_names[42] = 'probe'
|
|
198
|
+
trace = parser.trace_dict(parsed)
|
|
199
|
+
assert trace == {
|
|
200
|
+
'timestamp': 1000,
|
|
201
|
+
'time': '1970-01-01T00:00:00.000001+00:00',
|
|
202
|
+
'tid': 100,
|
|
203
|
+
'pid': 42,
|
|
204
|
+
'process': 'probe',
|
|
205
|
+
'event': 'BSC_open',
|
|
206
|
+
'fields': trace['fields'],
|
|
207
|
+
'text': trace['text'],
|
|
208
|
+
}
|
|
209
|
+
assert trace['text'].startswith('open(') and trace['text'].endswith('errno: ENOENT(2)')
|
|
210
|
+
assert json.loads(json.dumps(trace)) == trace # JSON-compatible: enums by name, flags as lists of names
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_trace_dict_values():
|
|
214
|
+
from enum import Enum
|
|
215
|
+
from dataclasses import dataclass
|
|
216
|
+
from uuid import UUID
|
|
217
|
+
from pykdebugparser.trace_handlers import UnknownEnumValue
|
|
218
|
+
|
|
219
|
+
class Color(Enum):
|
|
220
|
+
RED = 1
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class Inner:
|
|
224
|
+
ktraces: list
|
|
225
|
+
value: int
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class Trace:
|
|
229
|
+
ktraces: list
|
|
230
|
+
color: Color
|
|
231
|
+
unknown: UnknownEnumValue
|
|
232
|
+
flags: list
|
|
233
|
+
uuid: UUID
|
|
234
|
+
data: bytes
|
|
235
|
+
inner: Inner
|
|
236
|
+
|
|
237
|
+
event = Kevent(timestamp=5, data=b'\x00' * 32, values=(0, 0, 0, 0), tid=7, debugid=0x40c0015, eventid=0x40c0014,
|
|
238
|
+
func_qualifier=1)
|
|
239
|
+
trace = Trace([event], Color.RED, UnknownEnumValue(9), [Color.RED], UUID(int=1), b'\x01\x02', Inner([event], 3))
|
|
240
|
+
parser = PyKdebugParser()
|
|
241
|
+
assert parser.trace_dict(trace)['fields'] == {
|
|
242
|
+
'color': 'RED', 'unknown': 9, 'flags': ['RED'], 'uuid': '00000000-0000-0000-0000-000000000001',
|
|
243
|
+
'data': '0102', 'inner': {'value': 3},
|
|
244
|
+
}
|
|
245
|
+
# Without the device's time information there is no wall-clock time.
|
|
246
|
+
assert parser.trace_dict(trace)['time'] is None
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@dataclass
|
|
6
|
-
class VfsLookup:
|
|
7
|
-
ktraces: List
|
|
8
|
-
path: str
|
|
9
|
-
vnode_id: int
|
|
10
|
-
|
|
11
|
-
def __str__(self):
|
|
12
|
-
return f'lookup("{self.path}"), vnode id: {self.vnode_id}'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def handle_vfs_lookup(parser, events):
|
|
16
|
-
node = parser.parse_vnode(events)
|
|
17
|
-
return VfsLookup(events, node.path, node.vnode_id)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
handlers = {
|
|
21
|
-
'VFS_LOOKUP': handle_vfs_lookup,
|
|
22
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|