pykdebugparser 1.2.8__tar.gz → 1.3.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.2.8 → pykdebugparser-1.3.0}/PKG-INFO +4 -4
- pykdebugparser-1.3.0/pykdebugparser/kd_buf_parser.py +314 -0
- pykdebugparser-1.3.0/pykdebugparser/kevent.py +46 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/os_log_event.py +24 -21
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/pykdebugparser.py +44 -12
- pykdebugparser-1.3.0/pykdebugparser/trace_codes.py +52 -0
- pykdebugparser-1.3.0/pykdebugparser/trace_handlers/__init__.py +23 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/bsd.py +19 -16
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/mach.py +26 -14
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/perf.py +13 -3
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/turnstile.py +4 -2
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/traces_parser.py +25 -20
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/PKG-INFO +4 -4
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/SOURCES.txt +1 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/requires.txt +1 -1
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pyproject.toml +4 -4
- pykdebugparser-1.3.0/tests/test_kd_buf_parser.py +121 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/tests/test_os_log_event.py +35 -1
- pykdebugparser-1.3.0/tests/test_pykdebugparser.py +148 -0
- pykdebugparser-1.3.0/tests/test_trace_codes.py +26 -0
- pykdebugparser-1.2.8/pykdebugparser/kd_buf_parser.py +0 -211
- pykdebugparser-1.2.8/pykdebugparser/kevent.py +0 -31
- pykdebugparser-1.2.8/pykdebugparser/trace_codes.py +0 -30
- pykdebugparser-1.2.8/pykdebugparser/trace_handlers/__init__.py +0 -0
- pykdebugparser-1.2.8/tests/test_pykdebugparser.py +0 -39
- pykdebugparser-1.2.8/tests/test_trace_codes.py +0 -13
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/LICENSE +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/README.md +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/__init__.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/__main__.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/callstacks_parser.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace.codes +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/dyld.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/fsystem.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/trace.py +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/dependency_links.txt +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/entry_points.txt +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/top_level.txt +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/setup.cfg +0 -0
- {pykdebugparser-1.2.8 → pykdebugparser-1.3.0}/tests/test_kevent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykdebugparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Python parser for kdebug events
|
|
5
5
|
Author: Matan Perelman
|
|
6
6
|
Author-email: matan1008@gmail.com
|
|
@@ -28,17 +28,17 @@ License: MIT License
|
|
|
28
28
|
|
|
29
29
|
Project-URL: Homepage, https://github.com/matan1008/pykdebugparser
|
|
30
30
|
Project-URL: Source, https://github.com/matan1008/pykdebugparser
|
|
31
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
32
31
|
Classifier: Programming Language :: Python :: 3.9
|
|
33
32
|
Classifier: Programming Language :: Python :: 3.10
|
|
34
33
|
Classifier: Programming Language :: Python :: 3.11
|
|
35
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
36
35
|
Classifier: Programming Language :: Python :: 3.13
|
|
37
36
|
Classifier: Programming Language :: Python :: 3.14
|
|
38
|
-
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
38
|
+
Requires-Python: >=3.9
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist:
|
|
41
|
+
Requires-Dist: bytestruct>=0.1.0
|
|
42
42
|
Requires-Dist: pygments
|
|
43
43
|
Requires-Dist: click
|
|
44
44
|
Requires-Dist: termcolor
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import io
|
|
3
|
+
import plistlib
|
|
4
|
+
import struct
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
from bytestruct import GREEDY_BYTES, U64_LE, BinaryParseError, BinaryStruct, ByteReader, Bytes, CodecBase, \
|
|
9
|
+
PaddedString, Prefixed, TruncatedDataError, u32, u64
|
|
10
|
+
|
|
11
|
+
from pykdebugparser.kevent import from_kd_buf, from_kd_bufs, KD_BUF_FORMAT
|
|
12
|
+
from pykdebugparser.os_log_event import OsLogEvent
|
|
13
|
+
|
|
14
|
+
KEVENT_SIZE = struct.calcsize(KD_BUF_FORMAT)
|
|
15
|
+
# Upper bound for a single bulk read of kevents.
|
|
16
|
+
KEVENTS_READ_SIZE = KEVENT_SIZE * 1024
|
|
17
|
+
|
|
18
|
+
ProcessData = namedtuple('ProcessData', ['pid', 'name'])
|
|
19
|
+
|
|
20
|
+
RAW_VERSION_SIZE = 4
|
|
21
|
+
RAW_VERSION2_BYTES = b'\x00\x02\xaa\x55'
|
|
22
|
+
RAW_VERSION3_BYTES = b'\x00\x03\xaa\x55'
|
|
23
|
+
# Header flag of a 64-bit kernel.
|
|
24
|
+
KDBG_LP64 = 0x1
|
|
25
|
+
TRACEV3_STACKSHOT_END = b'stackshot_out_fl'
|
|
26
|
+
TRACEV3_THREADMAP_TAG = b'\x00\x1d\x00\x00\x00\x00\x00\x00'
|
|
27
|
+
TRACEV3_EVENTS_TAG = b'\x00\x1e\x00\x00\x00\x00\x00\x00'
|
|
28
|
+
TRACEV3_MORE_EVENTS = b'\x00\x20\x00\x00\x00\x00\x00\x00'
|
|
29
|
+
TRACEV3_DYLD_MODULES = b'\x01\x80\x00\x00\x00\x00\x00\x00'
|
|
30
|
+
TRACEV3_TRACE_CODES = b'\x0f\x80\x00\x00\x00\x00\x00\x00'
|
|
31
|
+
TRACEV3_PROCESSES = b'\x10\x80\x00\x00\x00\x00\x00\x00'
|
|
32
|
+
TRACEV3_LOG_EVENTS = b'\x11\x80\x00\x00\x00\x00\x00\x00'
|
|
33
|
+
TRACEV3_LOG_STRINGS = b'\x12\x80\x00\x00\x00\x00\x00\x00'
|
|
34
|
+
TRACEV3_KERNEL_EXTENSIONS = b'\x05\x80\x00\x00\x00\x00\x00\x00'
|
|
35
|
+
TRACEV3_IMAGES = b'\x04\x80\x00\x00\x01\x00\x00\x00'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Bplist(CodecBase[Any]):
|
|
39
|
+
"""
|
|
40
|
+
Codec for a plist (all remaining bytes).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def read(self, reader: ByteReader) -> Any:
|
|
44
|
+
return plistlib.loads(GREEDY_BYTES.read(reader))
|
|
45
|
+
|
|
46
|
+
def build(self, value: Any) -> bytes:
|
|
47
|
+
return plistlib.dumps(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class KdThreadmap(BinaryStruct, byte_order='<'):
|
|
52
|
+
tid: u64
|
|
53
|
+
pid: u32
|
|
54
|
+
process: Annotated[str, PaddedString(0x14)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class KdHeaderV2(BinaryStruct, byte_order='<'):
|
|
59
|
+
"""
|
|
60
|
+
Header of a trace version 2 (`RAW_VERSION2`), as written by kperf's `kperf_buffer_readdata`.
|
|
61
|
+
The fields up to `tod_usecs` follow XNU's `RAW_header`; kperf always writes the time of day as zero.
|
|
62
|
+
"""
|
|
63
|
+
number_of_threads: u32
|
|
64
|
+
tod_secs: u64
|
|
65
|
+
tod_usecs: u32
|
|
66
|
+
flags: u32
|
|
67
|
+
tick_frequency: u64
|
|
68
|
+
# Never written by kperf, so it holds whatever the buffer contained before.
|
|
69
|
+
_unused: Annotated[bytes, Bytes(0x100)] = field(default=bytes(0x100), init=False, repr=False, compare=False)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_64bit(self) -> bool:
|
|
73
|
+
return bool(self.flags & KDBG_LP64)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class KdHeaderV3(BinaryStruct, byte_order='<'):
|
|
78
|
+
tag: u32
|
|
79
|
+
sub_tag: u32
|
|
80
|
+
length: u64
|
|
81
|
+
timebase_numer: u32
|
|
82
|
+
timebase_denom: u32
|
|
83
|
+
timestamp: u64
|
|
84
|
+
walltime_secs: u64
|
|
85
|
+
walltime_usecs: u32
|
|
86
|
+
timezone_minuteswest: u32
|
|
87
|
+
timezone_dst: u32
|
|
88
|
+
flags: u32
|
|
89
|
+
tag2: u32
|
|
90
|
+
cpu_info: Annotated[Any, Prefixed(U64_LE, Bplist())]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class KdV3Block(BinaryStruct, byte_order='<'):
|
|
95
|
+
"""
|
|
96
|
+
An additional data block of a trace version 3, aligned to 8 bytes (unless it is the last one).
|
|
97
|
+
"""
|
|
98
|
+
tag: Annotated[bytes, Bytes(8)]
|
|
99
|
+
data: Annotated[bytes, Prefixed(U64_LE, GREEDY_BYTES)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def read_exactly(reader: io.IOBase, size: int) -> bytes:
|
|
103
|
+
"""
|
|
104
|
+
Read exactly `size` bytes from a stream.
|
|
105
|
+
:param reader: Stream to read from.
|
|
106
|
+
:param size: Number of bytes to read.
|
|
107
|
+
:return: The data read.
|
|
108
|
+
:raises TruncatedDataError: The stream ended first.
|
|
109
|
+
"""
|
|
110
|
+
data = reader.read(size)
|
|
111
|
+
if len(data) != size:
|
|
112
|
+
raise TruncatedDataError(f'expected {size} bytes, found {len(data)}')
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def skip_zeros(reader: io.IOBase):
|
|
117
|
+
"""
|
|
118
|
+
Skip zero bytes in a stream, leaving it at the first non-zero byte (or at its end).
|
|
119
|
+
Reads a byte at a time and seeks back once, so a stream never consumes data past the zeros.
|
|
120
|
+
:param reader: Stream to skip in.
|
|
121
|
+
"""
|
|
122
|
+
while True:
|
|
123
|
+
position = reader.tell()
|
|
124
|
+
if reader.read(1) != b'\x00':
|
|
125
|
+
reader.seek(position, 0)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_threadmap(data: bytes) -> list:
|
|
130
|
+
"""
|
|
131
|
+
Parse consecutive threadmap entries, stopping at the first one that can't be parsed.
|
|
132
|
+
:param data: Threadmap entries.
|
|
133
|
+
:return: Parsed entries.
|
|
134
|
+
"""
|
|
135
|
+
reader = ByteReader(data)
|
|
136
|
+
threadmap = []
|
|
137
|
+
while reader.remaining:
|
|
138
|
+
try:
|
|
139
|
+
threadmap.append(KdThreadmap.read(reader))
|
|
140
|
+
except BinaryParseError:
|
|
141
|
+
break
|
|
142
|
+
return threadmap
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_v3_blocks(data: bytes) -> list:
|
|
146
|
+
"""
|
|
147
|
+
Parse the additional data blocks of a trace version 3, stopping at the first one that can't be parsed.
|
|
148
|
+
:param data: Additional data blocks.
|
|
149
|
+
:return: Parsed blocks.
|
|
150
|
+
"""
|
|
151
|
+
reader = ByteReader(data)
|
|
152
|
+
blocks = []
|
|
153
|
+
while True:
|
|
154
|
+
try:
|
|
155
|
+
block = KdV3Block.read(reader)
|
|
156
|
+
except BinaryParseError:
|
|
157
|
+
break
|
|
158
|
+
blocks.append(block)
|
|
159
|
+
# The last block may be unpadded.
|
|
160
|
+
reader.skip(min(-(KdV3Block.SIZE + 8 + len(block.data)) % 8, reader.remaining))
|
|
161
|
+
return blocks
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def seek_until(reader, data: bytes):
|
|
165
|
+
"""
|
|
166
|
+
Read from a stream until matching data.
|
|
167
|
+
Reading is aligned to the data size.
|
|
168
|
+
:param reader: Stream to read from.
|
|
169
|
+
:param data: Data to match.
|
|
170
|
+
"""
|
|
171
|
+
found = reader.read(len(data))
|
|
172
|
+
while found != data:
|
|
173
|
+
found = found[1:] + reader.read(1)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class KdBufParser:
|
|
177
|
+
"""
|
|
178
|
+
Parser for raw kd_buf buffer.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, threads_pids=None, pids_names=None):
|
|
182
|
+
self.threads_pids = {} if threads_pids is None else threads_pids
|
|
183
|
+
self.pids_names = {} if pids_names is None else pids_names
|
|
184
|
+
self.versions = {
|
|
185
|
+
RAW_VERSION2_BYTES: self.parse_v2,
|
|
186
|
+
RAW_VERSION3_BYTES: self.parse_v3,
|
|
187
|
+
}
|
|
188
|
+
self.trace_codes = ''
|
|
189
|
+
self.images = {}
|
|
190
|
+
self.dyld_modules = {}
|
|
191
|
+
self.processes = {}
|
|
192
|
+
self.kernel_extensions = {'Binaries': []}
|
|
193
|
+
self.v3_header = None
|
|
194
|
+
|
|
195
|
+
def parse(self, reader: io.IOBase):
|
|
196
|
+
"""
|
|
197
|
+
Parse kevents from a stream.
|
|
198
|
+
:param reader: Stream to read from.
|
|
199
|
+
:return: Generator for parsed kevents.
|
|
200
|
+
"""
|
|
201
|
+
version = reader.read(RAW_VERSION_SIZE)
|
|
202
|
+
return self.versions[version](reader)
|
|
203
|
+
|
|
204
|
+
def set_thread_map(self, parsed_threadmap):
|
|
205
|
+
self.threads_pids.clear()
|
|
206
|
+
self.pids_names.clear()
|
|
207
|
+
for thread in parsed_threadmap:
|
|
208
|
+
self.threads_pids[thread.tid] = thread.pid
|
|
209
|
+
self.pids_names[thread.pid] = thread.process
|
|
210
|
+
|
|
211
|
+
def parse_v2(self, reader: io.IOBase):
|
|
212
|
+
"""
|
|
213
|
+
Parse trace version 2.
|
|
214
|
+
:param reader: Stream to parse.
|
|
215
|
+
:return: Generator for parsed kevents.
|
|
216
|
+
"""
|
|
217
|
+
header = KdHeaderV2.parse(read_exactly(reader, KdHeaderV2.SIZE))
|
|
218
|
+
self.set_thread_map([KdThreadmap.parse(read_exactly(reader, KdThreadmap.SIZE))
|
|
219
|
+
for _ in range(header.number_of_threads)])
|
|
220
|
+
skip_zeros(reader)
|
|
221
|
+
read1 = getattr(reader, 'read1', None)
|
|
222
|
+
if read1 is None:
|
|
223
|
+
# Without `read1` a bigger `read` might block until more data than currently available arrives.
|
|
224
|
+
while True:
|
|
225
|
+
buf = reader.read(KEVENT_SIZE)
|
|
226
|
+
if not buf:
|
|
227
|
+
break
|
|
228
|
+
yield from_kd_buf(buf)
|
|
229
|
+
return
|
|
230
|
+
# `read1` returns whatever is available, so kevents are decoded in bulk without delaying live streams.
|
|
231
|
+
leftover = b''
|
|
232
|
+
while True:
|
|
233
|
+
buf = read1(KEVENTS_READ_SIZE)
|
|
234
|
+
if not buf:
|
|
235
|
+
break
|
|
236
|
+
if leftover:
|
|
237
|
+
buf = leftover + buf
|
|
238
|
+
end = len(buf) - len(buf) % KEVENT_SIZE
|
|
239
|
+
if end != len(buf):
|
|
240
|
+
leftover = buf[end:]
|
|
241
|
+
buf = buf[:end]
|
|
242
|
+
else:
|
|
243
|
+
leftover = b''
|
|
244
|
+
yield from from_kd_bufs(buf)
|
|
245
|
+
if leftover:
|
|
246
|
+
# A truncated kevent, fail just like a non-bulk read would.
|
|
247
|
+
yield from_kd_buf(leftover)
|
|
248
|
+
|
|
249
|
+
def parse_v3(self, reader: io.IOBase):
|
|
250
|
+
"""
|
|
251
|
+
Parse trace version 3.
|
|
252
|
+
:param reader: Stream to parse.
|
|
253
|
+
:return: Generator for parsed kevents.
|
|
254
|
+
"""
|
|
255
|
+
header = read_exactly(reader, KdHeaderV3.SIZE + U64_LE.size)
|
|
256
|
+
header += read_exactly(reader, U64_LE.unpack_from(header, KdHeaderV3.SIZE)[0])
|
|
257
|
+
read_exactly(reader, -len(header) % 8)
|
|
258
|
+
self.v3_header = KdHeaderV3.parse(header)
|
|
259
|
+
# Align the reader to 8 bytes from the beginning of the stream
|
|
260
|
+
reader.read(8 - RAW_VERSION_SIZE)
|
|
261
|
+
# The threadmap tag appears randomly in the stackshot.
|
|
262
|
+
seek_until(reader, TRACEV3_STACKSHOT_END)
|
|
263
|
+
seek_until(reader, TRACEV3_THREADMAP_TAG)
|
|
264
|
+
threadmap_size = U64_LE.parse(read_exactly(reader, U64_LE.size))
|
|
265
|
+
self.set_thread_map(parse_threadmap(read_exactly(reader, threadmap_size)))
|
|
266
|
+
|
|
267
|
+
while True:
|
|
268
|
+
seek_until(reader, TRACEV3_EVENTS_TAG)
|
|
269
|
+
size = U64_LE.parse(read_exactly(reader, U64_LE.size))
|
|
270
|
+
reader.read(8) # All zeros, unknown.
|
|
271
|
+
for _ in range(size // KEVENT_SIZE):
|
|
272
|
+
buf = reader.read(KEVENT_SIZE)
|
|
273
|
+
yield from_kd_buf(buf)
|
|
274
|
+
if reader.read(len(TRACEV3_MORE_EVENTS)) != TRACEV3_MORE_EVENTS:
|
|
275
|
+
break
|
|
276
|
+
reader.seek(-8, 1)
|
|
277
|
+
|
|
278
|
+
additional_data = parse_v3_blocks(reader.read())
|
|
279
|
+
|
|
280
|
+
self.trace_codes = ''
|
|
281
|
+
self.kernel_extensions = {'Binaries': []}
|
|
282
|
+
self.dyld_modules = {}
|
|
283
|
+
self.images = {}
|
|
284
|
+
self.processes = {}
|
|
285
|
+
|
|
286
|
+
log_events = []
|
|
287
|
+
log_strings = {}
|
|
288
|
+
|
|
289
|
+
for block in additional_data:
|
|
290
|
+
if block.tag == TRACEV3_DYLD_MODULES:
|
|
291
|
+
data = plistlib.loads(block.data)
|
|
292
|
+
if not self.dyld_modules:
|
|
293
|
+
self.dyld_modules.update(data)
|
|
294
|
+
else:
|
|
295
|
+
self.dyld_modules['Binaries'].extend(data['Binaries'])
|
|
296
|
+
elif block.tag == TRACEV3_TRACE_CODES:
|
|
297
|
+
self.trace_codes += block.data.decode()
|
|
298
|
+
elif block.tag == TRACEV3_PROCESSES:
|
|
299
|
+
self.processes = plistlib.loads(block.data)
|
|
300
|
+
elif block.tag == TRACEV3_KERNEL_EXTENSIONS:
|
|
301
|
+
self.kernel_extensions['Binaries'].extend(plistlib.loads(block.data)['Binaries'])
|
|
302
|
+
elif block.tag == TRACEV3_IMAGES:
|
|
303
|
+
self.images = plistlib.loads(block.data)
|
|
304
|
+
elif block.tag == TRACEV3_LOG_EVENTS:
|
|
305
|
+
log_events.extend(plistlib.loads(block.data)['Events'])
|
|
306
|
+
elif block.tag == TRACEV3_LOG_STRINGS:
|
|
307
|
+
log_strings = {v: k for k, v in plistlib.loads(block.data)['StringIndex'].items()}
|
|
308
|
+
|
|
309
|
+
for event in log_events:
|
|
310
|
+
log_event = OsLogEvent.from_raw_log_event(event, log_strings)
|
|
311
|
+
if log_event.process and log_event.thread_identifier:
|
|
312
|
+
self.threads_pids[log_event.thread_identifier] = log_event.process_identifier
|
|
313
|
+
self.pids_names[log_event.process_identifier] = log_event.process
|
|
314
|
+
yield log_event
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
import enum
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
KDBG_EVENTID_MASK = 0xfffffffc
|
|
6
|
+
KDBG_FUNC_MASK = 0x00000003
|
|
7
|
+
KD_BUF_FORMAT = '<Q32sQIIQ'
|
|
8
|
+
# Same layout as `KD_BUF_FORMAT`, with the 32 bytes of arguments unpacked as 4 64-bit values.
|
|
9
|
+
KD_BUF_STRUCT = struct.Struct('<QQQQQQIIQ')
|
|
10
|
+
KD_BUF_SIZE = KD_BUF_STRUCT.size
|
|
11
|
+
Kevent = namedtuple('Kevent', ['timestamp', 'data', 'values', 'tid', 'debugid', 'eventid', 'func_qualifier'])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DgbFuncQual(enum.Enum):
|
|
15
|
+
"""
|
|
16
|
+
Event's role in the trace.
|
|
17
|
+
"""
|
|
18
|
+
DBG_FUNC_NONE = 0
|
|
19
|
+
DBG_FUNC_START = 1
|
|
20
|
+
DBG_FUNC_END = 2
|
|
21
|
+
DBG_FUNC_ALL = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def from_kd_buf(kd_buf: bytes) -> Kevent:
|
|
25
|
+
"""
|
|
26
|
+
Create a Kevent object from a kd_buf kevent's struct.
|
|
27
|
+
:param kd_buf: Buffer of kd_buf kevent's struct.
|
|
28
|
+
:return: Parsed kevent.
|
|
29
|
+
"""
|
|
30
|
+
timestamp, arg0, arg1, arg2, arg3, tid, debugid, cpuid, unused = KD_BUF_STRUCT.unpack(kd_buf)
|
|
31
|
+
return Kevent(timestamp, kd_buf[8:40], (arg0, arg1, arg2, arg3), tid, debugid, debugid & KDBG_EVENTID_MASK,
|
|
32
|
+
debugid & KDBG_FUNC_MASK)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def from_kd_bufs(kd_bufs: bytes):
|
|
36
|
+
"""
|
|
37
|
+
Create Kevent objects from consecutive kd_buf kevent's structs.
|
|
38
|
+
:param kd_bufs: Buffer of kd_buf kevent's structs, its length must be a multiple of the struct size.
|
|
39
|
+
:return: Generator of parsed kevents.
|
|
40
|
+
"""
|
|
41
|
+
new_kevent = tuple.__new__ # Skips the namedtuple's Python-level __new__, the result is identical.
|
|
42
|
+
offset = 8
|
|
43
|
+
for timestamp, arg0, arg1, arg2, arg3, tid, debugid, cpuid, unused in KD_BUF_STRUCT.iter_unpack(kd_bufs):
|
|
44
|
+
yield new_kevent(Kevent, (timestamp, kd_bufs[offset:offset + 32], (arg0, arg1, arg2, arg3), tid, debugid,
|
|
45
|
+
debugid & KDBG_EVENTID_MASK, debugid & KDBG_FUNC_MASK))
|
|
46
|
+
offset += KD_BUF_SIZE
|
|
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|
|
3
3
|
import enum
|
|
4
4
|
from typing import List, Dict
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from bytestruct import U64_LE, BinaryStruct, u8, u32
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class OsLogType(enum.Enum):
|
|
@@ -81,7 +81,7 @@ class FirehoseTracepointSignpostType(enum.IntFlag):
|
|
|
81
81
|
scope_system = 0xc0
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class FirehoseTracepointSingpostFlags(enum.
|
|
84
|
+
class FirehoseTracepointSingpostFlags(enum.IntFlag):
|
|
85
85
|
has_private_data = 1
|
|
86
86
|
has_subsystem = 2
|
|
87
87
|
has_rules = 4
|
|
@@ -100,22 +100,24 @@ tracepoint_types = {
|
|
|
100
100
|
|
|
101
101
|
tracepoint_flags = {
|
|
102
102
|
FirehoseTracepointNamespace.log: FirehoseTracepointLogFlags,
|
|
103
|
-
FirehoseTracepointNamespace.
|
|
103
|
+
FirehoseTracepointNamespace.signpost: FirehoseTracepointSingpostFlags,
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
# `trace_flags` bits.
|
|
107
|
+
TRACE_FLAGS_HAS_LARGE_OFFSET = 0x20
|
|
108
|
+
TRACE_FLAGS_HAS_UNIQUE_PID = 0x10
|
|
109
|
+
TRACE_FLAGS_PC_STYLE_SHIFT = 1
|
|
110
|
+
TRACE_FLAGS_PC_STYLE_MASK = 0x7
|
|
111
|
+
TRACE_FLAGS_HAS_CURRENT_AID = 0x01
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class FirehoseTracepointId(BinaryStruct, byte_order='<'):
|
|
116
|
+
namespace: u8
|
|
117
|
+
type_: u8
|
|
118
|
+
trace_flags: u8
|
|
119
|
+
flags: u8
|
|
120
|
+
code: u32
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
@dataclass
|
|
@@ -262,7 +264,8 @@ class OsLogEvent:
|
|
|
262
264
|
|
|
263
265
|
@classmethod
|
|
264
266
|
def parse_trace_identifier(cls, trace_identifier):
|
|
265
|
-
trace_id =
|
|
267
|
+
trace_id = FirehoseTracepointId.parse(U64_LE.build(trace_identifier))
|
|
268
|
+
trace_flags = trace_id.trace_flags
|
|
266
269
|
trace_namespace = FirehoseTracepointNamespace(trace_id.namespace)
|
|
267
270
|
if trace_namespace in tracepoint_types:
|
|
268
271
|
type_ = tracepoint_types[trace_namespace](trace_id.type_)
|
|
@@ -275,10 +278,10 @@ class OsLogEvent:
|
|
|
275
278
|
return TraceIdentifier(
|
|
276
279
|
namespace=trace_namespace,
|
|
277
280
|
type_=type_,
|
|
278
|
-
has_large_offset=
|
|
279
|
-
has_unique_pid=
|
|
280
|
-
pc_style=FirehoseTracepointFlagsPcStyle(
|
|
281
|
-
has_current_aid=
|
|
281
|
+
has_large_offset=bool(trace_flags & TRACE_FLAGS_HAS_LARGE_OFFSET),
|
|
282
|
+
has_unique_pid=bool(trace_flags & TRACE_FLAGS_HAS_UNIQUE_PID),
|
|
283
|
+
pc_style=FirehoseTracepointFlagsPcStyle((trace_flags >> TRACE_FLAGS_PC_STYLE_SHIFT) & TRACE_FLAGS_PC_STYLE_MASK),
|
|
284
|
+
has_current_aid=bool(trace_flags & TRACE_FLAGS_HAS_CURRENT_AID),
|
|
282
285
|
flags=tracepoint_flags[trace_namespace](
|
|
283
286
|
trace_id.flags) if trace_namespace in tracepoint_flags else None,
|
|
284
287
|
code=trace_id.code,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
import io
|
|
3
|
+
import math
|
|
3
4
|
|
|
4
5
|
from pygments import highlight, lexers, formatters
|
|
5
6
|
from termcolor import colored
|
|
@@ -25,6 +26,8 @@ class PyKdebugParser:
|
|
|
25
26
|
self.filter_process = None
|
|
26
27
|
self.filter_class = []
|
|
27
28
|
self.filter_subclass = []
|
|
29
|
+
# Event ids (debugids without the function qualifier) to include, on top of the class/subclass filters.
|
|
30
|
+
self.filter_eventid = set()
|
|
28
31
|
self.show_timestamp = True
|
|
29
32
|
self.show_name = True
|
|
30
33
|
self.show_func_qual = True
|
|
@@ -41,15 +44,29 @@ class PyKdebugParser:
|
|
|
41
44
|
self.pids_names = {}
|
|
42
45
|
self.dyld_addresses = []
|
|
43
46
|
self.dyld_uuids = []
|
|
47
|
+
self._timestamp_cache = None
|
|
44
48
|
|
|
45
49
|
def kevents(self, kdebug: io.IOBase):
|
|
46
50
|
events_generator = KdBufParser(self.threads_pids, self.pids_names).parse(kdebug)
|
|
47
|
-
events_generator
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
return self._filter_kevents(events_generator, self.filter_tid is not None,
|
|
52
|
+
bool(self.filter_class or self.filter_subclass or self.filter_eventid))
|
|
53
|
+
|
|
54
|
+
def _filter_kevents(self, events_generator, by_tid, by_class):
|
|
55
|
+
# A single generator instead of a `filter` chain, avoiding a lambda call per kevent per filter.
|
|
56
|
+
filter_class = self.filter_class
|
|
57
|
+
filter_subclass = self.filter_subclass
|
|
58
|
+
filter_eventid = set(self.filter_eventid)
|
|
59
|
+
for event in events_generator:
|
|
60
|
+
if isinstance(event, OsLogEvent):
|
|
61
|
+
continue
|
|
62
|
+
if by_tid and event.tid != self.filter_tid:
|
|
63
|
+
continue
|
|
64
|
+
if by_class:
|
|
65
|
+
eventid = event.eventid
|
|
66
|
+
if not ((eventid >> 24 in filter_class) or (eventid >> 16 in filter_subclass)
|
|
67
|
+
or eventid in filter_eventid):
|
|
68
|
+
continue
|
|
69
|
+
yield event
|
|
53
70
|
|
|
54
71
|
def formatted_kevents(self, kdebug: io.IOBase, trace_codes=None):
|
|
55
72
|
trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
|
|
@@ -58,11 +75,12 @@ class PyKdebugParser:
|
|
|
58
75
|
def traces(self, kdebug: io.IOBase, trace_codes=None):
|
|
59
76
|
trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
|
|
60
77
|
|
|
61
|
-
has_filters = self.filter_class or self.filter_subclass
|
|
78
|
+
has_filters = self.filter_class or self.filter_subclass or self.filter_eventid
|
|
62
79
|
add_trace_class = has_filters and DBG_TRACE not in self.filter_class
|
|
63
80
|
if add_trace_class:
|
|
64
81
|
self.filter_class.append(DBG_TRACE)
|
|
65
|
-
has_bsd = DBG_BSD in self.filter_class or any(
|
|
82
|
+
has_bsd = (DBG_BSD in self.filter_class or any(sc >> 8 == DBG_BSD for sc in self.filter_subclass)
|
|
83
|
+
or any(eventid >> 24 == DBG_BSD for eventid in self.filter_eventid))
|
|
66
84
|
add_fs_class = has_filters and has_bsd and DBG_FSYSTEM not in self.filter_class
|
|
67
85
|
if add_fs_class:
|
|
68
86
|
self.filter_class.append(DBG_FSYSTEM)
|
|
@@ -113,9 +131,22 @@ class PyKdebugParser:
|
|
|
113
131
|
offset_usec = (
|
|
114
132
|
((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
|
|
115
133
|
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
posix_time = (self.usecs_since_epoch + offset_usec) / 1000000
|
|
135
|
+
# Split to seconds and microseconds exactly like `datetime.fromtimestamp` does (round half to even), so
|
|
136
|
+
# the date and time part can be formatted only once per second.
|
|
137
|
+
fraction, seconds = math.modf(posix_time)
|
|
138
|
+
microseconds = round(fraction * 1000000)
|
|
139
|
+
if microseconds >= 1000000:
|
|
140
|
+
microseconds -= 1000000
|
|
141
|
+
seconds += 1.0
|
|
142
|
+
elif microseconds < 0:
|
|
143
|
+
microseconds += 1000000
|
|
144
|
+
seconds -= 1.0
|
|
145
|
+
cache = self._timestamp_cache
|
|
146
|
+
if cache is None or cache[0] != seconds or cache[1] is not self.timezone:
|
|
147
|
+
prefix = datetime.fromtimestamp(seconds, tz=self.timezone).strftime('%Y-%m-%d %H:%M:%S.')
|
|
148
|
+
cache = self._timestamp_cache = (seconds, self.timezone, prefix)
|
|
149
|
+
return f'{cache[2]}{microseconds:06d}'.ljust(27)
|
|
119
150
|
|
|
120
151
|
def _format_process(self, tid):
|
|
121
152
|
pid = self.threads_pids.get(tid, -1)
|
|
@@ -184,4 +215,5 @@ class PyKdebugParser:
|
|
|
184
215
|
return event_rep
|
|
185
216
|
|
|
186
217
|
def _is_eventid_allowed(self, event_id):
|
|
187
|
-
return (event_id >> 24 in self.filter_class) or (event_id >> 16 in self.filter_subclass)
|
|
218
|
+
return ((event_id >> 24 in self.filter_class) or (event_id >> 16 in self.filter_subclass)
|
|
219
|
+
or event_id in self.filter_eventid)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Iterable, Mapping, Optional, Set
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def from_trace_codes_text(codes_text: str) -> Mapping[int, str]:
|
|
6
|
+
"""
|
|
7
|
+
Convert a trace codes text to dictionary.
|
|
8
|
+
:param codes_text: Trace codes file data.
|
|
9
|
+
:return: Mapping between code and event name.
|
|
10
|
+
"""
|
|
11
|
+
return {int(s[0], 16): s[1] for s in map(lambda l: l.split(), codes_text.splitlines())}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def from_trace_codes_file(path: str) -> Mapping[int, str]:
|
|
15
|
+
"""
|
|
16
|
+
Read trace codes from a file.
|
|
17
|
+
:param path: Trace codes file path.
|
|
18
|
+
:return: Mapping between code and event name.
|
|
19
|
+
"""
|
|
20
|
+
with open(path, 'r') as fd:
|
|
21
|
+
return from_trace_codes_text(fd.read())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def default_trace_codes() -> Mapping[int, str]:
|
|
25
|
+
"""
|
|
26
|
+
Get the default trace codes mapping.
|
|
27
|
+
:return: Mapping between code and event name.
|
|
28
|
+
"""
|
|
29
|
+
with open(Path(__file__).resolve().parent.joinpath('trace.codes'), 'r') as fd:
|
|
30
|
+
return from_trace_codes_text(fd.read())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def syscall_eventids(names: Iterable[str], trace_codes: Optional[Mapping[int, str]] = None) -> Set[int]:
|
|
34
|
+
"""
|
|
35
|
+
Get the event ids of BSD syscalls by name, for `PyKdebugParser.filter_eventid`.
|
|
36
|
+
A name also matches its `_nocancel` variant (e.g. `open` gives `BSC_open` and `BSC_open_nocancel`).
|
|
37
|
+
:param names: Syscall names, such as `open` or `openat`.
|
|
38
|
+
:param trace_codes: Mapping between code and event name; the default trace codes if omitted.
|
|
39
|
+
:return: The event ids.
|
|
40
|
+
:raises ValueError: A name is not a known syscall.
|
|
41
|
+
"""
|
|
42
|
+
codes = {name: code for code, name in (default_trace_codes() if trace_codes is None else trace_codes).items()}
|
|
43
|
+
eventids = set()
|
|
44
|
+
unknown = []
|
|
45
|
+
for name in names:
|
|
46
|
+
matches = {codes[event] for event in (f'BSC_{name}', f'BSC_{name}_nocancel') if event in codes}
|
|
47
|
+
if not matches:
|
|
48
|
+
unknown.append(name)
|
|
49
|
+
eventids |= matches
|
|
50
|
+
if unknown:
|
|
51
|
+
raise ValueError(f'unknown syscalls: {", ".join(unknown)}')
|
|
52
|
+
return eventids
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class UnknownEnumValue(int):
|
|
2
|
+
"""
|
|
3
|
+
A raw value that its enum doesn't define (e.g. a combination of flags or a value added in a newer OS).
|
|
4
|
+
It has `name` and `value` like an enum member, so it is formatted as the raw number instead of failing the parse.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return str(int(self))
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def value(self) -> int:
|
|
13
|
+
return int(self)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_enum(enum_type, value):
|
|
17
|
+
"""
|
|
18
|
+
Convert a raw value to an enum member, falling back to `UnknownEnumValue` for values the enum doesn't define.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
return enum_type(value)
|
|
22
|
+
except ValueError:
|
|
23
|
+
return UnknownEnumValue(value)
|