From bf69025825fd2b8e7aac01f27d5c974bd30af542 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 18 Sep 2009 11:49:22 -0700 Subject: [PATCH] Mach-O file reading R=r DELTA=784 (784 added, 0 deleted, 0 changed) OCL=34715 CL=34788 --- src/pkg/debug/macho/Makefile | 12 + src/pkg/debug/macho/file.go | 374 ++++++++++++++++++ src/pkg/debug/macho/file_test.go | 159 ++++++++ src/pkg/debug/macho/macho.go | 230 +++++++++++ .../debug/macho/testdata/gcc-386-darwin-exec | Bin 0 -> 12588 bytes .../macho/testdata/gcc-amd64-darwin-exec | Bin 0 -> 8512 bytes .../testdata/gcc-amd64-darwin-exec-debug | Bin 0 -> 4540 bytes 7 files changed, 775 insertions(+) create mode 100644 src/pkg/debug/macho/Makefile create mode 100644 src/pkg/debug/macho/file.go create mode 100644 src/pkg/debug/macho/file_test.go create mode 100644 src/pkg/debug/macho/macho.go create mode 100755 src/pkg/debug/macho/testdata/gcc-386-darwin-exec create mode 100755 src/pkg/debug/macho/testdata/gcc-amd64-darwin-exec create mode 100644 src/pkg/debug/macho/testdata/gcc-amd64-darwin-exec-debug diff --git a/src/pkg/debug/macho/Makefile b/src/pkg/debug/macho/Makefile new file mode 100644 index 00000000000..1a88c73778a --- /dev/null +++ b/src/pkg/debug/macho/Makefile @@ -0,0 +1,12 @@ +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include $(GOROOT)/src/Make.$(GOARCH) + +TARG=debug/macho +GOFILES=\ + macho.go\ + file.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/src/pkg/debug/macho/file.go b/src/pkg/debug/macho/file.go new file mode 100644 index 00000000000..fee02fb27aa --- /dev/null +++ b/src/pkg/debug/macho/file.go @@ -0,0 +1,374 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package macho implements access to Mach-O object files, as defined by +// http://developer.apple.com/mac/library/documentation/DeveloperTools/Conceptual/MachORuntime/Reference/reference.html. +package macho + +// High level access to low level data structures. + +import ( + "bytes"; + "debug/binary"; + "debug/dwarf"; + "fmt"; + "io"; + "os"; +) + +// A File represents an open Mach-O file. +type File struct { + FileHeader; + ByteOrder binary.ByteOrder; + Loads []Load; + Sections []*Section; + + closer io.Closer; +} + +// A Load represents any Mach-O load command. +type Load interface { + Raw() []byte +} + +// A LoadBytes is the uninterpreted bytes of a Mach-O load command. +type LoadBytes []byte + +func (b LoadBytes) Raw() []byte { + return b +} + +// A SegmentHeader is the header for a Mach-O 32-bit or 64-bit load segment command. +type SegmentHeader struct { + Cmd LoadCmd; + Len uint32; + Name string; + Addr uint64; + Memsz uint64; + Offset uint64; + Filesz uint64; + Maxprot uint32; + Prot uint32; + Nsect uint32; + Flag uint32; +} + +// A Segment represents a Mach-O 32-bit or 64-bit load segment command. +type Segment struct { + LoadBytes; + SegmentHeader; + + // Embed ReaderAt for ReadAt method. + // Do not embed SectionReader directly + // to avoid having Read and Seek. + // If a client wants Read and Seek it must use + // Open() to avoid fighting over the seek offset + // with other clients. + io.ReaderAt; + sr *io.SectionReader; +} + +// Data reads and returns the contents of the segment. +func (s *Segment) Data() ([]byte, os.Error) { + dat := make([]byte, s.sr.Size()); + n, err := s.sr.ReadAt(dat, 0); + return dat[0:n], err; +} + +// Open returns a new ReadSeeker reading the segment. +func (s *Segment) Open() io.ReadSeeker { + return io.NewSectionReader(s.sr, 0, 1<<63 - 1); +} + +type SectionHeader struct { + Name string; + Seg string; + Addr uint64; + Size uint64; + Offset uint32; + Align uint32; + Reloff uint32; + Nreloc uint32; + Flags uint32; +} + +type Section struct { + SectionHeader; + + // Embed ReaderAt for ReadAt method. + // Do not embed SectionReader directly + // to avoid having Read and Seek. + // If a client wants Read and Seek it must use + // Open() to avoid fighting over the seek offset + // with other clients. + io.ReaderAt; + sr *io.SectionReader; +} + +// Data reads and returns the contents of the Mach-O section. +func (s *Section) Data() ([]byte, os.Error) { + dat := make([]byte, s.sr.Size()); + n, err := s.sr.ReadAt(dat, 0); + return dat[0:n], err; +} + +// Open returns a new ReadSeeker reading the Mach-O section. +func (s *Section) Open() io.ReadSeeker { + return io.NewSectionReader(s.sr, 0, 1<<63 - 1); +} + + +/* + * Mach-O reader + */ + +type FormatError struct { + off int64; + msg string; + val interface{}; +} + +func (e *FormatError) String() string { + msg := e.msg; + if e.val != nil { + msg += fmt.Sprintf(" '%v' ", e.val); + } + msg += fmt.Sprintf("in record at byte %#x", e.off); + return msg; +} + +// Open opens the named file using os.Open and prepares it for use as a Mach-O binary. +func Open(name string) (*File, os.Error) { + f, err := os.Open(name, os.O_RDONLY, 0); + if err != nil { + return nil, err; + } + ff, err := NewFile(f); + if err != nil { + f.Close(); + return nil, err; + } + ff.closer = f; + return ff, nil; +} + +// Close closes the File. +// If the File was created using NewFile directly instead of Open, +// Close has no effect. +func (f *File) Close() os.Error { + var err os.Error; + if f.closer != nil { + err = f.closer.Close(); + f.closer = nil; + } + return err; +} + +// NewFile creates a new File for acecssing a Mach-O binary in an underlying reader. +// The Mach-O binary is expected to start at position 0 in the ReaderAt. +func NewFile(r io.ReaderAt) (*File, os.Error) { + f := new(File); + sr := io.NewSectionReader(r, 0, 1<<63 - 1); + + // Read and decode Mach magic to determine byte order, size. + // Magic32 and Magic64 differ only in the bottom bit. + var ident [4]uint8; + if _, err := r.ReadAt(&ident, 0); err != nil { + return nil, err; + } + be := binary.BigEndian.Uint32(&ident); + le := binary.LittleEndian.Uint32(&ident); + switch Magic32&^1 { + case be&^1: + f.ByteOrder = binary.BigEndian; + f.Magic = be; + case le&^1: + f.ByteOrder = binary.LittleEndian; + f.Magic = le; + } + + // Read entire file header. + if err := binary.Read(sr, f.ByteOrder, &f.FileHeader); err != nil { + return nil, err; + } + + // Then load commands. + offset := int64(fileHeaderSize32); + if f.Magic == Magic64 { + offset = fileHeaderSize64; + } + dat := make([]byte, f.Cmdsz); + if _, err := r.ReadAt(dat, offset); err != nil { + return nil, err; + } + f.Loads = make([]Load, f.Ncmd); + bo := f.ByteOrder; + for i := range f.Loads { + // Each load command begins with uint32 command and length. + if len(dat) < 8 { + return nil, &FormatError{offset, "command block too small", nil}; + } + cmd, siz := LoadCmd(bo.Uint32(dat[0:4])), bo.Uint32(dat[4:8]); + if siz < 8 || siz > uint32(len(dat)) { + return nil, &FormatError{offset, "invalid command block size", nil}; + } + var cmddat []byte; + cmddat, dat = dat[0:siz], dat[siz:len(dat)]; + offset += int64(siz); + var s *Segment; + switch cmd { + default: + f.Loads[i] = LoadBytes(cmddat); + + case LoadCmdSegment: + var seg32 Segment32; + b := bytes.NewBuffer(cmddat); + if err := binary.Read(b, bo, &seg32); err != nil { + return nil, err; + } + s = new(Segment); + s.LoadBytes = cmddat; + s.Cmd = cmd; + s.Len = siz; + s.Name = cstring(&seg32.Name); + s.Addr = uint64(seg32.Addr); + s.Memsz = uint64(seg32.Memsz); + s.Offset = uint64(seg32.Offset); + s.Filesz = uint64(seg32.Filesz); + s.Maxprot = seg32.Maxprot; + s.Prot = seg32.Prot; + s.Nsect = seg32.Nsect; + s.Flag = seg32.Flag; + f.Loads[i] = s; + for i := 0; i < int(s.Nsect); i++ { + var sh32 Section32; + if err := binary.Read(b, bo, &sh32); err != nil { + return nil, err; + } + sh := new(Section); + sh.Name = cstring(&sh32.Name); + sh.Seg = cstring(&sh32.Seg); + sh.Addr = uint64(sh32.Addr); + sh.Size = uint64(sh32.Size); + sh.Offset = sh32.Offset; + sh.Align = sh32.Align; + sh.Reloff = sh32.Reloff; + sh.Nreloc = sh32.Nreloc; + sh.Flags = sh32.Flags; + f.pushSection(sh, r); + } + + case LoadCmdSegment64: + var seg64 Segment64; + b := bytes.NewBuffer(cmddat); + if err := binary.Read(b, bo, &seg64); err != nil { + return nil, err; + } + s = new(Segment); + s.LoadBytes = cmddat; + s.Cmd = cmd; + s.Len = siz; + s.Name = cstring(&seg64.Name); + s.Addr = seg64.Addr; + s.Memsz = seg64.Memsz; + s.Offset = seg64.Offset; + s.Filesz = seg64.Filesz; + s.Maxprot = seg64.Maxprot; + s.Prot = seg64.Prot; + s.Nsect = seg64.Nsect; + s.Flag = seg64.Flag; + f.Loads[i] = s; + for i := 0; i < int(s.Nsect); i++ { + var sh64 Section64; + if err := binary.Read(b, bo, &sh64); err != nil { + return nil, err; + } + sh := new(Section); + sh.Name = cstring(&sh64.Name); + sh.Seg = cstring(&sh64.Seg); + sh.Addr = sh64.Addr; + sh.Size = sh64.Size; + sh.Offset = sh64.Offset; + sh.Align = sh64.Align; + sh.Reloff = sh64.Reloff; + sh.Nreloc = sh64.Nreloc; + sh.Flags = sh64.Flags; + f.pushSection(sh, r); + } + } + if s != nil { + s.sr = io.NewSectionReader(r, int64(s.Offset), int64(s.Filesz)); + s.ReaderAt = s.sr; + } + } + return f, nil; +} + +func (f *File) pushSection(sh *Section, r io.ReaderAt) { + n := len(f.Sections); + if n >= cap(f.Sections) { + m := (n+1)*2; + new := make([]*Section, n, m); + for i, sh := range f.Sections { + new[i] = sh; + } + f.Sections = new; + } + f.Sections = f.Sections[0:n+1]; + f.Sections[n] = sh; + sh.sr = io.NewSectionReader(r, int64(sh.Offset), int64(sh.Size)); + sh.ReaderAt = sh.sr; +} + +func cstring(b []byte) string { + var i int; + for i=0; i= len(tt.segments) { + break; + } + sh := tt.segments[i]; + s, ok := l.(*Segment); + if sh == nil { + if ok { + t.Errorf("open %s, section %d: skipping %#v\n", tt.file, i, &s.SegmentHeader); + } + continue; + } + if !ok { + t.Errorf("open %s, section %d: not *Segment\n", tt.file, i); + continue; + } + have := &s.SegmentHeader; + want := sh; + if !reflect.DeepEqual(have, want) { + t.Errorf("open %s, segment %d:\n\thave %#v\n\twant %#v\n", tt.file, i, have, want); + } + } + tn := len(tt.segments); + fn := len(f.Loads); + if tn != fn { + t.Errorf("open %s: len(Loads) = %d, want %d", tt.file, fn, tn); + } + + for i, sh := range f.Sections { + if i >= len(tt.sections) { + break; + } + have := &sh.SectionHeader; + want := tt.sections[i]; + if !reflect.DeepEqual(have, want) { + t.Errorf("open %s, section %d:\n\thave %#v\n\twant %#v\n", tt.file, i, have, want); + } + } + tn = len(tt.sections); + fn = len(f.Sections); + if tn != fn { + t.Errorf("open %s: len(Sections) = %d, want %d", tt.file, fn, tn); + } + + } +} diff --git a/src/pkg/debug/macho/macho.go b/src/pkg/debug/macho/macho.go new file mode 100644 index 00000000000..78f2d7fc3b9 --- /dev/null +++ b/src/pkg/debug/macho/macho.go @@ -0,0 +1,230 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Mach-O header data structures +// http://developer.apple.com/mac/library/documentation/DeveloperTools/Conceptual/MachORuntime/Reference/reference.html + +package macho + +import "strconv" + +// A FileHeader represents a Mach-O file header. +type FileHeader struct { + Magic uint32; + Cpu Cpu; + SubCpu uint32; + Type Type; + Ncmd uint32; + Cmdsz uint32; + Flags uint32; +} +const ( + fileHeaderSize32 = 7*4; + fileHeaderSize64 = 8*4; +) + +const ( + Magic32 uint32 = 0xfeedface; + Magic64 uint32 = 0xfeedfacf; +) + +// A Type is a Mach-O file type, either an object or an executable. +type Type uint32 +const ( + TypeObj Type = 1; + TypeExec Type = 2; +) + +// A Cpu is a Mach-O cpu type. +type Cpu uint32 +const ( + Cpu386 Cpu = 7; + CpuAmd64 Cpu = Cpu386 + 1<<24; +) + +var cpuStrings = []intName { + intName{ uint32(Cpu386), "Cpu386" }, + intName{ uint32(CpuAmd64), "CpuAmd64" }, +} +func (i Cpu) String() string { + return stringName(uint32(i), cpuStrings, false) +} +func (i Cpu) GoString() string { + return stringName(uint32(i), cpuStrings, true) +} + +// A LoadCmd is a Mach-O load command. +type LoadCmd uint32; +const ( + LoadCmdSegment LoadCmd = 1; + LoadCmdSegment64 LoadCmd = 25; + LoadCmdThread LoadCmd = 4; + LoadCmdUnixThread LoadCmd = 5; // thread+stack +) +var cmdStrings = []intName { + intName{ uint32(LoadCmdSegment), "LoadCmdSegment" }, + intName{ uint32(LoadCmdSegment64), "LoadCmdSegment64" }, + intName{ uint32(LoadCmdThread), "LoadCmdThread" }, + intName{ uint32(LoadCmdUnixThread), "LoadCmdUnixThread" }, +} +func (i LoadCmd) String() string { + return stringName(uint32(i), cmdStrings, false) +} +func (i LoadCmd) GoString() string { + return stringName(uint32(i), cmdStrings, true) +} + +// A Segment64 is a 64-bit Mach-O segment load command. +type Segment64 struct { + Cmd LoadCmd; + Len uint32; + Name [16]byte; + Addr uint64; + Memsz uint64; + Offset uint64; + Filesz uint64; + Maxprot uint32; + Prot uint32; + Nsect uint32; + Flag uint32; +} + +// A Segment32 is a 32-bit Mach-O segment load command. +type Segment32 struct { + Cmd LoadCmd; + Len uint32; + Name [16]byte; + Addr uint32; + Memsz uint32; + Offset uint32; + Filesz uint32; + Maxprot uint32; + Prot uint32; + Nsect uint32; + Flag uint32; +} + +// A Section32 is a 32-bit Mach-O section header. +type Section32 struct { + Name [16]byte; + Seg [16]byte; + Addr uint32; + Size uint32; + Offset uint32; + Align uint32; + Reloff uint32; + Nreloc uint32; + Flags uint32; + Reserve1 uint32; + Reserve2 uint32; +} + +// A Section32 is a 64-bit Mach-O section header. +type Section64 struct { + Name [16]byte; + Seg [16]byte; + Addr uint64; + Size uint64; + Offset uint32; + Align uint32; + Reloff uint32; + Nreloc uint32; + Flags uint32; + Reserve1 uint32; + Reserve2 uint32; + Reserve3 uint32; +} + +// A Thread is a Mach-O thread state command. +type Thread struct { + Cmd LoadCmd; + Len uint32; + Type uint32; + Data []uint32; +} + +// Regs386 is the Mach-O 386 register structure. +type Regs386 struct { + AX uint32; + BX uint32; + CX uint32; + DX uint32; + DI uint32; + SI uint32; + BP uint32; + SP uint32; + SS uint32; + FLAGS uint32; + IP uint32; + CS uint32; + DS uint32; + ES uint32; + FS uint32; + GS uint32; +} + +// RegsAMD64 is the Mach-O AMD64 register structure. +type RegsAMD64 struct { + AX uint64; + BX uint64; + CX uint64; + DX uint64; + DI uint64; + SI uint64; + BP uint64; + SP uint64; + R8 uint64; + R9 uint64; + R10 uint64; + R11 uint64; + R12 uint64; + R13 uint64; + R14 uint64; + R15 uint64; + IP uint64; + FLAGS uint64; + CS uint64; + FS uint64; + GS uint64; +} + +type intName struct { + i uint32; + s string; +} + +func stringName(i uint32, names []intName, goSyntax bool) string { + for _, n := range names { + if n.i == i { + if goSyntax { + return "macho." + n.s + } + return n.s + } + } + return strconv.Uitoa64(uint64(i)) +} + +func flagName(i uint32, names []intName, goSyntax bool) string { + s := ""; + for _, n := range names { + if n.i & i == n.i { + if len(s) > 0 { + s += "+"; + } + if goSyntax { + s += "macho."; + } + s += n.s; + i -= n.i; + } + } + if len(s) == 0 { + return "0x" + strconv.Uitob64(uint64(i), 16) + } + if i != 0 { + s += "+0x" + strconv.Uitob64(uint64(i), 16) + } + return s +} diff --git a/src/pkg/debug/macho/testdata/gcc-386-darwin-exec b/src/pkg/debug/macho/testdata/gcc-386-darwin-exec new file mode 100755 index 0000000000000000000000000000000000000000..03ba1bafac0a6cba92b72239e51b2a9618668032 GIT binary patch literal 12588 zcmeI2OKTHR6vywhqXw;Q7Fmc&okdnsQVW7ETr|?4#Xdq?u`jtKlQBuni^)uFL9h@C zhEQ}N;ztnNxD;_wX*VkP846kmQl*Q6AZY!c%uEuSDqRVMbI6%{<~;5_^UG`o&c`2L ze;pG-v_spV9ne+WEMrxmm!VQd!`C9yk+D(Sc;fer)|Kj3p;AsnCMTNWRO8iQrE`x! z>0A;iEn~rQZ0G*Z`qVig9wLzHYSh%SRk2iCagkClT4p+zbRBzp*v1|w5!=Qd&EoYy zzDEB8;fZilmIm8<2Dg8Y8)ZqEh*>eR9FelMK0qPVHlOFF*B0!tm`OA|d-n)vLYzf7 z@Q*#H|NBrW2Zy7hV~u~{0-UH5nzIsrQpzWha?Z{cq!o*24ClVJ{x}6RA&%1OicbiY zO!_ZrFXx>Z8XUP185s0V??-^nMh`tmYCad<8jn+CAWrY#GptXro`6Q7B*dDOYN?OE zF+@l_M*V*APQ-{GozZI-&E8Bp-s>Vi10VG;eQB!i>3ID0&oA9q&cFD!PR~U>(6jlW z2Nil>_Y*q%B9zW@dZe9|MnC8&sq3;B>IwO)h_7AC+1 zm;e)C0!)AjFaajO1pYsPIZ@s$ecmo@`75{8@@!D74ExGMLAA7|y#3Use1OYWX)mp* z%d7Tx+;miX&}4ZaD5_KV=GduH%agk=bu5*oB(A#~j<2$Gt9Ey@yg6Rl3e=o4E2YzL zmepWYL-i}zqO=~EdAF7_GMRjjb}w%>e`QkK75c#OZ#?or>0_ru6T$~op@Ni!2`~XB zzyz286JP>NfC(@GCcp%k024TL1a|6j=vwE7nE(@D0!)AjFaajO1egF5U;<2l2{3^J z5}^6xZj2v4$E-5VKa>3ib_jMShOPaY5dE;bVACwXb=RhufT(NJT)-^sb{ZJKui*M= zW?+$Q?D-yhK5%WCi(ZDU2&FduTxqgtuA64QH9Q#S)Y>G8s0Ly1UM!NEXa|G8fAlu(K&?EEu|N8TEW4H=j21 YIe}f$IZ?D?rX`S+#wLgJDVnj z(nx4wDS_bGyP>BXJmg}Vw1?7D5j}X879vs!A|7&Z`uokkm&{Cq9&>pgy#4;X_x*VD zdo#;?^Zm8+@5@dhtPUX-4+$ZTV9dNB#1GJvcQ9-WDaSLPWxvW^`jXM8SW$Ohk2nvp zP|C^d)MRu?ke`aigz~XBf$ao?E2Q+Cd5>0G;&{h;a50#wsCoLlu3brp#a$bQl#PW- zzE+kEubDsl|NY%icwH)qepr$U@30C24?uFpDc2nrvH1R06ke}dPd~E1FnEw(Xn1a^ zI<4Yyyj6vFRL#>5OB`EDXGUIka}}q(Dvr0V@Ob7F`nWfC1^36bT?YowbtpL3;ml+v zG=uYtoU1FPcH$E&efr?2HrxsxEG1zrPcn1)Drry#w+La zty1?~oNIJ{Qwk3%5c*;6c^`-Uz2|ClWbBLV@Q8jU(lOrPQ$7jE6oNR)4C2HL?xl45 z+1-F%M2yezI0l7lIGFpUvZ3~swX_h4eP+55!<682FxYIkgXL0wFhudYh;ttoPOZD$ z*|`U&e}39p+kF2DpWSr~VxGY`Xwm-o^TAR7Z`2S7t9 zk6B7QFJaXXjpj`(G(4v=@M$D5$TmNQ0mFb{z%XDKFbo(53QWx9&Yj*h`y9d+EGrzM!@B0XEtCW9vhx=b4Go<>R-veLpkd z_x*}U&q?9;{Q~1ML6)`&)@yNd(eL0eY!#*dlTyM9pCq1-=)0@8wF*# z)}Q{S=C-SvV_XcezP%)@`IMFF>P)WS6}2BR)<`#zLwTEvC<(s5cBg(klwg^2RM)GV zdZnZ`r2oATNu{}#NWHq=Q97wK>vqoTI`@{UCz)r&Fkl!k3>XFs1BL;^fMLKeU>GnA z7zPXjhJk}&fW55~DA>nUoz6ZU{i5;*;P>ETfv)#IQhrL+`5!A^*ZrTv@1O(WcYv!H z%-7!s*vIa{#H_++pPs(%%QfIz!fLnNU@ydc_AlSU&^j&Hglsq6^QCGLopRlLZ?}Nu z^=7po%eC6q&AO0dQyF)Wab6o6;!qzO$335^7(8-p4F62z?(LIDXH6{xyF2o+jH#neumA8_o*c2K%2 zLV|5?0IP~4u;31C;SelAA`9S+J>xizqvQY{X=Xg{=bPs@ww0G}!=Has2w@SSqeX;J z0{W*U46H4~d>;Cx-0(;8`McDgmmfdeEZs6Mfb2U)h>)EJdppdSO&~@nKQ7}d*b7lS z-+~ZF8#r7$$t#|L;6`fqN(inK=qR`(#6Ik*rcP|9udYq<-bV0({%OWrU_#&lh_p4m zr&%Bi-|uk*Z!Eqb-1V>9j@2<*K7TxapCfodM3_y8)+S9$>1v);IKD3tJUDB=We_^> z7jQw(b=^JJ`?Z}~Km+GF>s%?9vk7S^jxzEmWD0L&;`;_f{w44Z^@hKMa~S7IJid|1 z44$s=N2TXj6TH0$UNHCcIS#IA-Uc9mxOQt#9*&}rLV08F#aB0(2vH8r8J%f^`_$Ba zizupUX>X?Us-F4O_l&E3o~0Nq&7O#_DtYm(GKVUQkNbyb&f||<_qa;|{S z;yl#7!V1b{?t%y^lQO!O6?3;UcQTi=+e_49gZ@i!4Z2tTq+RGMR%M=x+5%)n=>MQ$ zga`R-Dt*kVpA}{w_D@iKF8^Lg(Awzq!ebcOeweCo35M&?xnr=b7ZS-`P>J{jwG?rb z9e##3K`S8oyB*`-zZPNi&H* literal 0 HcmV?d00001