def __str__(self, **kargs): ''' text output, to be used by an assembler ''' out = self.txt(**kargs) fmt = '%-50s ## ' if self.long_display and hasattr(self, 'pic'): pic_label, pic_list = self.pic pic_list = ';'.join([str(_) for _ in pic_list]) if pic_label is not None: pic_list = "%s:%s" % (pic_label, pic_list) out = (fmt + 'PIC:%s') % (out, pic_list) fmt = '%-80s # ' if self.long_display and hasattr(self, 'stack'): registers, positions = self.stack if isinstance(registers, bool): stack_status = '%s:' % registers else: stack_status = [] for k in sorted(registers): stack_status.append('%r: %r' % (k, sorted(list(registers[k])))) stack_status = '{' + ', '.join(stack_status) + '}:' stack_status += str(tuple(sorted(positions))) out = (fmt + 'STACK:%s') % (out, stack_status) fmt = '%-80s # ' if self.long_display and hasattr(self, 'dead'): dead = self.dead dead = sorted([str(_) for _ in dead]) out = (fmt + ' DEAD:%s') % (out, ';'.join(dead)) if hasattr(self, 'pic'): for pic in self.pic[1]: for reg in dead: if str(reg) in str(pic): out += ' COLLISION' return out
def get_ancestors(self): # if visible symbol, there may be external ancestors if self.is_new_object(): return [None] + sorted(self.graph[1]) # if parsed from assembly, many elements of the graph are missing, # compared to binary parsing, e.g. arrows for switch tables if not 'endianess' in self.symbols.meta and self.is_named(): return [None] + sorted(self.graph[1]) return sorted(self.graph[1])
def do_compare(obj1, obj2, quick=False): do_exit = False symbols1, dump1 = parse_objdump(obj1, quick=quick) symbols2, dump2 = parse_objdump(obj2, quick=quick) from difflib import context_diff diff = [] if sorted(symbols1) != sorted(symbols2): diff.extend(list(context_diff(symbols1, symbols2, n=1))) diff.extend(list(context_diff(dump1, dump2, n=1))) return diff
def mk_objdump(symbols, filename=None): if symbols.file_type != 'ELF': return "Cannot output a %s file in objdump format" % symbols.file_type import struct from elfesteem import elf e = symbols.file_elfesteem from plasmasm.python.compatibility import hexbytes output = "\n" filetype = struct.unpack("B", e.Ehdr.ident[elf.EI_CLASS:elf.EI_CLASS + 1])[0] if filetype == 1: filetype = 'elf32' elif filetype == 2: filetype = 'elf64' else: raise ValueError("ELF file class %d" % filetype) cputype = {2: 'sparc', 3: 'i386', 62: 'x86-64', 192: 'm8'} if e.Ehdr.machine in cputype: cputype = cputype[e.Ehdr.machine] else: cputype = "[CPU%d]" % e.Ehdr.machine if cputype == 'i386': symbols_set_asm_format(symbols, 'att_syntax objdump') output += "%s: file format %s-%s\n" % (filename, filetype, cputype) output += "\n" output += "\n" symbols_by_section = {} for s in symbols.symbols: if hasattr(s, 'section') and hasattr(s, 'lines'): if not s.section in symbols_by_section: symbols_by_section[s.section] = [] symbols_by_section[s.section].append(s) for section in sorted(symbols_by_section): output += "Disassembly of section %s:\n" % section for s in sorted(symbols_by_section[section], key=lambda x: x.address): if s.is_symbol(): output += "\n" output += "%08x <%s>:\n" % (s.address, s) ad_fmt = "%4x" if s.lines[-1].offset >= 0x1000: ad_fmt = "%8x" for l in s.lines: output += ad_fmt % l.offset output += ":\t" b = hexbytes(l.pack()) if hasattr(l, 'miasm'): output += "%-21s\t%s\n" % (" ".join(b[:7]), l) if len(b) > 7: output += ad_fmt % (l.offset + 7) output += ":\t" output += "%s \n" % (" ".join(b[7:])) elif hasattr(l, 'amoco'): lstr = str(l) if lstr in ["nop", "retl"]: lstr += " " output += "%-15s\t%s\n" % (" ".join(b), lstr) else: output += "%s\n" % l return output[:-1]
def display(self): out = "%s\n" % repr(self) if hasattr(self, 'section'): out += " ENDER %s -- NXT %s\n" % (self.ender, self.nxt) for cfg in self.cfg: out += " CFG %s\n" % cfg for label in sorted(self.graph[0]): out += " GRAPH_NXT %s\n" % label for label in sorted(self.graph[1]): out += " GRAPH_PRV %s\n" % label out += self.stack.display() if hasattr(self, 'lines'): for l in self.lines: out += "%s\n" % l.display() return out
def find_symbol(self, **props): found = self.find_symbols(**props) if len(found) == 0: return None elif len(found) == 1: return found[0] else: found = sorted(found, key=lambda x: x.name) return getattr(self, 'choose_symbol', lambda _, f: f[0])(found)
def update_pic(l, pic): # x86 specific, but it should be generalized pic_list = pic[1] if l.flow in ['sub', 'D.sub']: # During a function call, some registers may be overwritten, # but edi, esi, ebx, ebp and esp are kept. pic_list = [ _ for _ in pic_list if 'edi' in _ or 'esi' in _ or 'ebx' in _ or 'ebp' in _ or 'esp' in _ ] else: # Delete overwritten registers from pic_list # and references (to esp or ebp) if these registers are written # Not applied for 'call', because 'call' modifies esp, but it is # restored after returning from the call for w in l.rw[1]: w = str(w) pic_list = [_ for _ in pic_list if not w in str(_)] if l.opname == 'mov': # Add places where the PIC value is stored read = str(l.api_arg_txt(1)) written = str(l.api_arg_txt(0)) if written in pic_list: # Erased pic_list.remove(written) if read in pic_list: # Copied pic_list.append(written) return (pic[0], tuple(sorted(pic_list)))
def test_io(file, suffix, kargs): pool = File().from_filename("non_regression/" + file, **kargs) fd = open("non_regression/" + file + "." + suffix, "r") res = fd.read() fd.close() from plasmasm import constants if "rw" in kargs: # Same output as disass.py -rLWT pool.arch.set_asm_format('att_syntax') pool.arch.long_display = True constants.Constant.out_format = None else: # Same output as disass.py -r pool.arch.set_asm_format('raw') constants.Constant.out_format = 'raw' if "pic" in kargs: from staticasm.pic_tracking import analyze_PIC analyze_PIC(pool) from staticasm.stack_tracking import analyze_stack analyze_stack(pool) if "dead" in kargs: from staticasm.dead_registers import analyze_dead analyze_dead(pool) rep = [_.display() for _ in pool.blocs] rep += [ _.display() for _ in sorted( [s for s in pool.symbols if not s in pool.bloc_set], key=lambda x: (getattr(x, 'section', ''), getattr(x, 'address', 0), getattr(x, 'name', None))) ] rep += [ str(tuple((s, pool.sections.asm_name[s]))) for s in sorted(pool.sections.asm_name) ] rep += [ ','.join( ["%s:%r" % (_, pool.meta[_]) for _ in reversed(sorted(pool.meta))]) ] rep += [''] rep = '\n'.join(rep) if sys.version_info[0] == 2 and sys.version_info[1] <= 3: # long are displayed differently import re rep = re.sub('([0-9]+)L', '\\1', rep) assert rep == res
def __init__(self, dst, src): #if dst is slice=> replace with id make composed src if isinstance(dst, ExprSlice): self.dst = dst.arg rest = [(ExprSlice(dst.arg, r[0], r[1]), r[0], r[1]) for r in slice_rest(dst.arg.size, dst.start, dst.stop)] all_a = sorted([(src, dst.start, dst.stop)] + rest, key=lambda x: x[1]) self.src = ExprCompose(all_a) else: self.dst, self.src = dst, src
def flatten_loop(self, todo=[]): # Outputs the list of all known possible stacks # Non-recursive variant: stack length may be more than 1000 todo = [self] # stacks where nothing is computed seen = [] # stacks where all substacks have been added to todo done = {} # stacks that have been flattened while len(todo): x = todo.pop() # If empty, then we are done if len(x.stacks) == 0: done[x] = [] continue # If not, explore all substacks, # in reversed order, to get the same result as the recursive variant for pos, stk in reversed(sorted(x.stacks, key=lambda _: str(_[0]))): if not stk in seen and not stk in todo: todo.append(stk) seen.append(x) while len(seen): x = seen.pop() # 'x' has substacks, all of them have been done, because # they were added to 'seen' after 'x' done[x] = [] for pos, stk in sorted(x.stacks, key=lambda _: str(_[0])): # If not in done, it is a recursive stack # it is made empty to avoid infinite loops stk = done.get(stk, []) if stk == []: if not isinstance(pos, StackTopVoid): done[x].append([pos]) else: if isinstance(pos, StackTopVoid): pos = [] else: pos = [pos] for s in stk: s = pos + s if not s == [] and not s in done[x]: done[x].append(s) return done[self]
def repr_attr(self, f, _stack={}): v = getattr(self, f) # _stack is added to detect recursion loops # not necessary with CPython, but necessary with Pypy if v.__class__ == tuple: if (self, f) in _stack: return "%s=(...)" % f else: _stack[(self, f)] = 1 if type(v) == dict: v = ','.join([ "%s:%s" % (k, v[k]) for k in sorted(v.keys(), key=lambda a: str(a)) ]) if v.__class__.__name__ == 'long': return "%s=%s" % (f, v) return "%s=%r" % (f, v)
def flatten_recursive(self, marked=None): # Outputs the list of all known possible stacks if marked is None: marked = set() if self in marked: return [] out = [] marked.add(self) for pos, stk in sorted(self.stacks, key=lambda _: str(_[0])): stk = stk.flatten_recursive(marked) if stk == []: if not isinstance(pos, StackTopVoid): out.append([pos]) else: if isinstance(pos, StackTopVoid): pos = [] else: pos = [pos] for s in stk: out.append(pos + s) return out
def display(self): ''' (long) display of internals of the instruction ''' out = str(type(self)) out = out.replace("<class '", "'") if hasattr(self, 'offset'): out += "%s " % self.offset if self.flow != False: out += "[%s]" % self.flow if hasattr(self, 'immutable'): out += "[immutable=%s]" % self.immutable out += self.txt() if self.long_display: if hasattr(self, 'rw'): out += "\n\t R%s W%s" % (sorted([ self.reg_name(r) for r in self.rw[0] ]), sorted([self.reg_name(r) for r in self.rw[1]])) if hasattr(self, 'dead'): out += "\n\t D%s" % (sorted( [self.reg_name(r) for r in self.dead])) if hasattr(self, 'pic'): pic_label, pic_list = self.pic pic_list = ';'.join([str(_) for _ in pic_list]) if pic_label is not None: pic_list = "%s:%s" % (pic_label, pic_list) out += "\n\t PIC:%s" % pic_list if hasattr(self, 'stack'): registers, positions = self.stack if isinstance(registers, bool): stack_status = '%s:' % registers else: stack_status = [] for k in sorted(registers): stack_status.append('%r: %r' % (k, sorted(list(registers[k])))) stack_status = '{' + ', '.join(stack_status) + '}:' stack_status += str(tuple(sorted(positions))) out += "\n\t STACK:%s" % stack_status if hasattr(self, 'dst') and len(self.dst) != 0: out += "\n\t dst=%s" % (self.dst) return out
def guess_asm_cpu(symbols): from plasmasm import arch cpu_opcodes = {} for cpu in arch.CPUs: cpu_opcodes.update(arch.import_cpu_meta(cpu).opcodes) opcodes = set() operands = set() for label in symbols.symbols: for line in getattr(label, 'lines', []): if not isinstance(line, Instruction): continue opcodes.add(line.opname) operands.update(line.operands) if len(operands) == 0: return None delta = [] for cpu in cpu_opcodes: delta.append((len(opcodes.difference(cpu_opcodes[cpu])), cpu)) delta.sort() cpu = delta[0][1] if delta[0][0] == 0 and delta[1][0] != 0: return cpu if delta[0][0] == delta[1][0]: val = delta[0][0] cpu = [x for n, x in delta if n == val] # Filter possible cpu by looking at operands # Necessary to make the difference between I386 and X64 cpu_base = set([_ for _ in cpu if not '-' in _]) cpu_base.update(set([_[:_.find('-')] for _ in cpu if '-' in _])) for op in operands: if op in ('%g0', '%g1', '%o0'): NON_REGRESSION_FOUND cpu_base.intersection_update(set(['SPARC'])) for reg in [ 'rax', 'rbx', 'rcx', 'rdx', 'rbp', 'rsp', 'rsi', 'rdi', 'rip' ] + ['r%d' % _ for _ in range(8, 16)]: if op in (reg, '%' + reg, '(%' + reg + ')'): cpu_base.intersection_update(set(['X64'])) if '%' + reg in op or '[' + reg in op: cpu_base.intersection_update(set(['X64'])) if op in ('eax', '%eax'): cpu_base.intersection_update(set(['X64', 'I386'])) if len(cpu_base) <= 1: break if cpu_base == set(['I386', 'X64']): # Sometimes an assembly file generated for 64-bit architecture # is also a valid 32-bit assembly; but we need to know which # was the target architecture, e.g. obfuscation will be different if '.rodata.str1.8' in symbols.sections.asm_name: # 32-bit uses .rodata.str1.4 NON_REGRESSION_FOUND cpu_base = set(['X64']) else: cpu_base = set(['I386']) # Get back to full CPU description (with AT&T/Intel syntax) def startswith(cpu, cpu_base): # str.startswith(tuple) is invalid for python 2.3 for _ in cpu_base: if cpu.startswith(_): return True return False cpu = [_ for _ in cpu if startswith(_, cpu_base)] if len(cpu) == 1: return cpu[0] if set(cpu) == set(['I386-att', 'I386-intel']): if not 'format' in symbols.get_meta(): return 'I386-att' else: NON_REGRESSION_FOUND return 'I386-intel' log.warning("Guessing cpu: %s are matching", cpu) return cpu[0] diff = opcodes.difference(cpu_opcodes[cpu]) if len(diff) * 2 > len(opcodes): log.warning( "Guessing cpu: best guess is %s but too many opcodes are missing", cpu) return None log.warning("Guessing cpu %s; additional opcodes are %s", cpu, sorted(list(diff))) return cpu
def eval_ExprMem(self, e, eval_cache={}): a_val = expr_simp(self.eval_expr(e.arg, eval_cache)) if isinstance(a_val, ExprTop): #XXX hack test ee = ExprMem(e.arg, e.size) ee.is_term = True return ee a = expr_simp(ExprMem(a_val, size=e.size)) if a in self.pool: return self.pool[a] tmp = None #test if mem lookup is known """ for k in self.pool: if not isinstance(k, ExprMem): continue if a_val == k.arg: tmp = k break """ if a_val in self.pool.pool_mem: tmp = self.pool.pool_mem[a_val][0] """ for k in self.pool: if not isinstance(k, ExprMem): continue if a_val == k.arg: tmp = k break """ if tmp is None: v = self.find_mem_by_addr(a_val) if not v: out = [] ov = self.get_mem_overlapping(a, eval_cache) off_base = 0 ov.sort() ov.reverse() for off, x in ov: off_base = off * 8 if off >= 0: m = min(a.get_size() - off_base, x.get_size()) ee = ExprSlice(self.pool[x], 0, m) ee = expr_simp(ee) out.append((ee, off_base, off_base + ee.get_size())) off_base += ee.get_size() else: m = min(a.get_size() - off * 8, x.get_size()) ee = ExprSlice(self.pool[x], -off * 8, m) ee = expr_simp(ee) out.append((ee, off_base, off_base + ee.get_size())) off_base += ee.get_size() if out: missing_slice = self.rest_slice(out, 0, a.get_size()) for sa, sb in missing_slice: ptr = expr_simp(a_val + ExprInt32(sa / 8)) out.append((ExprMem(ptr, size=sb - sa), sa, sb)) out = sorted(out, key=lambda x: x[1]) #for e, sa, sb in out: # print("%s %s %s"%(e, sa, sb)) ee = ExprSlice(ExprCompose(out), 0, a.get_size()) ee = expr_simp(ee) return ee if self.func_read and isinstance(a.arg, ExprInt): return self.func_read(self, a) else: #XXX hack test a.is_term = True return a #eq lookup if a.size == tmp.size: return self.pool[tmp] #bigger lookup if a.size > tmp.size: rest = a.size ptr = a_val out = [] ptr_index = 0 while rest: v = self.find_mem_by_addr(ptr) if v is None: #raise ValueError("cannot find %s in mem"%str(ptr)) val = ExprMem(ptr, 8) v = val diff_size = 8 elif rest >= v.size: val = self.pool[v] diff_size = v.size else: diff_size = rest val = self.pool[v][0:diff_size] val = (val, ptr_index, ptr_index + diff_size) out.append(val) ptr_index += diff_size rest -= diff_size ptr = expr_simp( self.eval_expr( ExprOp('+', ptr, ExprInt(uint32(v.size / 8))), eval_cache)) e = expr_simp(ExprCompose(out)) return e #part lookup tmp = expr_simp(ExprSlice(self.pool[tmp], 0, a.size)) return tmp
def label_def(label): res = {} if getattr(label, 'type', None) == 'function': res['.type'] = 32 if getattr(label, 'bind', None) == 'globl': res['.scl'] = 2 if getattr(label, 'bind', None) == 'local': res['.scl'] = 3 return ';\t'.join(['%s\t%s' % (_, res[_]) for _ in sorted(res)])
def set_pic_reg(todo): # 'todo' is a list of blocs where PIC register needs to be computed # of the form [ (label, start, (pic_label, pic_list)) ] # 'label' is the bloc label # 'start' is the idx of the first line where to set the PIC attribute # 'pic_label' is None or the label used as offset for PIC computation # 'pic_list' is a tuple of places containing the PIC offset done = {} blocs = () while True: if len(todo) == 0: # When all 'todo' blocs have been analyzed, we can deduce that # all other blocs have no PIC data. This needs to be enforced, # e.g. when a bloc is a target of a jmp with PIC data and a jmp # without PIC data, this target should have no PIC data for _ in blocs: if _ in done: continue if len(_.lines) == 0: continue # Not executable bloc if not hasattr(_.lines[0], 'rw'): continue # Padding or alignment if isinstance(_.lines[0], P2Align): continue if getattr(_, 'type', None) == 'padding': continue # Data bloc that may be reached (the CFG may be incomplete) todo.append((_, 0, (False, ()))) if len(todo) == 0: break label, start, pic = todo.pop(0) if label is None: continue blocs = label.symbols.blocs if not label in blocs: continue if label in done: prev_pic = done[label] # The pic_label of l should be the same if pic[0] is True or prev_pic[0] is True: # we are in the start of the function # we don't erase the pic tracking below call;pop continue if not pic[0] in [False, prev_pic[0]]: raise ValueError( 'PIC label should stay identical in a function. in %s, %s != %s' % (label, pic[0], prev_pic[0])) # The pic_list of l should be included in pic_list # In other words, when a label is reached both through a jump # rather than through the normal flow, the PIC register may have # been overwritten pic = (pic[0], tuple(sorted(set(pic[1]).intersection(set(prev_pic[1]))))) if prev_pic == pic: continue done[label] = pic # TODO: exchange analysis of last lines when there is a delay slot for l in label.lines[start:]: l.pic = pic pic = update_pic(l, pic) if label.flow in ['sub', 'D.sub']: # We could propagate PIC status through function calls, # (if there is not external symbol and if we update esp) # but we don't want to todo.append((label.nxt, 0, pic)) else: # Most jmp stay in the function, and keep the PIC register # However, tail-call optimization replaces a call ret by a jmp todo.extend([(_, 0, pic) for _ in label.cfg]) for label in blocs: # Delete pic field when there is no PIC info for l in label.lines: if not hasattr(l, 'pic'): continue if l.pic[1] == (): del l.pic
def mk_asm_file(symbols, output_filename=None): meta = symbols.get_meta() asm_format, format_file = find_format(meta) symbols_set_asm_format(symbols, asm_format) if output_filename is None: output = '' else: output = TextIOWrapperWithAppend(output_filename) file = [s for s in symbols.symbols if getattr(s, 'bind', None) == 'file'] if len(file) == 1: output += '\t.file\t"%s"\n' % file[0] output += format_file if meta.get('compiler') == 'clang' and 'os_minversion' in meta: osmaj, osmin, type = meta.get('os_minversion') if type == 'vermin': output += "\t.macosx_version_min %d, %d\n" % (osmaj, osmin) elif type == 'bldver': output += "\t.build_version macos, %d, %d" % (osmaj, osmin) output += "\tsdk_version %d, %d\n" % (osmaj, osmin) output += hack_for_gcc492(symbols) output += mk_asm_file_v2(symbols, meta, asm_format) for label in symbols.symbols: if hasattr(symbols, 'cds') and symbols.cds.hide_symbol(label.name): continue if label not in symbols.bloc_set: if hasattr(label, 'visibility'): output += '\t.%s\t%s\n' % (label.visibility, label.name) elif getattr(label, 'bind', None) == 'weak' and not getattr( label, 'type', None) == 'endofsymbol': output += '\t.%s\t%s\n' % (label.bind, label.name) for key in getattr(label, 'data', {}): if key == 'set': if getattr(label, 'bind', None) == 'globl': output += '\t.%s\t%s\n' % (label.bind, label.name) output += '\t.set\t%s,%s\n' % (label.name, label.data['set']) if hasattr(label, 'size') and label.size != getattr( label.data['set'], 'size', None): output += '\t.size\t%s, %s\n' % (label.name, label.size) elif key == 'weakref': output += '\t.weakref\t%s,%s\n' % (label.data['weakref'], label.name) elif key == 'symver': output += '\t.symver\t%s,%s\n' % (label.name, label.data['symver']) elif not key in [ 'LFB', 'cfi_personality', 'cfi_lsda', 'indirect_symbol', 'cfg_warn', 'data_object', 'function' ]: raise ValueError("Unknown data key %r for %r" % (key, label)) if meta.get('compiler') == 'mingw': s_external = [ _ for _ in symbols.symbols if not _ in symbols.bloc_set and hasattr(_, 'type') ] for label in sorted( s_external, key=lambda x: (getattr(x, 'section', ''), getattr(x, 'address', 0))): if not (hasattr(symbols, 'cds') and symbols.cds.hide_symbol(label.name)): output += '\t.def\t%s;\t%s;\t.endef\n' % (label.name, label_def(label)) if 'cfi_sections' in meta: output += '\t.cfi_sections\t%s\n' % meta['cfi_sections'] if 'ident' in meta: output += '\t.ident\t"%s"\n' % meta['ident'] if meta.get('compiler') == 'gcc' and asm_format != 'sparc': output += '\t.section\t.note.GNU-stack,"",@progbits\n' if meta.get('compiler') == 'clang': output += '\n.subsections_via_symbols\n' if output_filename is not None: output.file.close() return output
def canonize_expr_list(l): return sorted(l, key=key_expr)
def object_list(self): if 'object_list' in self._precomputed: return self._precomputed['object_list'] # res is a list of objects, each being a list of blocs # the end of an object is determined with the following heuristics: # - symbol with type, data or bind==globl # - symbol in comm_symbol_section # if not at the end, the object continues within the same section # res_sections is the list of sections of the objects res = [] res_sections = [] for label in self.blocs: section = label.section if is_comm(label): section = comm_symbol_section if label.is_new_object(): prv = None else: for idx in range(len(res)): prv_section = res_sections[-idx - 1] if prv_section == section: prv = res[-idx - 1] break else: prv = None if prv is None: # New object/function res.append([label]) res_sections.append(section) else: prv.append(label) # nxt is the list of objects having a 'nxt', therefore needing to be # merged with the next object nxt = [] for o in res: if o[-1].nxt is not None \ and section_type(o[-1].section) != 'common' \ and getattr(o[-1], 'type', None) != 'padding': nxt.append(o) for o in nxt: log.warning("Object %s has nxt %s", [str(l) for l in o], o[-1].nxt) # reorder objects, based on the original section order, plus some hints # objects in .text.__i686.get_pc_thunk.* should be last # objects in __IMPORT,__pointers should be last, too last_section_eos = -1 for o in self.sections.eos: if not o.__class__ == str and last_section_eos < o: last_section_eos = o res_ordered = {} for o, section in zip(res, res_sections): if section.startswith('__IMPORT,__pointers'): pos = last_section_eos + 2 elif section.endswith('.__i686.get_pc_thunk.cx'): pos = last_section_eos + 2 elif section.endswith('.__i686.get_pc_thunk.bx'): pos = last_section_eos + 3 elif section in self.sections.eos: pos = self.sections.eos[section] else: pos = last_section_eos + 1 if not pos in res_ordered: res_ordered[pos] = [] res_ordered[pos].append(o) res = [] for pos in sorted(res_ordered.keys()): res.extend(res_ordered[pos]) self._precomputed['object_list'] = res return res
def canonize_expr_list_compose(l): return sorted(l, key=key_expr_compose)