1*0f4c859eSApple OSS Distributions# XNU debugging 2*0f4c859eSApple OSS Distributions 3*0f4c859eSApple OSS Distributionsxnu’s debugging macros are compatible with both Python 2 and 3. In practice, this means that Python 3 4*0f4c859eSApple OSS Distributionsfeatures are unavailable and some Python 2 syntax is not allowed. Unfortunately, any syntax error will 5*0f4c859eSApple OSS Distributionsprevent use of all the macros, as they’re all imported into the same scripting environment. 6*0f4c859eSApple OSS Distributions 7*0f4c859eSApple OSS Distributions## Compatibility 8*0f4c859eSApple OSS Distributions 9*0f4c859eSApple OSS DistributionsAvoid introducing specific compatibility shims, as there are a few existing ones that come with 10*0f4c859eSApple OSS DistributionsPython 2 and 3: 11*0f4c859eSApple OSS Distributions 12*0f4c859eSApple OSS Distributions* **six** has helpers that work in both Python 2 and 3, for things like the string type change 13*0f4c859eSApple OSS Distributions* **future** backports features from Python 3 to Python 2 14*0f4c859eSApple OSS Distributions 15*0f4c859eSApple OSS DistributionsFor example, Python 2 contains **range** and **xrange**. Python 3 contains only **range** which has 16*0f4c859eSApple OSS Distributions**xrange** semantics. The simplest solution is to port your code and use Python 3 way: 17*0f4c859eSApple OSS Distributions 18*0f4c859eSApple OSS Distributions``` 19*0f4c859eSApple OSS Distributions# Use backported range from Python 3 20*0f4c859eSApple OSS Distributionsfrom builtins import range 21*0f4c859eSApple OSS Distributions 22*0f4c859eSApple OSS Distributions# Use range on both Python 2/3 runtimes 23*0f4c859eSApple OSS Distributionsfor x in range(....): 24*0f4c859eSApple OSS Distributions .... 25*0f4c859eSApple OSS Distributions``` 26*0f4c859eSApple OSS Distributions 27*0f4c859eSApple OSS DistributionsBe very careful about using imports from 'future' library. Some of them are **very invasive** and change 28*0f4c859eSApple OSS Distributionsbehavior of your code. This may cause strange runtime errors. For example: 29*0f4c859eSApple OSS Distributions 30*0f4c859eSApple OSS Distributions``` 31*0f4c859eSApple OSS Distributions# Changes modules handling logic to make your code working with std library reorg (PEP 3108) 32*0f4c859eSApple OSS Distributionsfrom future import standard_library 33*0f4c859eSApple OSS Distributionsstandard_library.install_aliases() 34*0f4c859eSApple OSS Distributions 35*0f4c859eSApple OSS Distributions# Replaces lot of common types like str with future's Python 3 backports. 36*0f4c859eSApple OSS Distributionsfrom builtins import * 37*0f4c859eSApple OSS Distributions``` 38*0f4c859eSApple OSS Distributions 39*0f4c859eSApple OSS Distributions## Handling strings 40*0f4c859eSApple OSS Distributions 41*0f4c859eSApple OSS DistributionsMacros use strings produced from the LLDB runtime. They must use **six** when doing certain operations 42*0f4c859eSApple OSS Distributionsto avoid exceptions. Until the transition is done, these canonical ways of dealing with strings cannot 43*0f4c859eSApple OSS Distributionsbe used: 44*0f4c859eSApple OSS Distributions 45*0f4c859eSApple OSS Distributions* Using Unicode literals by default: 46*0f4c859eSApple OSS Distributions `from __future__ import unicode_literals` 47*0f4c859eSApple OSS Distributions* **f-strings** 48*0f4c859eSApple OSS Distributions 49*0f4c859eSApple OSS DistributionsSome advice: 50*0f4c859eSApple OSS Distributions 51*0f4c859eSApple OSS Distributions* Use byte strings explicitly when dealing with memory and not strings: 52*0f4c859eSApple OSS Distributions `b'string'` 53*0f4c859eSApple OSS Distributions* Always properly encode/decode raw data to/from strings before passing it around, with `six.ensure_str` or 54*0f4c859eSApple OSS Distributions `six.ensure_bytes`. 55*0f4c859eSApple OSS Distributions 56*0f4c859eSApple OSS DistributionsImproperly-typed strings will raise *different* exceptions on each runtime. 57*0f4c859eSApple OSS Distributions 58*0f4c859eSApple OSS Distributions* Python 2 raises codec exceptions when printing strings. 59*0f4c859eSApple OSS Distributions* Python 3 complains about concatenation of objects of incompatible types (bytes and strings). 60*0f4c859eSApple OSS Distributions 61*0f4c859eSApple OSS Distributions### No convenient, common string type 62*0f4c859eSApple OSS Distributions 63*0f4c859eSApple OSS DistributionsWhile it is possible to use future’s **newstr** to backport new string type to Python 3, there are 64*0f4c859eSApple OSS Distributionsissues with the Scripting Bridge (SB) API from LLDB. Python 3 will work out of the box but Python 2 65*0f4c859eSApple OSS Distributionswill complain because **newstr** maps to **unicode**. SB exposes **const char \*** as a native string, 66*0f4c859eSApple OSS Distributionsor just **str** in Python 2. For Python 2 we would have to explicitly encode all Unicode strings 67*0f4c859eSApple OSS Distributionsbefore calling the API. 68*0f4c859eSApple OSS Distributions 69*0f4c859eSApple OSS DistributionsAnother problem is that literals in form `'string'` are no longer compatible with unicode and need 70*0f4c859eSApple OSS Distributionsto be switched to `u'string'`. This can be changed with single import at the top of the file, but 71*0f4c859eSApple OSS Distributionsin some scenarios byte strings are expected. That change would require checking all strings in the 72*0f4c859eSApple OSS Distributionscode and changing some back to `b'string'`. 73*0f4c859eSApple OSS Distributions 74*0f4c859eSApple OSS DistributionsHere’s an example of just how pervasive a change would be because this code would break in Python 2: 75*0f4c859eSApple OSS Distributions 76*0f4c859eSApple OSS Distributions``` 77*0f4c859eSApple OSS Distributionsfrom xnu import * 78*0f4c859eSApple OSS Distributions 79*0f4c859eSApple OSS Distributions@lldb_type_summary(['type']) 80*0f4c859eSApple OSS Distributionsdef print_summary(): 81*0f4c859eSApple OSS Distributions .... 82*0f4c859eSApple OSS Distributions``` 83*0f4c859eSApple OSS Distributions 84*0f4c859eSApple OSS DistributionsThe result is that we have non-unicode literal being registered with unicode API in Python 3. 85*0f4c859eSApple OSS DistributionsUnfortunately `'type' != b'type'` and thus LLDB will never match the type when printing summaries. 86*0f4c859eSApple OSS Distributions 87*0f4c859eSApple OSS DistributionsUsing native strings and literals allows for only minimal code changes to the macros that are still 88*0f4c859eSApple OSS Distributionscompatible with other projects using Python 2. 89*0f4c859eSApple OSS Distributions 90*0f4c859eSApple OSS Distributions### Check that an object is a string 91*0f4c859eSApple OSS Distributions 92*0f4c859eSApple OSS DistributionsAvoid testing for `str` explicitly like `type(obj) == str`. This won’t work correctly as Python 2 93*0f4c859eSApple OSS Distributionshas multiple string types (`unicode`, `str`). Additionally, compatibility shims might introduce new 94*0f4c859eSApple OSS Distributionsstring types. 95*0f4c859eSApple OSS Distributions 96*0f4c859eSApple OSS DistributionsInstead, always use an inheritance-sensitive like like `isinstance(obj, six.string_types)`. 97*0f4c859eSApple OSS Distributions 98*0f4c859eSApple OSS Distributions### Dealing with binary data 99*0f4c859eSApple OSS Distributions 100*0f4c859eSApple OSS DistributionsPython 2 bytes and strings are the same thing. This was the wrong design decision and Python 3 101*0f4c859eSApple OSS Distributions(wisely) switched to using a separate type for human text. This lack of distinction in Python 2 102*0f4c859eSApple OSS Distributionscaused many programming errors, so it’s recommended to use **bytearray**, **bytes**, and 103*0f4c859eSApple OSS Distributions**memoryviews** instead of a string. If a string is really required, encode the raw data explicitly 104*0f4c859eSApple OSS Distributionsusing an escape method. 105*0f4c859eSApple OSS Distributions 106*0f4c859eSApple OSS Distributions### Accessing large amounts of binary data (or accessing small amounts frequently) 107*0f4c859eSApple OSS Distributions 108*0f4c859eSApple OSS DistributionsIn case you're planning on accessing large contiguous blocks of memory (e.g. reading a whole 10KB of memory), 109*0f4c859eSApple OSS Distributionsor you're accessing small semi-contiguous chunks (e.g. if you're parsing large structured data), then it might 110*0f4c859eSApple OSS Distributionsbe hugely beneficial performance-wise to make use of the `io.SBProcessRawIO` class. Furthermore, if you're in 111*0f4c859eSApple OSS Distributionsa hurry and just want to read one specific chunk once, then it might be easier to use `LazyTarget.GetProcess().ReadMemory()` 112*0f4c859eSApple OSS Distributionsdirectly. 113*0f4c859eSApple OSS Distributions 114*0f4c859eSApple OSS DistributionsIn other words, avoid the following: 115*0f4c859eSApple OSS Distributions 116*0f4c859eSApple OSS Distributions``` 117*0f4c859eSApple OSS Distributionsdata_ptr = kern.GetValueFromAddress(start_addr, 'uint8_t *') 118*0f4c859eSApple OSS Distributionswith open(filepath, 'wb') as f: 119*0f4c859eSApple OSS Distributions f.write(data_ptr[:4096]) 120*0f4c859eSApple OSS Distributions``` 121*0f4c859eSApple OSS Distributions 122*0f4c859eSApple OSS DistributionsAnd instead use: 123*0f4c859eSApple OSS Distributions 124*0f4c859eSApple OSS Distributions``` 125*0f4c859eSApple OSS Distributionsfrom core.io import SBProcessRawIO 126*0f4c859eSApple OSS Distributionsimport shutil 127*0f4c859eSApple OSS Distributions 128*0f4c859eSApple OSS Distributionsio_access = SBProcessRawIO(LazyTarget.GetProcess(), start_addr, 4096) 129*0f4c859eSApple OSS Distributionswith open(filepath, 'wb') as f: 130*0f4c859eSApple OSS Distributions shutil.copyfileobj(io_access, f) 131*0f4c859eSApple OSS Distributions``` 132*0f4c859eSApple OSS Distributions 133*0f4c859eSApple OSS DistributionsOr, if you're in a hurry: 134*0f4c859eSApple OSS Distributions 135*0f4c859eSApple OSS Distributions``` 136*0f4c859eSApple OSS Distributionserr = lldb.SBError() 137*0f4c859eSApple OSS Distributionsmy_data = LazyTarget.GetProcess().ReadMemory(start_addr, length, err) 138*0f4c859eSApple OSS Distributionsif err.Success(): 139*0f4c859eSApple OSS Distributions # Use my precious data 140*0f4c859eSApple OSS Distributions pass 141*0f4c859eSApple OSS Distributions``` 142*0f4c859eSApple OSS Distributions 143*0f4c859eSApple OSS DistributionsFor small semi-contiguous chunks, you can map the whole region and access random chunks from it like so: 144*0f4c859eSApple OSS Distributions 145*0f4c859eSApple OSS Distributions``` 146*0f4c859eSApple OSS Distributionsfrom core.io import SBProcessRawIO 147*0f4c859eSApple OSS Distributions 148*0f4c859eSApple OSS Distributionsio_access = SBProcessRawIO(LazyTarget.GetProcess(), start_addr, size) 149*0f4c859eSApple OSS Distributionsio_access.seek(my_struct_offset) 150*0f4c859eSApple OSS Distributionsmy_struct_contents = io_access.read(my_struct_size) 151*0f4c859eSApple OSS Distributions``` 152*0f4c859eSApple OSS Distributions 153*0f4c859eSApple OSS DistributionsNot only that, but you can also tack on a BufferedRandom class on top of the SBProcessRawIO instance, which 154*0f4c859eSApple OSS Distributionsprovides you with buffering (aka caching) in case your random small chunk accesses are repeated: 155*0f4c859eSApple OSS Distributions 156*0f4c859eSApple OSS Distributions``` 157*0f4c859eSApple OSS Distributionsfrom core.io import SBProcessRawIO 158*0f4c859eSApple OSS Distributionsfrom io import BufferedRandom 159*0f4c859eSApple OSS Distributions 160*0f4c859eSApple OSS Distributionsio_access = SBProcessRawIO(LazyTarget.GetProcess(), start_addr, size) 161*0f4c859eSApple OSS Distributionsbuffered_io = BufferedRandom(io_access) 162*0f4c859eSApple OSS Distributions# And then use buffered_io for your accesses 163*0f4c859eSApple OSS Distributions``` 164*0f4c859eSApple OSS Distributions 165*0f4c859eSApple OSS Distributions### Encoding data to strings and back 166*0f4c859eSApple OSS Distributions 167*0f4c859eSApple OSS DistributionsThe simplest solution is to use **six** library and one of the functions like: 168*0f4c859eSApple OSS Distributions 169*0f4c859eSApple OSS Distributions``` 170*0f4c859eSApple OSS Distributionsmystring = six.ensure_str(object) 171*0f4c859eSApple OSS Distributions``` 172*0f4c859eSApple OSS Distributions 173*0f4c859eSApple OSS DistributionsThis ensures the resulting value is a native string. It deals with Unicode in Python 2 automatically. 174*0f4c859eSApple OSS DistributionsThe six library is still required even if data is encoding manually, since it converts types. 175*0f4c859eSApple OSS Distributions 176*0f4c859eSApple OSS Distributions``` 177*0f4c859eSApple OSS Distributionsfrom builtins import bytes 178*0f4c859eSApple OSS Distributionsstr = six.ensure_str(bytes.decode('utf-8')) 179*0f4c859eSApple OSS Distributions``` 180*0f4c859eSApple OSS Distributions 181*0f4c859eSApple OSS DistributionsWhen converting data to a string, add an encoding type so Python knows how handle raw bytes. In most 182*0f4c859eSApple OSS Distributionscases **utf-8** will work but be careful to be sure that the encoding matches your data. 183*0f4c859eSApple OSS Distributions 184*0f4c859eSApple OSS DistributionsThere are two options to consider when trying to get a string out of the raw data without knowing if 185*0f4c859eSApple OSS Distributionsthey are valid string or not: 186*0f4c859eSApple OSS Distributions 187*0f4c859eSApple OSS Distributions* **lossy conversion** - escapes all non-standard characters in form of ‘\xNNN’ 188*0f4c859eSApple OSS Distributions* **lossless conversion** - maps invalid characters to special unicode range so it can reconstruct 189*0f4c859eSApple OSS Distributionsthe string precisely 190*0f4c859eSApple OSS Distributions 191*0f4c859eSApple OSS DistributionsWhich to use depends on the transformation goals. The lossy conversion produces a printable string 192*0f4c859eSApple OSS Distributionswith strange characters in it. The lossless option is meant to be used when a string is only a transport 193*0f4c859eSApple OSS Distributionsmechanism and needs to be converted back to original values later. 194*0f4c859eSApple OSS Distributions 195*0f4c859eSApple OSS DistributionsSwitch the method by using `errors` handler during conversion: 196*0f4c859eSApple OSS Distributions 197*0f4c859eSApple OSS Distributions``` 198*0f4c859eSApple OSS Distributions# Lossy escapes invalid chars 199*0f4c859eSApple OSS Distributionsb.decode('utf-8', errors='`backslashreplace'`) 200*0f4c859eSApple OSS Distributions# Lossy removes invalid chars 201*0f4c859eSApple OSS Distributionsb.decode('utf-8', errors='ignore') 202*0f4c859eSApple OSS Distributions# Loss-less but may likely fail to print() 203*0f4c859eSApple OSS Distributionsb.decode('utf-8', errors='surrogateescape') 204*0f4c859eSApple OSS Distributions``` 205*0f4c859eSApple OSS Distributions 206*0f4c859eSApple OSS Distributions## Handling numbers 207*0f4c859eSApple OSS Distributions 208*0f4c859eSApple OSS DistributionsNumeric types are incompatible between Python 2 and 3: 209*0f4c859eSApple OSS Distributions 210*0f4c859eSApple OSS Distributions* **long** is not available in Python 3. 211*0f4c859eSApple OSS Distributions* **int** is the only integral type in Python 3 and hasunlimited precission (but 32-bits in Python 2). 212*0f4c859eSApple OSS Distributions 213*0f4c859eSApple OSS DistributionsThis creates all sorts of issues with macros. Follow these rules to make integral types compatible 214*0f4c859eSApple OSS Distributionsin both modes: 215*0f4c859eSApple OSS Distributions 216*0f4c859eSApple OSS Distributions* Do not use **long** — replace it with **int**. 217*0f4c859eSApple OSS Distributions* When using the **value** class, types will be promoted to **long** as there is special number 218*0f4c859eSApple OSS Distributionshandling in the xnu macro library. Remaining code should be reviewed and fixed, if appropriate. 219*0f4c859eSApple OSS Distributions* Avoid relying on sign extension. 220*0f4c859eSApple OSS Distributions* Always switch Python to use Python 3 division, where `/` converts to floating point and does 221*0f4c859eSApple OSS Distributionsa fractional division `//` is a floor division (like integers in C): 222*0f4c859eSApple OSS Distributions `from __future__ import division 223*0f4c859eSApple OSS Distributions ` 224*0f4c859eSApple OSS Distributions* Use division operators according to Python 3 rules. 225*0f4c859eSApple OSS Distributions 226*0f4c859eSApple OSS Distributions### Common integer representation 227*0f4c859eSApple OSS Distributions 228*0f4c859eSApple OSS DistributionsThe goal is to always use Python 3’s integer handling, which means using **int** everywhere. 229*0f4c859eSApple OSS Distributions 230*0f4c859eSApple OSS Distributionsxnu’s macros provide a custom integer type called **valueint** that is a replacement for **int** 231*0f4c859eSApple OSS Distributionsin the Python 2 runtime. That means it behaves almost like **int** from Python 3. When importing 232*0f4c859eSApple OSS Distributionsfrom macros this type replaces any use of **int**: 233*0f4c859eSApple OSS Distributions 234*0f4c859eSApple OSS Distributions``` 235*0f4c859eSApple OSS Distributions# Replaces all int()s to be valueint 236*0f4c859eSApple OSS Distributionsfrom xnu import * 237*0f4c859eSApple OSS Distributionsfrom xnu import int 238*0f4c859eSApple OSS Distributions 239*0f4c859eSApple OSS Distributions# Does not replace int()s 240*0f4c859eSApple OSS Distributionsimport xnu 241*0f4c859eSApple OSS Distributionsfrom xnu import a, b, c 242*0f4c859eSApple OSS Distributions``` 243*0f4c859eSApple OSS Distributions 244*0f4c859eSApple OSS DistributionsAvoid using `from builtins import int` suggested on the internet. It does not work correctly with 245*0f4c859eSApple OSS Distributionsxnu’s **value** class. The **valueint** class inherits from **newint** and fixes problematic behavior. 246*0f4c859eSApple OSS Distributions 247*0f4c859eSApple OSS DistributionsThis impacts the way an object is checked for being an integer. Be careful about following constructs: 248*0f4c859eSApple OSS Distributions 249*0f4c859eSApple OSS Distributions``` 250*0f4c859eSApple OSS Distributions# BAD: generally not a good way to do type checking in Python 251*0f4c859eSApple OSS Distributionsif type(obj) is int: 252*0f4c859eSApple OSS Distributions 253*0f4c859eSApple OSS Distributions# BAD: int may have been replaced with valueint. 254*0f4c859eSApple OSS Distributionsif isinstance(obj, int): 255*0f4c859eSApple OSS Distributions``` 256*0f4c859eSApple OSS Distributions 257*0f4c859eSApple OSS DistributionsInstead, use the base integral type: 258*0f4c859eSApple OSS Distributions 259*0f4c859eSApple OSS Distributions``` 260*0f4c859eSApple OSS Distributionsif isinstance(obj, numbers.Integral): 261*0f4c859eSApple OSS Distributions``` 262*0f4c859eSApple OSS Distributions 263*0f4c859eSApple OSS Distributions### Dealing with signed numbers 264*0f4c859eSApple OSS Distributions 265*0f4c859eSApple OSS DistributionsOriginal code was using two operators to convert **value** class instance to number: 266*0f4c859eSApple OSS Distributions 267*0f4c859eSApple OSS Distributions* **__int__** produced **int** and was either signed or unsigned based on underlying SBType. 268*0f4c859eSApple OSS Distributions* **__long__** was always signed. 269*0f4c859eSApple OSS Distributions 270*0f4c859eSApple OSS DistributionsThis is confusing when dealing with types. Always use **unsigned()** or **signed()** regardless of 271*0f4c859eSApple OSS Distributionswhat the actual underlying type is to ensure that macros use the correct semantics. 272*0f4c859eSApple OSS Distributions 273*0f4c859eSApple OSS Distributions### Dividing numbers 274*0f4c859eSApple OSS Distributions 275*0f4c859eSApple OSS DistributionsPython 2’s **/** operator has two behaviors depending on the types of its arguments (**float** vs. **int**). 276*0f4c859eSApple OSS DistributionsAlways use Python 3’s division operator: 277*0f4c859eSApple OSS Distributions 278*0f4c859eSApple OSS Distributions``` 279*0f4c859eSApple OSS Distributions# Switch compiler to use Python 3 semantics 280*0f4c859eSApple OSS Distributionsfrom __future__ import division 281*0f4c859eSApple OSS Distributions 282*0f4c859eSApple OSS Distributionsfloat_val = a / b # This becomes true, fractional division that yields float 283*0f4c859eSApple OSS Distributionsfloor_div = a // b # This is floor division, like C 284*0f4c859eSApple OSS Distributions``` 285*0f4c859eSApple OSS Distributions 286*0f4c859eSApple OSS DistributionsIf the original behavior is required, use **old_div** to get Python 2 behavior: 287*0f4c859eSApple OSS Distributions 288*0f4c859eSApple OSS Distributions``` 289*0f4c859eSApple OSS Distributionsfrom past.utils import old_div 290*0f4c859eSApple OSS Distributions 291*0f4c859eSApple OSS Distributionsvalue = old_div(a, b) # Matches Python 2 semantics 292*0f4c859eSApple OSS Distributions``` 293*0f4c859eSApple OSS Distributions 294*0f4c859eSApple OSS DistributionsIf this isn’t handled correctly, `format` will complain that a float value is being passed to 295*0f4c859eSApple OSS Distributionsa non-float formatting character. Automated scripts that convert from Python 2 to 3 tend to use 296*0f4c859eSApple OSS Distributions**old_div** during porting. In most cases that is not required. For kernel debugging and integer 297*0f4c859eSApple OSS Distributionstypes, `//` is used commonly to match the C’s division behavior for integers. 298*0f4c859eSApple OSS Distributions 299*0f4c859eSApple OSS Distributions## Testing changes 300*0f4c859eSApple OSS Distributions 301*0f4c859eSApple OSS DistributionsThere is no perfect test suite to check that macros are producing a correct value compared to what 302*0f4c859eSApple OSS Distributionsthe debugger sees in a target. 303*0f4c859eSApple OSS Distributions 304*0f4c859eSApple OSS DistributionsBe careful when touching common framework code. For larger changes, ask the Platform Triage team to 305*0f4c859eSApple OSS Distributionsvalidate that the changes work in their environment before integration. 306*0f4c859eSApple OSS Distributions 307*0f4c859eSApple OSS Distributions### Coding style 308*0f4c859eSApple OSS Distributions 309*0f4c859eSApple OSS DistributionsUse a static analyzer like **pylint** or **flake8** to check the macro source code: 310*0f4c859eSApple OSS Distributions 311*0f4c859eSApple OSS Distributions``` 312*0f4c859eSApple OSS Distributions# Python 2 313*0f4c859eSApple OSS Distributions$ pip install --user pylint flake8 314*0f4c859eSApple OSS Distributions 315*0f4c859eSApple OSS Distributions# Python 3 316*0f4c859eSApple OSS Distributions$ pip install --user pylint flake8 317*0f4c859eSApple OSS Distributions 318*0f4c859eSApple OSS Distributions# Run the lint either by setting your path to point to one of the runtimes 319*0f4c859eSApple OSS Distributions# or through python 320*0f4c859eSApple OSS Distributions$ python2 -m pylint <src files/dirs> 321*0f4c859eSApple OSS Distributions$ python3 -m pylint <src files/dirs> 322*0f4c859eSApple OSS Distributions$ python2 -m flake8 <src files/dirs> 323*0f4c859eSApple OSS Distributions$ python3 -m flake8 <src files/dirs> 324*0f4c859eSApple OSS Distributions``` 325*0f4c859eSApple OSS Distributions 326*0f4c859eSApple OSS Distributions### Correctness 327*0f4c859eSApple OSS Distributions 328*0f4c859eSApple OSS DistributionsEnsure the macro matches what LLDB returns from the REPL. For example, compare `showproc(xxx)` with `p/x *(proc_t)xxx`. 329*0f4c859eSApple OSS Distributions 330*0f4c859eSApple OSS Distributions``` 331*0f4c859eSApple OSS Distributions# 1. Run LLDB with debug options set 332*0f4c859eSApple OSS Distributions$ DEBUG_XNU_LLDBMACROS=1 LLDB_DEFAULT_PYTHON_VERSION=2 xcrun -sdk <sdk> lldb -c core <dsympath>/mach_kernel 333*0f4c859eSApple OSS Distributions 334*0f4c859eSApple OSS Distributions# 2. Optionally load modified operating system plugin 335*0f4c859eSApple OSS Distributions(lldb) settings set target.process.python-os-plugin-path <srcpath>/tools/lldbmacros/core/operating_system.py 336*0f4c859eSApple OSS Distributions 337*0f4c859eSApple OSS Distributions# 3. Load modified scripts 338*0f4c859eSApple OSS Distributions(lldb) command script import <srcpath>/tools/lldbmacros/xnu.py 339*0f4c859eSApple OSS Distributions 340*0f4c859eSApple OSS Distributions# 4. Exercise macros 341*0f4c859eSApple OSS Distributions``` 342*0f4c859eSApple OSS Distributions 343*0f4c859eSApple OSS DistributionsDepending on the change, test other targets and architectures (for instance, both Astris and KDP). 344*0f4c859eSApple OSS Distributions 345*0f4c859eSApple OSS Distributions### Regression 346*0f4c859eSApple OSS Distributions 347*0f4c859eSApple OSS DistributionsThis is simpler than previous step because the goal is to ensure behavior has not changed. 348*0f4c859eSApple OSS DistributionsYou can speed up few things by using local symbols: 349*0f4c859eSApple OSS Distributions 350*0f4c859eSApple OSS Distributions``` 351*0f4c859eSApple OSS Distributions# 1. Get a coredump from a device and kernel UUID 352*0f4c859eSApple OSS Distributions# 2. Grab symbols with dsymForUUID 353*0f4c859eSApple OSS Distributions$ dsymForUUID --nocache --copyExecutable --copyDestination <dsym path> 354*0f4c859eSApple OSS Distributions 355*0f4c859eSApple OSS Distributions# 3. Run lldb with local symbols to avoid dsymForUUID NFS 356*0f4c859eSApple OSS Distributions 357*0f4c859eSApple OSS Distributions$ xcrun -sdk <sdk> lldb -c core <dsym_path>/<kernel image> 358*0f4c859eSApple OSS Distributions``` 359*0f4c859eSApple OSS Distributions 360*0f4c859eSApple OSS DistributionsThe actual steps are identical to previous testing. Run of a macro to different file with `-o <outfile>` 361*0f4c859eSApple OSS Distributionsoption. Then run `diff` on the outputs of the baseline and both Python 2 and 3: 362*0f4c859eSApple OSS Distributions 363*0f4c859eSApple OSS Distributions* No environment variables to get baseline 364*0f4c859eSApple OSS Distributions* Python 2 with changes 365*0f4c859eSApple OSS Distributions* Python 3 with changes 366*0f4c859eSApple OSS Distributions 367*0f4c859eSApple OSS DistributionsThere may be different ordering of elements based on internal implementation differences of each 368*0f4c859eSApple OSS DistributionsPython runtime. Some macros produce files — check the actual file contents. 369*0f4c859eSApple OSS Distributions 370*0f4c859eSApple OSS DistributionsIt’s difficult to make this automated: 371*0f4c859eSApple OSS Distributions 372*0f4c859eSApple OSS Distributions* Some macros needs arguments which must be found in a core file. 373*0f4c859eSApple OSS Distributions* Some macros take a long time to run against a target (more than 30 minutes). Instead, a core dump 374*0f4c859eSApple OSS Distributions should be taken and then inspected afterwards, but this ties up a lab device for the duration of the 375*0f4c859eSApple OSS Distributions test. 376*0f4c859eSApple OSS Distributions* Even with coredumps, testing the macros takes too long in our automation system and triggers the 377*0f4c859eSApple OSS Distributions failsafe timeout. 378*0f4c859eSApple OSS Distributions 379*0f4c859eSApple OSS Distributions### Code coverage 380*0f4c859eSApple OSS Distributions 381*0f4c859eSApple OSS DistributionsUse code coverage to check which parts of macros have actually been tested. 382*0f4c859eSApple OSS DistributionsInstall **coverage** lib with: 383*0f4c859eSApple OSS Distributions 384*0f4c859eSApple OSS Distributions``` 385*0f4c859eSApple OSS Distributions$ pip install --user coverage 386*0f4c859eSApple OSS Distributions$ pip3 install --user coverage 387*0f4c859eSApple OSS Distributions``` 388*0f4c859eSApple OSS Distributions 389*0f4c859eSApple OSS DistributionsThen collect coverage:. 390*0f4c859eSApple OSS Distributions 391*0f4c859eSApple OSS Distributions``` 392*0f4c859eSApple OSS Distributions# 1. Start LLDB with your macros as described above. 393*0f4c859eSApple OSS Distributions 394*0f4c859eSApple OSS Distributions# 2. Load and start code coverage recording. 395*0f4c859eSApple OSS Distributions(lldb) script import coverage 396*0f4c859eSApple OSS Distributions(lldb) script cov = coverage.Coverage() 397*0f4c859eSApple OSS Distributions(lldb) script cov.start() 398*0f4c859eSApple OSS Distributions 399*0f4c859eSApple OSS Distributions# 3. Do the testing. 400*0f4c859eSApple OSS Distributions 401*0f4c859eSApple OSS Distributions# 4. Collect the coverage. 402*0f4c859eSApple OSS Distributions(lldb) script cov.stop() 403*0f4c859eSApple OSS Distributions(lldb) script cov.save() 404*0f4c859eSApple OSS Distributions``` 405*0f4c859eSApple OSS Distributions 406*0f4c859eSApple OSS DistributionsYou can override the default file (*.coverage*) by adding an additional environment variable to LLDB: 407*0f4c859eSApple OSS Distributions 408*0f4c859eSApple OSS Distributions``` 409*0f4c859eSApple OSS Distributions$ env COVERAGE_FILE="${OUTDIR}/.coverage.mytest.py2" # usual LLDB command line 410*0f4c859eSApple OSS Distributions``` 411*0f4c859eSApple OSS Distributions 412*0f4c859eSApple OSS DistributionsCombine coverage from multiple files: 413*0f4c859eSApple OSS Distributions 414*0f4c859eSApple OSS Distributions``` 415*0f4c859eSApple OSS Distributions# Point PATH to local python where coverage is installed. 416*0f4c859eSApple OSS Distributions$ export PATH="$HOME/Library/Python/3.8/bin:$PATH" 417*0f4c859eSApple OSS Distributions 418*0f4c859eSApple OSS Distributions# Use --keep to avoid deletion of input files after merge. 419*0f4c859eSApple OSS Distributions$ coverage combine --keep <list of .coverage files or dirs to scan> 420*0f4c859eSApple OSS Distributions 421*0f4c859eSApple OSS Distributions# Get HTML report or use other subcommands to inspect. 422*0f4c859eSApple OSS Distributions$ coverage html 423*0f4c859eSApple OSS Distributions``` 424*0f4c859eSApple OSS Distributions 425*0f4c859eSApple OSS DistributionsIt is possible to start coverage collection **before** importing the operating system library and 426*0f4c859eSApple OSS Distributionsloading macros to check code run during bootstrapping. 427*0f4c859eSApple OSS Distributions 428*0f4c859eSApple OSS Distributions### Performance testing 429*0f4c859eSApple OSS Distributions 430*0f4c859eSApple OSS DistributionsSome macros can run for a long time. Some code may be costly even if it looks simple because objects 431*0f4c859eSApple OSS Distributionsaren’t cached or too many temporary objects are created. Simple profiling is similar to collecting 432*0f4c859eSApple OSS Distributionscode coverage. 433*0f4c859eSApple OSS Distributions 434*0f4c859eSApple OSS DistributionsFirst setup your environment: 435*0f4c859eSApple OSS Distributions 436*0f4c859eSApple OSS Distributions``` 437*0f4c859eSApple OSS Distributions# Install gprof2dot 438*0f4c859eSApple OSS Distributions$ python3 -m pip install gprof2dot 439*0f4c859eSApple OSS Distributions# Install graphviz 440*0f4c859eSApple OSS Distributions$ brew install graphviz 441*0f4c859eSApple OSS Distributions``` 442*0f4c859eSApple OSS Distributions 443*0f4c859eSApple OSS DistributionsThen to profile commands, follow this sequence: 444*0f4c859eSApple OSS Distributions 445*0f4c859eSApple OSS Distributions``` 446*0f4c859eSApple OSS Distributions(lldb) xnudebug profile /tmp/macro.prof showcurrentstacks 447*0f4c859eSApple OSS Distributions[... command outputs ...] 448*0f4c859eSApple OSS Distributions 449*0f4c859eSApple OSS Distributions Ordered by: cumulative time 450*0f4c859eSApple OSS Distributions List reduced from 468 to 30 due to restriction <30> 451*0f4c859eSApple OSS Distributions 452*0f4c859eSApple OSS Distributions ncalls tottime percall cumtime percall filename:lineno(function) 453*0f4c859eSApple OSS Distributions [... profiling output ...] 454*0f4c859eSApple OSS Distributions 455*0f4c859eSApple OSS DistributionsProfile info saved to "/tmp/macro.prof" 456*0f4c859eSApple OSS Distributions``` 457*0f4c859eSApple OSS Distributions 458*0f4c859eSApple OSS DistributionsThen to visualize callgraphs in context, in a separate shell: 459*0f4c859eSApple OSS Distributions 460*0f4c859eSApple OSS Distributions``` 461*0f4c859eSApple OSS Distributions# Now convert the file to a colored SVG call graph 462*0f4c859eSApple OSS Distributions$ python3 -m gprof2dot -f pstats /tmp/macro.prof -o /tmp/call.dot 463*0f4c859eSApple OSS Distributions$ dot -O -T svg /tmp/call.dot 464*0f4c859eSApple OSS Distributions 465*0f4c859eSApple OSS Distributions# and view it in your favourite viewer 466*0f4c859eSApple OSS Distributions$ open /tmp/call.dot.svg 467*0f4c859eSApple OSS Distributions``` 468*0f4c859eSApple OSS Distributions 469*0f4c859eSApple OSS Distributions## Debugging your changes 470*0f4c859eSApple OSS Distributions 471*0f4c859eSApple OSS DistributionsYES, It is possible to use a debugger to debug your code! 472*0f4c859eSApple OSS Distributions 473*0f4c859eSApple OSS DistributionsThe steps are similar to testing techniques described above (use scripting interactive mode). There is no point to 474*0f4c859eSApple OSS Distributionsdocument the debugger itself. Lets focus on how to use it on a real life example. The debugger used here is PDB which 475*0f4c859eSApple OSS Distributionsis part of Python installation so works out of the box. 476*0f4c859eSApple OSS Distributions 477*0f4c859eSApple OSS DistributionsProblem: Something wrong is going on with addkext macro. What now? 478*0f4c859eSApple OSS Distributions 479*0f4c859eSApple OSS Distributions (lldb) addkext -N com.apple.driver.AppleT8103PCIeC 480*0f4c859eSApple OSS Distributions Failed to read MachO for address 18446741875027613136 errormessage: seek to offset 2169512 is outside window [0, 1310] 481*0f4c859eSApple OSS Distributions Failed to read MachO for address 18446741875033537424 errormessage: seek to offset 8093880 is outside window [0, 1536] 482*0f4c859eSApple OSS Distributions Failed to read MachO for address 18446741875033568304 errormessage: seek to offset 8124208 is outside window [0, 1536] 483*0f4c859eSApple OSS Distributions ... 484*0f4c859eSApple OSS Distributions Fetching dSYM for 049b9a29-2efc-32c0-8a7f-5f29c12b870c 485*0f4c859eSApple OSS Distributions Adding dSYM (049b9a29-2efc-32c0-8a7f-5f29c12b870c) for /Library/Caches/com.apple.bni.symbols/bursar.apple.com/dsyms/StarE/AppleEmbeddedPCIE/AppleEmbeddedPCIE-502.100.35~3/049B9A29-2EFC-32C0-8A7F-5F29C12B870C/AppleT8103PCIeC 486*0f4c859eSApple OSS Distributions section '__TEXT' loaded at 0xfffffe001478c780 487*0f4c859eSApple OSS Distributions 488*0f4c859eSApple OSS DistributionsThere is no exception, lot of errors and no output. So what next? 489*0f4c859eSApple OSS DistributionsTry to narrow the problem down to an isolated piece of macro code: 490*0f4c859eSApple OSS Distributions 491*0f4c859eSApple OSS Distributions 1. Try to get values of globals through regular LLDB commands 492*0f4c859eSApple OSS Distributions 2. Use interactive mode and invoke functions with arguments directly. 493*0f4c859eSApple OSS Distributions 494*0f4c859eSApple OSS DistributionsAfter inspecting addkext macro code and calling few functions with arguments directly we can see that there is an 495*0f4c859eSApple OSS Distributionsexception in the end. It was just captured in try/catch block. So the simplified reproducer is: 496*0f4c859eSApple OSS Distributions 497*0f4c859eSApple OSS Distributions (lldb) script 498*0f4c859eSApple OSS Distributions >>> import lldb 499*0f4c859eSApple OSS Distributions >>> import xnu 500*0f4c859eSApple OSS Distributions >>> err = lldb.SBError() 501*0f4c859eSApple OSS Distributions >>> data = xnu.LazyTarget.GetProcess().ReadMemory(0xfffffe0014c0f3f0, 0x000000000001b5d0, err) 502*0f4c859eSApple OSS Distributions >>> m = macho.MemMacho(data, len(data)) 503*0f4c859eSApple OSS Distributions Traceback (most recent call last): 504*0f4c859eSApple OSS Distributions File "<console>", line 1, in <module> 505*0f4c859eSApple OSS Distributions File ".../lldbmacros/macho.py", line 91, in __init__ 506*0f4c859eSApple OSS Distributions self.load(fp) 507*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/MachO.py", line 133, in load 508*0f4c859eSApple OSS Distributions self.load_header(fh, 0, size) 509*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/MachO.py", line 168, in load_header 510*0f4c859eSApple OSS Distributions hdr = MachOHeader(self, fh, offset, size, magic, hdr, endian) 511*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/MachO.py", line 209, in __init__ 512*0f4c859eSApple OSS Distributions self.load(fh) 513*0f4c859eSApple OSS Distributions File ".../lldbmacros/macho.py", line 23, in new_load 514*0f4c859eSApple OSS Distributions _old_MachOHeader_load(s, fh) 515*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/MachO.py", line 287, in load 516*0f4c859eSApple OSS Distributions fh.seek(seg.offset) 517*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/util.py", line 91, in seek 518*0f4c859eSApple OSS Distributions self._checkwindow(seekto, "seek") 519*0f4c859eSApple OSS Distributions File ".../site-packages/macholib/util.py", line 76, in _checkwindow 520*0f4c859eSApple OSS Distributions raise IOError( 521*0f4c859eSApple OSS Distributions OSError: seek to offset 9042440 is outside window [0, 112080] 522*0f4c859eSApple OSS Distributions 523*0f4c859eSApple OSS DistributionsClearly an external library is involved and execution flow jumps between dSYM and the library few times. 524*0f4c859eSApple OSS DistributionsLets try to look around with a debugger. 525*0f4c859eSApple OSS Distributions 526*0f4c859eSApple OSS Distributions (lldb) script 527*0f4c859eSApple OSS Distributions # Prepare data variable as described above. 528*0f4c859eSApple OSS Distributions 529*0f4c859eSApple OSS Distributions # Run last statement with debugger. 530*0f4c859eSApple OSS Distributions >>> import pdb 531*0f4c859eSApple OSS Distributions >>> pdb.run('m = macho.MemMacho(data, len(data))', globals(), locals()) 532*0f4c859eSApple OSS Distributions > <string>(1)<module>() 533*0f4c859eSApple OSS Distributions 534*0f4c859eSApple OSS Distributions # Show debugger's help 535*0f4c859eSApple OSS Distributions (Pdb) help 536*0f4c859eSApple OSS Distributions 537*0f4c859eSApple OSS DistributionsIt is not possible to break on exception. Python uses them a lot so it is better to put a breakpoint to source 538*0f4c859eSApple OSS Distributionscode. This puts breakpoint on the IOError exception mentioned above. 539*0f4c859eSApple OSS Distributions 540*0f4c859eSApple OSS Distributions (Pdb) break ~/Library/Python/3.8/lib/python/site-packages/macholib/util.py:76 541*0f4c859eSApple OSS Distributions Breakpoint 4 at ~/Library/Python/3.8/lib/python/site-packages/macholib/util.py:76 542*0f4c859eSApple OSS Distributions 543*0f4c859eSApple OSS DistributionsYou can now single step or continue the execution as usuall for a debugger. 544*0f4c859eSApple OSS Distributions 545*0f4c859eSApple OSS Distributions (Pdb) cont 546*0f4c859eSApple OSS Distributions > /Users/tjedlicka/Library/Python/3.8/lib/python/site-packages/macholib/util.py(76)_checkwindow() 547*0f4c859eSApple OSS Distributions -> raise IOError( 548*0f4c859eSApple OSS Distributions (Pdb) bt 549*0f4c859eSApple OSS Distributions /Volumes/.../Python3.framework/Versions/3.8/lib/python3.8/bdb.py(580)run() 550*0f4c859eSApple OSS Distributions -> exec(cmd, globals, locals) 551*0f4c859eSApple OSS Distributions <string>(1)<module>() 552*0f4c859eSApple OSS Distributions /Volumes/...dSYM/Contents/Resources/Python/lldbmacros/macho.py(91)__init__() 553*0f4c859eSApple OSS Distributions -> self.load(fp) 554*0f4c859eSApple OSS Distributions /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/MachO.py(133)load() 555*0f4c859eSApple OSS Distributions -> self.load_header(fh, 0, size) 556*0f4c859eSApple OSS Distributions /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/MachO.py(168)load_header() 557*0f4c859eSApple OSS Distributions -> hdr = MachOHeader(self, fh, offset, size, magic, hdr, endian) 558*0f4c859eSApple OSS Distributions /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/MachO.py(209)__init__() 559*0f4c859eSApple OSS Distributions -> self.load(fh) 560*0f4c859eSApple OSS Distributions /Volumes/...dSYM/Contents/Resources/Python/lldbmacros/macho.py(23)new_load() 561*0f4c859eSApple OSS Distributions -> _old_MachOHeader_load(s, fh) 562*0f4c859eSApple OSS Distributions /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/MachO.py(287)load() 563*0f4c859eSApple OSS Distributions -> fh.seek(seg.offset) 564*0f4c859eSApple OSS Distributions /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/util.py(91)seek() 565*0f4c859eSApple OSS Distributions -> self._checkwindow(seekto, "seek") 566*0f4c859eSApple OSS Distributions > /Users/.../Library/Python/3.8/lib/python/site-packages/macholib/util.py(76)_checkwindow() 567*0f4c859eSApple OSS Distributions -> raise IOError( 568*0f4c859eSApple OSS Distributions 569*0f4c859eSApple OSS Distributions 570*0f4c859eSApple OSS DistributionsNow we can move a frame above and inspect stopped target: 571*0f4c859eSApple OSS Distributions 572*0f4c859eSApple OSS Distributions # Show current frame arguments 573*0f4c859eSApple OSS Distributions (Pdb) up 574*0f4c859eSApple OSS Distributions (Pdb) a 575*0f4c859eSApple OSS Distributions self = <fileview [0, 112080] <macho.MemFile object at 0x1075cafd0>> 576*0f4c859eSApple OSS Distributions offset = 9042440 577*0f4c859eSApple OSS Distributions whence = 0 578*0f4c859eSApple OSS Distributions 579*0f4c859eSApple OSS Distributions # globals, local or expressons 580*0f4c859eSApple OSS Distributions (Pdb) p type(seg.offset) 581*0f4c859eSApple OSS Distributions <class 'macholib.ptypes.p_uint32'> 582*0f4c859eSApple OSS Distributions (Pdb) p hex(seg.offset) 583*0f4c859eSApple OSS Distributions '0x89fa08' 584*0f4c859eSApple OSS Distributions 585*0f4c859eSApple OSS Distributions # Find attributes of a Python object. 586*0f4c859eSApple OSS Distributions (Pdb) p dir(section_cls) 587*0f4c859eSApple OSS Distributions ['__class__', '__cmp__', ... ,'reserved3', 'sectname', 'segname', 'size', 'to_fileobj', 'to_mmap', 'to_str'] 588*0f4c859eSApple OSS Distributions (Pdb) p section_cls.sectname 589*0f4c859eSApple OSS Distributions <property object at 0x1077bbef0> 590*0f4c859eSApple OSS Distributions 591*0f4c859eSApple OSS DistributionsUnfortunately everything looks correct but there is actually one ineteresting frame in the stack. The one which 592*0f4c859eSApple OSS Distributionsprovides the offset to the seek method. Lets see where we are in the source code. 593*0f4c859eSApple OSS Distributions 594*0f4c859eSApple OSS Distributions (Pdb) up 595*0f4c859eSApple OSS Distributions > /Users/tjedlicka/Library/Python/3.8/lib/python/site-packages/macholib/MachO.py(287)load() 596*0f4c859eSApple OSS Distributions -> fh.seek(seg.offset) 597*0f4c859eSApple OSS Distributions (Pdb) list 598*0f4c859eSApple OSS Distributions 282 not_zerofill = (seg.flags & S_ZEROFILL) != S_ZEROFILL 599*0f4c859eSApple OSS Distributions 283 if seg.offset > 0 and seg.size > 0 and not_zerofill: 600*0f4c859eSApple OSS Distributions 284 low_offset = min(low_offset, seg.offset) 601*0f4c859eSApple OSS Distributions 285 if not_zerofill: 602*0f4c859eSApple OSS Distributions 286 c = fh.tell() 603*0f4c859eSApple OSS Distributions 287 -> fh.seek(seg.offset) 604*0f4c859eSApple OSS Distributions 288 sd = fh.read(seg.size) 605*0f4c859eSApple OSS Distributions 289 seg.add_section_data(sd) 606*0f4c859eSApple OSS Distributions 290 fh.seek(c) 607*0f4c859eSApple OSS Distributions 291 segs.append(seg) 608*0f4c859eSApple OSS Distributions 292 # data is a list of segments 609*0f4c859eSApple OSS Distributions 610*0f4c859eSApple OSS DistributionsRunning debugger on working case and stepping through the load() method shows that this code is not present. 611*0f4c859eSApple OSS DistributionsThat means we are broken by a library update! Older versions of library do not load data for a section. 612