From 314ae669a371873bc1615081d35e3f70836a7f3b Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Fri, 5 Mar 2021 05:17:00 +0100 Subject: [PATCH] dwarf/frame,proc: use eh_frame section (#2344) The eh_frame section is similar to debug_frame but uses a slightly different format. Gcc and clang by default only emit eh_frame. --- pkg/dwarf/frame/entries.go | 61 ++++++++++- pkg/dwarf/frame/entries_test.go | 2 +- pkg/dwarf/frame/parser.go | 189 ++++++++++++++++++++++++++++---- pkg/dwarf/frame/parser_test.go | 3 +- pkg/dwarf/util/util.go | 12 +- pkg/proc/bininfo.go | 67 ++++++++--- service/test/variables_test.go | 4 - 7 files changed, 291 insertions(+), 47 deletions(-) diff --git a/pkg/dwarf/frame/entries.go b/pkg/dwarf/frame/entries.go index df31b231..cdfd204b 100644 --- a/pkg/dwarf/frame/entries.go +++ b/pkg/dwarf/frame/entries.go @@ -18,6 +18,9 @@ type CommonInformationEntry struct { ReturnAddressRegister uint64 InitialInstructions []byte staticBase uint64 + + // eh_frame pointer encoding + ptrEncAddr ptrEnc } // FrameDescriptionEntry represents a Frame Descriptor Entry in the @@ -80,8 +83,64 @@ func (fdes FrameDescriptionEntries) FDEForPC(pc uint64) (*FrameDescriptionEntry, // Append appends otherFDEs to fdes and returns the result. func (fdes FrameDescriptionEntries) Append(otherFDEs FrameDescriptionEntries) FrameDescriptionEntries { r := append(fdes, otherFDEs...) - sort.Slice(r, func(i, j int) bool { + sort.SliceStable(r, func(i, j int) bool { return r[i].Begin() < r[j].Begin() }) + // remove duplicates + uniqFDEs := fdes[:0] + for _, fde := range fdes { + if len(uniqFDEs) > 0 { + last := uniqFDEs[len(uniqFDEs)-1] + if last.Begin() == fde.Begin() && last.End() == fde.End() { + continue + } + } + uniqFDEs = append(uniqFDEs, fde) + } return r } + +// ptrEnc represents a pointer encoding value, used during eh_frame decoding +// to determine how pointers were encoded. +// Least significant 4 (0xf) bytes encode the size as well as its +// signed-ness, most significant 4 bytes (0xf0) are flags describing how +// the value should be interpreted (absolute, relative...) +// See https://www.airs.com/blog/archives/460. +type ptrEnc uint8 + +const ( + ptrEncAbs ptrEnc = 0x00 // pointer-sized unsigned integer + ptrEncOmit ptrEnc = 0xff // omitted + ptrEncUleb ptrEnc = 0x01 // ULEB128 + ptrEncUdata2 ptrEnc = 0x02 // 2 bytes + ptrEncUdata4 ptrEnc = 0x03 // 4 bytes + ptrEncUdata8 ptrEnc = 0x04 // 8 bytes + ptrEncSigned ptrEnc = 0x08 // pointer-sized signed integer + ptrEncSleb ptrEnc = 0x09 // SLEB128 + ptrEncSdata2 ptrEnc = 0x0a // 2 bytes, signed + ptrEncSdata4 ptrEnc = 0x0b // 4 bytes, signed + ptrEncSdata8 ptrEnc = 0x0c // 8 bytes, signed + + ptrEncPCRel ptrEnc = 0x10 // value is relative to the memory address where it appears + ptrEncTextRel ptrEnc = 0x20 // value is relative to the address of the text section + ptrEncDataRel ptrEnc = 0x30 // value is relative to the address of the data section + ptrEncFuncRel ptrEnc = 0x40 // value is relative to the start of the function + ptrEncAligned ptrEnc = 0x50 // value should be aligned + ptrEncIndirect ptrEnc = 0x80 // value is an address where the real value of the pointer is stored +) + +// Supported returns true if this pointer encoding is supported. +func (ptrEnc ptrEnc) Supported() bool { + if ptrEnc != ptrEncOmit { + szenc := ptrEnc & 0x0f + if ((szenc > ptrEncUdata8) && (szenc < ptrEncSigned)) || (szenc > ptrEncSdata8) { + // These values aren't defined at the moment + return false + } + if ptrEnc&0xf0 != ptrEncPCRel { + // Currently only the PC relative flag is supported + return false + } + } + return true +} diff --git a/pkg/dwarf/frame/entries_test.go b/pkg/dwarf/frame/entries_test.go index 30506380..593d4516 100644 --- a/pkg/dwarf/frame/entries_test.go +++ b/pkg/dwarf/frame/entries_test.go @@ -67,7 +67,7 @@ func BenchmarkFDEForPC(b *testing.B) { if err != nil { b.Fatal(err) } - fdes := Parse(data, binary.BigEndian, 0, ptrSizeByRuntimeArch()) + fdes, _ := Parse(data, binary.BigEndian, 0, ptrSizeByRuntimeArch(), 0) for i := 0; i < b.N; i++ { // bench worst case, exhaustive search diff --git a/pkg/dwarf/frame/parser.go b/pkg/dwarf/frame/parser.go index 8b0c165b..ba5ddd59 100644 --- a/pkg/dwarf/frame/parser.go +++ b/pkg/dwarf/frame/parser.go @@ -6,6 +6,8 @@ package frame import ( "bytes" "encoding/binary" + "fmt" + "io" "github.com/go-delve/delve/pkg/dwarf/util" ) @@ -15,82 +17,135 @@ type parsefunc func(*parseContext) parsefunc type parseContext struct { staticBase uint64 - buf *bytes.Buffer - entries FrameDescriptionEntries - common *CommonInformationEntry - frame *FrameDescriptionEntry - length uint32 - ptrSize int + buf *bytes.Buffer + totalLen int + entries FrameDescriptionEntries + ciemap map[int]*CommonInformationEntry + common *CommonInformationEntry + frame *FrameDescriptionEntry + length uint32 + ptrSize int + ehFrameAddr uint64 + err error } // Parse takes in data (a byte slice) and returns FrameDescriptionEntries, // which is a slice of FrameDescriptionEntry. Each FrameDescriptionEntry // has a pointer to CommonInformationEntry. -func Parse(data []byte, order binary.ByteOrder, staticBase uint64, ptrSize int) FrameDescriptionEntries { +// If ehFrameAddr is not zero the .eh_frame format will be used, a minor variant of DWARF described at https://www.airs.com/blog/archives/460. +// The value of ehFrameAddr will be used as the address at which eh_frame will be mapped into memory +func Parse(data []byte, order binary.ByteOrder, staticBase uint64, ptrSize int, ehFrameAddr uint64) (FrameDescriptionEntries, error) { var ( buf = bytes.NewBuffer(data) - pctx = &parseContext{buf: buf, entries: newFrameIndex(), staticBase: staticBase, ptrSize: ptrSize} + pctx = &parseContext{buf: buf, totalLen: len(data), entries: newFrameIndex(), staticBase: staticBase, ptrSize: ptrSize, ehFrameAddr: ehFrameAddr, ciemap: map[int]*CommonInformationEntry{}} ) for fn := parselength; buf.Len() != 0; { fn = fn(pctx) + if pctx.err != nil { + return nil, pctx.err + } } for i := range pctx.entries { pctx.entries[i].order = order } - return pctx.entries + return pctx.entries, nil } -func cieEntry(data []byte) bool { - return bytes.Equal(data, []byte{0xff, 0xff, 0xff, 0xff}) +func (ctx *parseContext) parsingEHFrame() bool { + return ctx.ehFrameAddr > 0 +} + +func (ctx *parseContext) cieEntry(cieid uint32) bool { + if ctx.parsingEHFrame() { + return cieid == 0x00 + } + return cieid == 0xffffffff +} + +func (ctx *parseContext) offset() int { + return ctx.totalLen - ctx.buf.Len() } func parselength(ctx *parseContext) parsefunc { - binary.Read(ctx.buf, binary.LittleEndian, &ctx.length) + start := ctx.offset() + binary.Read(ctx.buf, binary.LittleEndian, &ctx.length) //TODO(aarzilli): this does not support 64bit DWARF if ctx.length == 0 { // ZERO terminator return parselength } - var data = ctx.buf.Next(4) + var cieid uint32 + binary.Read(ctx.buf, binary.LittleEndian, &cieid) ctx.length -= 4 // take off the length of the CIE id / CIE pointer. - if cieEntry(data) { + if ctx.cieEntry(cieid) { ctx.common = &CommonInformationEntry{Length: ctx.length, staticBase: ctx.staticBase} + ctx.ciemap[start] = ctx.common return parseCIE } - ctx.frame = &FrameDescriptionEntry{Length: ctx.length, CIE: ctx.common} + if ctx.ehFrameAddr > 0 { + cieid = uint32(start - int(cieid) + 4) + } + + common := ctx.ciemap[int(cieid)] + + if common == nil { + ctx.err = fmt.Errorf("unknown CIE_id %#x at %#x", cieid, start) + } + + ctx.frame = &FrameDescriptionEntry{Length: ctx.length, CIE: common} return parseFDE } func parseFDE(ctx *parseContext) parsefunc { - var num uint64 + startOff := ctx.offset() r := ctx.buf.Next(int(ctx.length)) reader := bytes.NewReader(r) - num, _ = util.ReadUintRaw(reader, binary.LittleEndian, ctx.ptrSize) + num := ctx.readEncodedPtr(addrSum(ctx.ehFrameAddr+uint64(startOff), reader), reader, ctx.frame.CIE.ptrEncAddr) ctx.frame.begin = num + ctx.staticBase - num, _ = util.ReadUintRaw(reader, binary.LittleEndian, ctx.ptrSize) - ctx.frame.size = num + + // For the size field in .eh_frame only the size encoding portion of the + // address pointer encoding is considered. + // See decode_frame_entry_1 in gdb/dwarf2-frame.c. + // For .debug_frame ptrEncAddr is always ptrEncAbs and never has flags. + sizePtrEnc := ctx.frame.CIE.ptrEncAddr & 0x0f + ctx.frame.size = ctx.readEncodedPtr(0, reader, sizePtrEnc) // Insert into the tree after setting address range begin // otherwise compares won't work. ctx.entries = append(ctx.entries, ctx.frame) + if ctx.parsingEHFrame() && len(ctx.frame.CIE.Augmentation) > 0 { + // If we are parsing a .eh_frame and we saw an agumentation string then we + // need to read the augmentation data, which are encoded as a ULEB128 + // size followed by 'size' bytes. + n, _ := util.DecodeULEB128(reader) + reader.Seek(int64(n), io.SeekCurrent) + } + // The rest of this entry consists of the instructions // so we can just grab all of the data from the buffer // cursor to length. - ctx.frame.Instructions = r[2*ctx.ptrSize:] + + off, _ := reader.Seek(0, io.SeekCurrent) + ctx.frame.Instructions = r[off:] ctx.length = 0 return parselength } +func addrSum(base uint64, buf *bytes.Reader) uint64 { + n, _ := buf.Seek(0, io.SeekCurrent) + return base + uint64(n) +} + func parseCIE(ctx *parseContext) parsefunc { data := ctx.buf.Next(int(ctx.length)) buf := bytes.NewBuffer(data) @@ -100,6 +155,15 @@ func parseCIE(ctx *parseContext) parsefunc { // parse augmentation ctx.common.Augmentation, _ = util.ParseString(buf) + if ctx.parsingEHFrame() { + if ctx.common.Augmentation == "eh" { + ctx.err = fmt.Errorf("unsupported 'eh' augmentation at %#x", ctx.offset()) + } + if len(ctx.common.Augmentation) > 0 && ctx.common.Augmentation[0] != 'z' { + ctx.err = fmt.Errorf("unsupported augmentation at %#x (does not start with 'z')", ctx.offset()) + } + } + // parse code alignment factor ctx.common.CodeAlignmentFactor, _ = util.DecodeULEB128(buf) @@ -107,7 +171,48 @@ func parseCIE(ctx *parseContext) parsefunc { ctx.common.DataAlignmentFactor, _ = util.DecodeSLEB128(buf) // parse return address register - ctx.common.ReturnAddressRegister, _ = util.DecodeULEB128(buf) + if ctx.parsingEHFrame() && ctx.common.Version == 1 { + b, _ := buf.ReadByte() + ctx.common.ReturnAddressRegister = uint64(b) + } else { + ctx.common.ReturnAddressRegister, _ = util.DecodeULEB128(buf) + } + + ctx.common.ptrEncAddr = ptrEncAbs + + if ctx.parsingEHFrame() && len(ctx.common.Augmentation) > 0 { + _, _ = util.DecodeULEB128(buf) // augmentation data length + for i := 1; i < len(ctx.common.Augmentation); i++ { + switch ctx.common.Augmentation[i] { + case 'L': + _, _ = buf.ReadByte() // LSDA pointer encoding, we don't support this. + case 'R': + // Pointer encoding, describes how begin and size fields of FDEs are encoded. + b, _ := buf.ReadByte() + ctx.common.ptrEncAddr = ptrEnc(b) + if !ctx.common.ptrEncAddr.Supported() { + ctx.err = fmt.Errorf("pointer encoding not supported %#x at %#x", ctx.common.ptrEncAddr, ctx.offset()) + return nil + } + case 'S': + // Signal handler invocation frame, we don't support this but there is no associated data to read. + case 'P': + // Personality function encoded as a pointer encoding byte followed by + // the pointer to the personality function encoded as specified by the + // pointer encoding. + // We don't support this but have to read it anyway. + e, _ := buf.ReadByte() + if !ptrEnc(e).Supported() { + ctx.err = fmt.Errorf("pointer encoding not supported %#x at %#x", e, ctx.offset()) + return nil + } + ctx.readEncodedPtr(0, buf, ptrEnc(e)) + default: + ctx.err = fmt.Errorf("unsupported augmentation character %c at %#x", ctx.common.Augmentation[i], ctx.offset()) + return nil + } + } + } // parse initial instructions // The rest of this entry consists of the instructions @@ -119,6 +224,48 @@ func parseCIE(ctx *parseContext) parsefunc { return parselength } +// readEncodedPtr reads a pointer from buf encoded as specified by ptrEnc. +// This function is used to read pointers from a .eh_frame section, when +// used to parse a .debug_frame section ptrEnc will always be ptrEncAbs. +// The parameter addr is the address that the current byte of 'buf' will be +// mapped to when the executable file containing the eh_frame section being +// parse is loaded in memory. +func (ctx *parseContext) readEncodedPtr(addr uint64, buf util.ByteReaderWithLen, ptrEnc ptrEnc) uint64 { + if ptrEnc == ptrEncOmit { + return 0 + } + + var ptr uint64 + + switch ptrEnc & 0xf { + case ptrEncAbs, ptrEncSigned: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, ctx.ptrSize) + case ptrEncUleb: + ptr, _ = util.DecodeULEB128(buf) + case ptrEncUdata2: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, 2) + case ptrEncSdata2: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, 2) + ptr = uint64(int16(ptr)) + case ptrEncUdata4: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, 4) + case ptrEncSdata4: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, 4) + ptr = uint64(int32(ptr)) + case ptrEncUdata8, ptrEncSdata8: + ptr, _ = util.ReadUintRaw(buf, binary.LittleEndian, 8) + case ptrEncSleb: + n, _ := util.DecodeSLEB128(buf) + ptr = uint64(n) + } + + if ptrEnc&0xf0 == ptrEncPCRel { + ptr += addr + } + + return ptr +} + // DwarfEndian determines the endianness of the DWARF by using the version number field in the debug_info section // Trick borrowed from "debug/dwarf".New() func DwarfEndian(infoSec []byte) binary.ByteOrder { diff --git a/pkg/dwarf/frame/parser_test.go b/pkg/dwarf/frame/parser_test.go index ce6c9e50..752ff3b5 100644 --- a/pkg/dwarf/frame/parser_test.go +++ b/pkg/dwarf/frame/parser_test.go @@ -14,6 +14,7 @@ func TestParseCIE(t *testing.T) { common: &CommonInformationEntry{Length: 12}, length: 12, } + ctx.totalLen = ctx.buf.Len() _ = parseCIE(ctx) common := ctx.common @@ -53,6 +54,6 @@ func BenchmarkParse(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - Parse(data, binary.BigEndian, 0, ptrSizeByRuntimeArch()) + Parse(data, binary.BigEndian, 0, ptrSizeByRuntimeArch(), 0) } } diff --git a/pkg/dwarf/util/util.go b/pkg/dwarf/util/util.go index 8cb06e40..0fcf64f7 100644 --- a/pkg/dwarf/util/util.go +++ b/pkg/dwarf/util/util.go @@ -8,12 +8,20 @@ import ( "io" ) +// ByteReaderWithLen is a io.ByteReader with a Len method. This interface is +// satisified by both bytes.Buffer and bytes.Reader. +type ByteReaderWithLen interface { + io.ByteReader + io.Reader + Len() int +} + // The Little Endian Base 128 format is defined in the DWARF v4 standard, // section 7.6, page 161 and following. // DecodeULEB128 decodes an unsigned Little Endian Base 128 // represented number. -func DecodeULEB128(buf *bytes.Buffer) (uint64, uint32) { +func DecodeULEB128(buf ByteReaderWithLen) (uint64, uint32) { var ( result uint64 shift uint64 @@ -46,7 +54,7 @@ func DecodeULEB128(buf *bytes.Buffer) (uint64, uint32) { // DecodeSLEB128 decodes a signed Little Endian Base 128 // represented number. -func DecodeSLEB128(buf *bytes.Buffer) (int64, uint32) { +func DecodeSLEB128(buf ByteReaderWithLen) (int64, uint32) { var ( b byte err error diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index d31cc4eb..22e5237f 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -809,7 +809,7 @@ func (bi *BinaryInfo) LoadImageFromData(dwdata *dwarf.Data, debugFrameBytes, deb image.dwarfTreeCache, _ = simplelru.NewLRU(dwarfTreeCacheSize, nil) if debugFrameBytes != nil { - bi.frameEntries = frame.Parse(debugFrameBytes, frame.DwarfEndian(debugFrameBytes), 0, bi.Arch.PtrSize()) + bi.frameEntries, _ = frame.Parse(debugFrameBytes, frame.DwarfEndian(debugFrameBytes), 0, bi.Arch.PtrSize(), 0) } image.loclist2 = loclist.NewDwarf2Reader(debugLocBytes, bi.Arch.PtrSize()) @@ -1009,6 +1009,38 @@ func (bi *BinaryInfo) funcToImage(fn *Function) *Image { return fn.cu.image } +// parseDebugFrameGeneral parses a debug_frame and a eh_frame section. +// At least one of the two must be present and parsed correctly, if +// debug_frame is present it must be parsable correctly. +func (bi *BinaryInfo) parseDebugFrameGeneral(image *Image, debugFrameBytes []byte, debugFrameName string, debugFrameErr error, ehFrameBytes []byte, ehFrameAddr uint64, ehFrameName string, byteOrder binary.ByteOrder) { + if debugFrameBytes == nil && ehFrameBytes == nil { + image.setLoadError("could not get %s section: %v", debugFrameName, debugFrameErr) + return + } + + if debugFrameBytes != nil { + fe, err := frame.Parse(debugFrameBytes, byteOrder, image.StaticBase, bi.Arch.PtrSize(), 0) + if err != nil { + image.setLoadError("could not parse %s section: %v", debugFrameName, err) + return + } + bi.frameEntries = bi.frameEntries.Append(fe) + } + + if ehFrameBytes != nil && ehFrameAddr > 0 { + fe, err := frame.Parse(ehFrameBytes, byteOrder, image.StaticBase, bi.Arch.PtrSize(), ehFrameAddr) + if err != nil { + if debugFrameBytes == nil { + image.setLoadError("could not parse %s section: %v", ehFrameName, err) + return + } + bi.logger.Warnf("could not parse %s section: %v", ehFrameName, err) + return + } + bi.frameEntries = bi.frameEntries.Append(fe) + } +} + // ELF /////////////////////////////////////////////////////////////// // ErrNoBuildIDNote is used in openSeparateDebugInfo to signal there's no @@ -1207,13 +1239,16 @@ func (bi *BinaryInfo) loadSymbolName(image *Image, file *elf.File, wg *sync.Wait func (bi *BinaryInfo) parseDebugFrameElf(image *Image, exe *elf.File, debugInfoBytes []byte, wg *sync.WaitGroup) { defer wg.Done() - debugFrameData, err := godwarf.GetDebugSectionElf(exe, "frame") - if err != nil { - image.setLoadError("could not get .debug_frame section: %v", err) - return + debugFrameData, debugFrameErr := godwarf.GetDebugSectionElf(exe, "frame") + ehFrameSection := exe.Section(".eh_frame") + var ehFrameData []byte + var ehFrameAddr uint64 + if ehFrameSection != nil { + ehFrameAddr = ehFrameSection.Addr + ehFrameData, _ = ehFrameSection.Data() } - bi.frameEntries = bi.frameEntries.Append(frame.Parse(debugFrameData, frame.DwarfEndian(debugInfoBytes), image.StaticBase, bi.Arch.PtrSize())) + bi.parseDebugFrameGeneral(image, debugFrameData, ".debug_frame", debugFrameErr, ehFrameData, ehFrameAddr, ".eh_frame", frame.DwarfEndian(debugInfoBytes)) } func (bi *BinaryInfo) setGStructOffsetElf(image *Image, exe *elf.File, wg *sync.WaitGroup) { @@ -1363,12 +1398,7 @@ func (bi *BinaryInfo) parseDebugFramePE(image *Image, exe *pe.File, debugInfoByt defer wg.Done() debugFrameBytes, err := godwarf.GetDebugSectionPE(exe, "frame") - if err != nil { - image.setLoadError("could not get .debug_frame section: %v", err) - return - } - - bi.frameEntries = bi.frameEntries.Append(frame.Parse(debugFrameBytes, frame.DwarfEndian(debugInfoBytes), image.StaticBase, bi.Arch.PtrSize())) + bi.parseDebugFrameGeneral(image, debugFrameBytes, ".debug_frame", err, nil, 0, "", frame.DwarfEndian(debugInfoBytes)) } // Borrowed from https://golang.org/src/cmd/internal/objfile/pe.go @@ -1453,13 +1483,16 @@ func (bi *BinaryInfo) setGStructOffsetMacho() { func (bi *BinaryInfo) parseDebugFrameMacho(image *Image, exe *macho.File, debugInfoBytes []byte, wg *sync.WaitGroup) { defer wg.Done() - debugFrameBytes, err := godwarf.GetDebugSectionMacho(exe, "frame") - if err != nil { - image.setLoadError("could not get __debug_frame section: %v", err) - return + debugFrameBytes, debugFrameErr := godwarf.GetDebugSectionMacho(exe, "frame") + ehFrameSection := exe.Section("__eh_frame") + var ehFrameBytes []byte + var ehFrameAddr uint64 + if ehFrameSection != nil { + ehFrameAddr = ehFrameSection.Addr + ehFrameBytes, _ = ehFrameSection.Data() } - bi.frameEntries = bi.frameEntries.Append(frame.Parse(debugFrameBytes, frame.DwarfEndian(debugInfoBytes), image.StaticBase, bi.Arch.PtrSize())) + bi.parseDebugFrameGeneral(image, debugFrameBytes, "__debug_frame", debugFrameErr, ehFrameBytes, ehFrameAddr, "__eh_frame", frame.DwarfEndian(debugInfoBytes)) } // Do not call this function directly it isn't able to deal correctly with package paths diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 77bdcd94..64ec5dc9 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1550,10 +1550,6 @@ func TestPluginVariables(t *testing.T) { func TestCgoEval(t *testing.T) { protest.MustHaveCgo(t) - if runtime.GOARCH == "arm64" { - t.Skip("cgo evaluation broken on arm64") - } - testcases := []varTest{ {"s", true, `"a string"`, `"a string"`, "*char", nil}, {"longstring", true, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, "*const char", nil}, -- GitLab