xref: /xnu-10063.121.3/tools/lldbmacros/core/collections.py (revision 2c2f96dc2b9a4408a43d3150ae9c105355ca3daa)
1"""
2XNU Collection iterators
3"""
4from .cvalue import gettype
5from .standard import xnu_format
6
7from abc import ABCMeta, abstractmethod, abstractproperty
8
9
10#
11# Note to implementers
12# ~~~~~~~~~~~~~~~~~~~~
13#
14# Iterators must be named iter_* in accordance with typical python API naming
15#
16# The "root" or "head" of the collection must be passed by reference
17# and not by pointer.
18#
19# Returned elements must be references to the element type,
20# and not pointers.
21#
22# Intrusive data types should ask for an "element type" (as an SBType),
23# and a field name or path, and use SBType.xContainerOfTransform
24# to cache the transform to apply for the entire iteration.
25#
26
27
28def iter_linked_list(head_value, next_field_or_path,
29        first_field_or_path = None):
30    """
31    Iterates a NULL-terminated linked list.
32
33        Assuming these C types:
34
35            struct container {
36                struct node *list1_head;
37                struct node *list2_head;
38            }
39
40            struct node {
41                struct node *next;
42            }
43
44        and "v" is an SBValue to a `struct container` type,
45        enumerating list1 is:
46
47            iter_linked_list(v, 'next', 'list1_head')
48
49        if "v" is a `struct node *` directly, then the enumeration
50        becomes:
51
52            iter_linked_list(v, 'next')
53
54
55    @param head_value (SBValue)
56        a reference to the list head.
57
58    @param next_field_or_path (str)
59        The name of (or path to if starting with '.')
60        the field linking to the next element.
61
62    @param first_field_or_path (str or None)
63        The name of (or path to if starting with '.')
64        the field from @c head_value holding the pointer
65        to the first element of the list.
66    """
67
68    if first_field_or_path is None:
69        elt = head_value
70    elif first_field_or_path[0] == '.':
71        elt = head_value.chkGetValueForExpressionPath(first_field_or_path)
72    else:
73        elt = head_value.chkGetChildMemberWithName(first_field_or_path)
74
75    if next_field_or_path[0] == '.':
76        while elt.GetValueAsAddress():
77            elt = elt.Dereference()
78            yield elt
79            elt = elt.chkGetValueForExpressionPath(next_field_or_path)
80    else:
81        while elt.GetValueAsAddress():
82            elt = elt.Dereference()
83            yield elt
84            elt = elt.chkGetChildMemberWithName(next_field_or_path)
85
86
87def iter_queue_entries(head_value, elt_type, field_name_or_path, backwards=False):
88    """
89    Iterate over a queue of entries (<kern/queue.h> method 1)
90
91    @param head_value (SBValue)
92        a reference to the list head (queue_head_t)
93
94    @param elt_type (SBType)
95        The type of the elements on the chain
96
97    @param field_name_or_path (str)
98        The name of (or path to if starting with '.') the field
99        containing the struct mpsc_queue_chain linkage.
100
101    @param backwards (bool)
102         Whether the walk is forward or backwards
103    """
104
105    stop = head_value.GetLoadAddress()
106    elt  = head_value
107    key  = 'prev' if backwards else 'next'
108
109    transform = elt_type.xContainerOfTransform(field_name_or_path)
110
111    while True:
112        elt  = elt.chkGetChildMemberWithName(key)
113        addr = elt.GetValueAsAddress()
114        if addr == 0 or addr == stop:
115            break
116        elt = elt.Dereference()
117        yield transform(elt)
118
119
120def iter_queue(head_value, elt_type, field_name_or_path, backwards=False, unpack=None):
121    """
122    Iterate over queue of elements (<kern/queue.h> method 2)
123
124    @param head_value (SBValue)
125        A reference to the list head (queue_head_t)
126
127    @param elt_type (SBType)
128        The type of the elements on the chain
129
130    @param field_name_or_path (str)
131        The name of (or path to if starting with '.') the field
132        containing the struct mpsc_queue_chain linkage.
133
134    @param backwards (bool)
135         Whether the walk is forward or backwards
136
137    @param unpack (Function or None)
138        A function to unpack the pointer or None.
139    """
140
141    stop = head_value.GetLoadAddress()
142    elt  = head_value
143    key  = '.prev' if backwards else '.next'
144    addr = elt.xGetScalarByPath(key)
145    if field_name_or_path[0] == '.':
146        path = field_name_or_path + key
147    else:
148        path = "." + field_name_or_path + key
149
150    while True:
151        if unpack is not None:
152            addr = unpack(addr)
153        if addr == 0 or addr == stop:
154            break
155
156        elt  = elt.xCreateValueFromAddress('element', addr, elt_type)
157        addr = elt.xGetScalarByPath(path)
158        yield elt
159
160
161def iter_circle_queue(head_value, elt_type, field_name_or_path, backwards=False):
162    """
163    Iterate over a queue of entries (<kern/circle_queue.h>)
164
165    @param head_value (SBValue)
166        a reference to the list head (circle_queue_head_t)
167
168    @param elt_type (SBType)
169        The type of the elements on the chain
170
171    @param field_name_or_path (str)
172        The name of (or path to if starting with '.') the field
173        containing the struct mpsc_queue_chain linkage.
174
175    @param backwards (bool)
176         Whether the walk is forward or backwards
177    """
178
179    elt  = head_value.chkGetChildMemberWithName('head')
180    stop = elt.GetValueAsAddress()
181    key  = 'prev' if backwards else 'next'
182
183    transform = elt_type.xContainerOfTransform(field_name_or_path)
184
185    if stop:
186        if backwards:
187            elt  = elt.chkGetValueForExpressionPath('->prev')
188            stop = elt.GetValueAsAddress()
189
190        while True:
191            elt = elt.Dereference()
192            yield transform(elt)
193
194            elt  = elt.chkGetChildMemberWithName(key)
195            addr = elt.GetValueAsAddress()
196            if addr == 0 or addr == stop:
197                break
198
199
200def iter_mpsc_queue(head_value, elt_type, field_name_or_path):
201    """
202    Iterates a struct mpsc_queue_head
203
204    @param head_value (SBValue)
205        A struct mpsc_queue_head value.
206
207    @param elt_type (SBType)
208        The type of the elements on the chain.
209
210    @param field_name_or_path (str)
211        The name of (or path to if starting with '.') the field
212        containing the struct mpsc_queue_chain linkage.
213
214    @returns (generator)
215        An iterator for the MPSC queue.
216    """
217
218    transform = elt_type.xContainerOfTransform(field_name_or_path)
219
220    return (transform(e) for e in iter_linked_list(
221        head_value, 'mpqc_next', '.mpqh_head.mpqc_next'
222    ))
223
224
225class iter_priority_queue(object):
226    """
227    Iterates any of the priority queues from <kern/priority_queue.h>
228
229    @param head_value (SBValue)
230        A struct priority_queue* value.
231
232    @param elt_type (SBType)
233        The type of the elements on the chain.
234
235    @param field_name_or_path (str)
236        The name of (or path to if starting with '.') the field
237        containing the struct priority_queue_entry* linkage.
238
239    @returns (generator)
240        An iterator for the priority queue.
241    """
242
243    def __init__(self, head_value, elt_type, field_name_or_path):
244        self.transform = elt_type.xContainerOfTransform(field_name_or_path)
245
246        self.gen   = iter_linked_list(head_value, 'next', 'pq_root')
247        self.queue = []
248
249    def __iter__(self):
250        return self
251
252    def __next__(self):
253        queue = self.queue
254        elt   = next(self.gen, None)
255
256        if elt is None:
257            if not len(queue):
258                raise StopIteration
259            elt = queue.pop()
260            self.gen = iter_linked_list(elt, 'next', 'next')
261
262        addr = elt.xGetScalarByName('child')
263        if addr:
264            queue.append(elt.xCreateValueFromAddress(
265                None, addr & 0xffffffffffffffff, elt.GetType()))
266
267        return self.transform(elt)
268
269def iter_SLIST_HEAD(head_value, link_field):
270    """ Specialized version of iter_linked_list() for <sys/queue.h> SLIST_HEAD """
271
272    next_path = ".{}.sle_next".format(link_field)
273    return (e for e in iter_linked_list(head_value, next_path, "slh_first"))
274
275def iter_LIST_HEAD(head_value, link_field):
276    """ Specialized version of iter_linked_list() for <sys/queue.h> LIST_HEAD """
277
278    next_path = ".{}.le_next".format(link_field)
279    return (e for e in iter_linked_list(head_value, next_path, "lh_first"))
280
281def iter_STAILQ_HEAD(head_value, link_field):
282    """ Specialized version of iter_linked_list() for <sys/queue.h> STAILQ_HEAD """
283
284    next_path = ".{}.stqe_next".format(link_field)
285    return (e for e in iter_linked_list(head_value, next_path, "stqh_first"))
286
287def iter_TAILQ_HEAD(head_value, link_field):
288    """ Specialized version of iter_linked_list() for <sys/queue.h> TAILQ_HEAD """
289
290    next_path = ".{}.tqe_next".format(link_field)
291    return (e for e in iter_linked_list(head_value, next_path, "tqh_first"))
292
293def iter_SYS_QUEUE_HEAD(head_value, link_field):
294    """ Specialized version of iter_linked_list() for any <sys/queue> *_HEAD """
295
296    field  = head_value.rawGetType().GetFieldAtIndex(0).GetName()
297    next_path = ".{}.{}e_next".format(link_field, field.partition('_')[0])
298    return (e for e in iter_linked_list(head_value, next_path, field))
299
300
301class RB_HEAD(object):
302    """
303    Class providing utilities to manipulate a collection made with RB_HEAD()
304    """
305
306    def __init__(self, rbh_root_value, link_field, cmp):
307        """
308        Create an rb-tree wrapper
309
310        @param rbh_root_value (SBValue)
311            A value to an RB_HEAD() field
312
313        @param link_field (str)
314            The name of the RB_ENTRY() field in the elements
315
316        @param cmp (function)
317            The comparison function between a linked element,
318            and a key
319        """
320
321        self.root_sbv = rbh_root_value.chkGetChildMemberWithName('rbh_root')
322        self.field    = link_field
323        self.cmp      = cmp
324        self.etype    = self.root_sbv.GetType().GetPointeeType()
325
326    def _parent(self, elt):
327        RB_COLOR_MASK = 0x1
328
329        path   = "." + self.field + ".rbe_parent"
330        parent = elt.chkGetValueForExpressionPath(path)
331        paddr  = parent.GetValueAsAddress()
332
333        if paddr == 0:
334            return None, 0
335
336        if paddr & RB_COLOR_MASK == 0:
337            return parent.Dereference(), paddr
338
339        paddr &= ~RB_COLOR_MASK
340        return parent.xCreateValueFromAddress(None, paddr, self.etype), paddr
341
342    def _sibling(self, elt, rbe_left, rbe_right):
343
344        field = self.field
345        lpath = "." + field + rbe_left
346        rpath = "." + field + rbe_right
347
348        e_r = elt.chkGetValueForExpressionPath(rpath)
349        if e_r.GetValueAsAddress():
350            #
351            # IF (RB_RIGHT(elm, field)) {
352            #     elm = RB_RIGHT(elm, field);
353            #     while (RB_LEFT(elm, field))
354            #         elm = RB_LEFT(elm, field);
355            #
356
357            path = "->" + field + rbe_left
358            res = e_r
359            e_l = res.chkGetValueForExpressionPath(path)
360            while e_l.GetValueAsAddress():
361                res = e_l
362                e_l = res.chkGetValueForExpressionPath(path)
363
364            return res.Dereference()
365
366        eaddr = elt.GetLoadAddress()
367        e_p, paddr = self._parent(elt)
368
369        if paddr:
370            #
371            # IF (RB_GETPARENT(elm) &&
372            #     (elm == RB_LEFT(RB_GETPARENT(elm), field)))
373            #         elm = RB_GETPARENT(elm)
374            #
375
376            if e_p.xGetScalarByPath(lpath) == eaddr:
377                return e_p
378
379            #
380            # WHILE (RB_GETPARENT(elm) &&
381            #     (elm == RB_RIGHT(RB_GETPARENT(elm), field)))
382            #         elm = RB_GETPARENT(elm);
383            # elm = RB_GETPARENT(elm);
384            #
385
386            while paddr:
387                if e_p.xGetScalarByPath(rpath) != eaddr:
388                    return e_p
389
390                eaddr = paddr
391                e_p, paddr = self._parent(e_p)
392
393        return None
394
395    def _find(self, key):
396        elt = self.root_sbv
397        f   = self.field
398        le  = None
399        ge  = None
400
401        while elt.GetValueAsAddress():
402            elt = elt.Dereference()
403            rc  = self.cmp(elt, key)
404            if rc == 0:
405                return elt, elt, elt
406
407            if rc < 0:
408                ge  = elt
409                elt = elt.chkGetValueForExpressionPath("->" + f + ".rbe_left")
410            else:
411                le  = elt
412                elt = elt.chkGetValueForExpressionPath("->" + f + ".rbe_right")
413
414        return le, None, ge
415
416    def _minmax(self, direction):
417        """ Returns the first element in the tree """
418
419        elt  = self.root_sbv
420        res  = None
421        path = "->" + self.field + direction
422
423        while elt.GetValueAsAddress():
424            res = elt
425            elt = elt.chkGetValueForExpressionPath(path)
426
427        return res.Dereference() if res is not None else None
428
429    def find_lt(self, key):
430        """ Find the element smaller than the specified key """
431
432        elt, _, _ = self._find(key)
433        return self.prev(elt) if elt is not None else None
434
435    def find_le(self, key):
436        """ Find the element smaller or equal to the specified key """
437
438        elt, _, _ = self._find(key)
439        return elt
440
441    def find(self, key):
442        """ Find the element with the specified key """
443
444        _, elt, _ = self._find(key)
445        return elt
446
447    def find_ge(self, key):
448        """ Find the element greater or equal to the specified key """
449
450        _, _, elt = self._find(key)
451        return elt
452
453    def find_gt(self, key):
454        """ Find the element greater than the specified key """
455
456        _, _, elt = self._find(key)
457        return self.next(elt.Dereference()) if elt is not None else None
458
459    def first(self):
460        """ Returns the first element in the tree """
461
462        return self._minmax(".rbe_left")
463
464    def last(self):
465        """ Returns the last element in the tree """
466
467        return self._minmax(".rbe_right")
468
469    def next(self, elt):
470        """ Returns the next element in rbtree order or None """
471
472        return self._sibling(elt, ".rbe_left", ".rbe_right")
473
474    def prev(self, elt):
475        """ Returns the next element in rbtree order or None """
476
477        return self._sibling(elt, ".rbe_right", ".rbe_left")
478
479    def iter(self, min_key=None, max_key=None):
480        """
481        Iterates all elements in this red-black tree
482        with min_key <= key < max_key
483        """
484
485        e = self.first() if min_key is None else self.find_ge(min_key)
486
487        if max_key is None:
488            while e is not None:
489                yield e
490                e = self.next(e)
491        else:
492            cmp = self.cmp
493            while e is not None and cmp(e, max_key) >= 0:
494                yield e
495                e = self.next(e)
496
497    def __iter__(self):
498        return self.iter()
499
500
501def iter_RB_HEAD(rbh_root_value, link_field):
502    """ Conveniency wrapper for RB_HEAD iteration """
503
504    return RB_HEAD(rbh_root_value, link_field, None).iter()
505
506
507def iter_smr_queue(head_value, elt_type, field_name_or_path):
508    """
509    Iterate over an SMR queue of entries (<kern/smr.h>)
510
511    @param head_value (SBValue)
512        a reference to the list head (struct smrq_*_head)
513
514    @param elt_type (SBType)
515        The type of the elements on the chain
516
517    @param field_name_or_path (str)
518        The name of (or path to if starting with '.') the field
519        containing the struct mpsc_queue_chain linkage.
520    """
521
522    transform = elt_type.xContainerOfTransform(field_name_or_path)
523
524    return (transform(e) for e in iter_linked_list(
525        head_value, '.next.__smr_ptr', '.first.__smr_ptr'
526    ))
527
528class _Hash(object, metaclass=ABCMeta):
529    @abstractproperty
530    def buckets(self):
531        """
532        Returns the number of buckets in the hash table
533        """
534        pass
535
536    @abstractproperty
537    def count(self):
538        """
539        Returns the number of elements in the hash table
540        """
541        pass
542
543    @abstractproperty
544    def rehashing(self):
545        """
546        Returns whether the hash is currently rehashing
547        """
548        pass
549
550    @abstractmethod
551    def iter(self, detailed=False):
552        """
553        @param detailed (bool)
554            whether to enumerate just elements, or show bucket info too
555            when bucket info is requested, enumeration returns a tuple of:
556            (0, bucket, index_in_bucket, element)
557        """
558        pass
559
560    def __iter__(self):
561        return self.iter()
562
563    def describe(self):
564        fmt = (
565            "Hash table info\n"
566            " address              : {1:#x}\n"
567            " element count        : {0.count}\n"
568            " bucket count         : {0.buckets}\n"
569            " rehashing            : {0.rehashing}"
570        )
571        print(xnu_format(fmt, self, self.hash_value.GetLoadAddress()))
572
573        if self.rehashing:
574            print()
575            return
576
577        b_len = {}
578        for _, b_idx, e_idx, _ in self.iter(detailed=True):
579            b_len[b_idx] = e_idx + 1
580
581        stats = {i: 0 for i in range(max(b_len.values()) + 1) }
582        for v in b_len.values():
583            stats[v] += 1
584        stats[0] = self.buckets - len(b_len)
585
586        fmt = (
587            " histogram            :\n"
588            "  {:>4}  {:>6}  {:>6}  {:>6}  {:>5}"
589        )
590        print(xnu_format(fmt, "size", "count", "(cum)", "%", "(cum)"))
591
592        fmt = "  {:>4,d}  {:>6,d}  {:>6,d}  {:>6.1%}  {:>5.0%}"
593        tot = 0
594        for sz, n in stats.items():
595            tot += n
596            print(xnu_format(fmt, sz, n, tot, n / self.buckets, tot / self.buckets))
597
598
599class SMRHash(_Hash):
600    """
601    Class providing utilities to manipulate SMR hash tables
602    """
603
604    def __init__(self, hash_value, traits_value):
605        """
606        Create an smr hash table iterator
607
608        @param hash_value (SBValue)
609            a reference to a struct smr_hash instance.
610
611        @param traits_value (SBValue)
612            a reference to the traits for this hash table
613        """
614        super(SMRHash, self).__init__()
615        self.hash_value = hash_value
616        self.traits_value = traits_value
617
618    @property
619    def buckets(self):
620        hash_arr = self.hash_value.xGetScalarByName('smrh_array')
621        return 1 << (64 - ((hash_arr >> 48) & 0xffff))
622
623    @property
624    def count(self):
625        return self.hash_value.xGetScalarByName('smrh_count')
626
627    @property
628    def rehashing(self):
629        return self.hash_value.xGetScalarByName('smrh_resizing')
630
631    def iter(self, detailed=False):
632        obj_null = self.traits_value.chkGetChildMemberWithName('smrht_obj_type')
633        obj_ty   = obj_null.GetType().GetArrayElementType().GetPointeeType()
634        lnk_offs = self.traits_value.xGetScalarByPath('.smrht.link_offset')
635        hash_arr = self.hash_value.xGetScalarByName('smrh_array')
636        hash_sz  = 1 << (64 - ((hash_arr >> 48) & 0xffff))
637        hash_arr = obj_null.xCreateValueFromAddress(None,
638            hash_arr | 0xffff000000000000, gettype('struct smrq_slist_head'));
639
640        if detailed:
641            return (
642                (0, head_idx, e_idx, e.xCreateValueFromAddress(None, e.GetLoadAddress() - lnk_offs, obj_ty))
643                for head_idx, head in enumerate(hash_arr.xIterSiblings(0, hash_sz))
644                for e_idx, e in enumerate(iter_linked_list(head, '.next.__smr_ptr', '.first.__smr_ptr'))
645            )
646
647        return (
648            e.xCreateValueFromAddress(None, e.GetLoadAddress() - lnk_offs, obj_ty)
649            for head in hash_arr.xIterSiblings(0, hash_sz)
650            for e in iter_linked_list(head, '.next.__smr_ptr', '.first.__smr_ptr')
651        )
652
653
654class SMRScalableHash(_Hash):
655
656    def __init__(self, hash_value, traits_value):
657        """
658        Create an smr hash table iterator
659
660        @param hash_value (SBValue)
661            a reference to a struct smr_shash instance.
662
663        @param traits_value (SBValue)
664            a reference to the traits for this hash table
665        """
666        super(SMRScalableHash, self).__init__()
667        self.hash_value = hash_value
668        self.traits_value = traits_value
669
670    @property
671    def buckets(self):
672        shift = self.hash_value.xGetScalarByPath('.smrsh_state.curshift')
673        return (0xffffffff >> shift) + 1;
674
675    @property
676    def count(self):
677        sbv      = self.hash_value.chkGetChildMemberWithName('smrsh_count')
678        addr     = sbv.GetValueAsAddress() | 0x8000000000000000
679        target   = sbv.GetTarget()
680        ncpus    = target.chkFindFirstGlobalVariable('zpercpu_early_count').xGetValueAsInteger()
681        pg_shift = target.chkFindFirstGlobalVariable('page_shift').xGetValueAsInteger()
682
683        return sum(
684            target.xReadInt64(addr + (cpu << pg_shift))
685            for cpu in range(ncpus)
686        )
687
688    @property
689    def rehashing(self):
690        curidx = self.hash_value.xGetScalarByPath('.smrsh_state.curidx');
691        newidx = self.hash_value.xGetScalarByPath('.smrsh_state.newidx');
692        return curidx != newidx
693
694    def iter(self, detailed=False):
695        """
696        @param detailed (bool)
697            whether to enumerate just elements, or show bucket info too
698            when bucket info is requested, enumeration returns a tuple of:
699            (table_index, bucket, index_in_bucket, element)
700        """
701
702        hash_value   = self.hash_value
703        traits_value = self.traits_value
704
705        obj_null = traits_value.chkGetChildMemberWithName('smrht_obj_type')
706        obj_ty   = obj_null.GetType().GetArrayElementType().GetPointeeType()
707        lnk_offs = traits_value.xGetScalarByPath('.smrht.link_offset')
708        hashes   = []
709
710        curidx   = hash_value.xGetScalarByPath('.smrsh_state.curidx');
711        newidx   = hash_value.xGetScalarByPath('.smrsh_state.newidx');
712        arrays   = hash_value.chkGetChildMemberWithName('smrsh_array')
713
714        array    = arrays.chkGetChildAtIndex(curidx)
715        shift    = hash_value.xGetScalarByPath('.smrsh_state.curshift')
716        hashes.append((curidx, array.Dereference(), shift))
717        if newidx != curidx:
718            array    = arrays.chkGetChildAtIndex(newidx)
719            shift    = hash_value.xGetScalarByPath('.smrsh_state.newshift')
720            hashes.append((newidx, array.Dereference(), shift))
721
722        seen = set()
723
724        def _iter_smr_shash_bucket(head):
725            addr  = head.xGetScalarByName('lck_ptr_bits')
726            tg    = head.GetTarget()
727
728            while addr & 1 == 0:
729                addr &= 0xfffffffffffffffe
730                e = head.xCreateValueFromAddress(None, addr - lnk_offs, obj_ty)
731                if addr not in seen:
732                    seen.add(addr)
733                    yield e
734                addr = tg.xReadULong(addr);
735
736        if detailed:
737            return (
738                (hash_idx, head_idx, e_idx, e)
739                for hash_idx, hash_arr, hash_shift in hashes
740                for head_idx, head in enumerate(hash_arr.xIterSiblings(
741                    0, 1 + (0xffffffff >> hash_shift)))
742                for e_idx, e in enumerate(_iter_smr_shash_bucket(head))
743            )
744
745        return (
746            e
747            for hash_idx, hash_arr, hash_shift in hashes
748            for head in hash_arr.xIterSiblings(
749                0, 1 + (0xffffffff >> hash_shift))
750            for e in _iter_smr_shash_bucket(head)
751        )
752
753
754__all__ = [
755    iter_linked_list.__name__,
756    iter_queue_entries.__name__,
757    iter_queue.__name__,
758    iter_circle_queue.__name__,
759
760    iter_mpsc_queue.__name__,
761
762    iter_priority_queue.__name__,
763
764    iter_SLIST_HEAD.__name__,
765    iter_LIST_HEAD.__name__,
766    iter_STAILQ_HEAD.__name__,
767    iter_TAILQ_HEAD.__name__,
768    iter_SYS_QUEUE_HEAD.__name__,
769    iter_RB_HEAD.__name__,
770    RB_HEAD.__name__,
771
772    iter_smr_queue.__name__,
773    SMRHash.__name__,
774    SMRScalableHash.__name__,
775]
776