{
	"id": "9be45b96-f687-46c9-aaee-45e5fdd161ae",
	"created_at": "2026-04-06T01:30:04.990117Z",
	"updated_at": "2026-04-10T03:20:36.697437Z",
	"deleted_at": null,
	"sha1_hash": "ec337ae7b43eab03d4de8059fe206af1544dff67",
	"title": "Ready, Set, Go — Golang Internals and Symbol Recovery",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3104649,
	"plain_text": "Ready, Set, Go — Golang Internals and Symbol Recovery\r\nBy Mandiant\r\nPublished: 2022-02-28 · Archived: 2026-04-06 01:22:07 UTC\r\nWritten by: Stephen Eckels\r\nGolang (Go) is a compiled language introduced by Google in 2009. The language, runtime, and tooling has\r\nevolved significantly since then. In recent years, Go features such as easy-to-use cross-compilation, self-contained\r\nexecutables, and excellent tooling have provided malware authors with a powerful new language to design cross-platform malware. Unfortunately for reverse engineers, the tooling to separate malware author code from Go\r\nruntime code has fallen behind.\r\nToday, Mandiant is releasing a tool named GoReSym to parse Go symbol information and other embedded\r\nmetadata. This blog post will discuss the internals of relevant structures and their evolution with each language\r\nversion. It will also highlight challenges faced when analyzing packed, obfuscated, and stripped binaries.\r\nDesign Decisions\r\nGo is a bit different from other languages in that it generates binaries that are fully self-contained. A system that\r\nexecutes a compiled Go binary doesn’t require a runtime or additional dependencies to be installed. This contrasts\r\nwith languages such as Java or .NET that require a user to install a runtime before binaries will execute correctly.\r\nWith Go's approach, the compiler embeds runtime code for various language features (e.g., garbage collection,\r\nstack traces, type reflection) into each compiled program. This is a major reason why Go binaries are larger than\r\nan equivalent program written in a language such as C. In addition to the runtime code, the compiler also embeds\r\nmetadata about the source code and its binary layout to support language features, the runtime, and debug tooling.\r\nSome of this embedded information has been thoroughly documented, namely the pclntab, moduledata, and\r\nbuildinfo structures. Each of these structures has seen major changes as Go has evolved. This evolution, combined\r\nwith common obfuscator or packing tricks, can make type recovery trickier than expected. To effectively handle\r\never-changing runtime structures, GoReSym is based on the Go runtime source code to transparently handle all\r\nruntime versions. This makes supporting new Go versions trivial. We can also be more confident in edge cases\r\nsince GoReSym uses the same parsers as the runtime.\r\nMatching Recovered Symbols to Language Features\r\nGo binaries without debug symbols, also referred to as stripped binaries, provide a unique challenge to reverse\r\nengineers. Without symbols, analyzing a binary can be extremely complex and time consuming. With symbols\r\nrestored, a reverse engineer can begin to map disassembled code back to its original source. Figure 1 illustrates the\r\nimportance of recovering symbols using the disassembly of two samples: one without symbols and another with\r\nsymbols recovered using GoReSym.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 1 of 11\n\nFigure 1: Stripped Go binary vs. symbols recovered by GoReSym\r\nBefore we examine how GoReSymextracts this information, we'll use recovered symbols to illustrate core Go\r\nconcepts such as channels, Go routines, deferred routines, and function returns. The examples in the sections that\r\nfollow depict a Go binary that has been annotated using function names recovered by GoReSym. Table 1\r\nsummarizes how these concepts map directly to the runtime functions described in each section.\r\nConcept Keyword Runtime Function Names\r\nGoroutines go runtime.newproc\r\nChannels\r\n\u003c- [recv channel]\r\n[send channel] \u003c-\r\nruntime.makechan runtime.chanrecv runtime.sendchan\r\nDeferred routines defer\r\nruntime.deferproc\r\nruntime.deferprocStack\r\nruntime.deferreturn\r\nTable 1: Go keyword-to-runtime function mappings\r\nGo Routines and Channels\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 2 of 11\n\nFigure 2: Runtime functions implementing Go routines\r\nThe go keyword starts a new thread of execution that cooperatively interlaces execution with the rest of the\r\napplication. This is not an operating system thread; instead, multiple Go routines take turns executing on one\r\nthread. Communication across Go routines is done via \"channels\" in a message passing fashion. When a channel is\r\nallocated, an interface type is passed to the runtime routine runtime.makechan, defining the type of data flowing\r\nacross the channel. Data can be sent and received across a channel using the \u003c- operator. Based on the direction,\r\nthe routine runtime.chansend or runtime.chanrecv will be present in the disassembly. Channel logic is often\r\nadjacent to Go routine code, which begins execution by passing a function pointer to the runtime routine\r\nruntime.newproc.\r\nDeferring Cleanup\r\nIf you’re familiar with C++ destructors or finally blocks in C#, Go's defer keyword is similar. It allows Go\r\nprograms to queue a routine for execution on function exit. This is commonly used to close handles or free\r\nresources. The runtime maintains a stack of functions to execute on scope exit in last-in, first-out (LIFO) order\r\nwith each deferpushing to this stack. To add routines to the stack runtime.deferproc or its variant are called. To\r\nexecute the routines on the stack the Go compiler places a call to runtime.deferreturn before the function exits.\r\nThe source code and disassembly in Figure 3 illustrates this concept.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 3 of 11\n\nFigure 3: Runtime functions implementing defer\r\nReturn Values with Errors\r\nMost Go functions return both a value and an optional error. Unlike C, most Go functions return two values. Prior\r\nto Go 1.17, these values were passed on the stack. In recent versions, Go introduced a register-based ABI so both\r\nvalues are often stored in registers.\r\nFigure 4: Stack vs. register ABI in Go return values\r\nReturning a single value and an error is a common idiom but Go doesn’t place a limit on the number of return\r\nvalues. It’s possible to see functions returning tens of values.\r\nNow that it's apparent why symbol recovery is significant, let's examine some important Go structures that\r\nGoReSym parses to extract symbol information, beginning with the pclntab structure.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 4 of 11\n\nPCLNTAB\r\nThe pclntab structure is short for “Program Counter Line Table”. The design for this table is documented on this\r\npage, but has evolved in more recent Go versions. The table is used to map a virtual memory address back to the\r\nnearest symbol name to generate stack traces with function and file names. The original specification states this\r\ninformation can be used for language features such as garbage collection, but this doesn’t appear to be true in\r\nmodern runtime versions. For symbol recovery purposes, the pclntab is important because it stores function\r\nnames, function start and end addresses, file names, and more.\r\nLocating the pclntab works differently depending on the file format. For ELF and Mach-O files, the pclntab is\r\nlocated in a named section within the binary. ELF files contain a section named .gopclntab that stores the pclntab\r\nwhile Mach-O files use a section named __gopclntab. Locating the pclntab in PE files is more complex and begins\r\nwith identifying a symbol table referred to in the Go source code as .symtab.\r\nFor PE files, the .symtab symbol table is pointed to by the FileHeader.PointerToSymbolTable field. A symbol in\r\nthis table named runtime.pclntab contains the address of the pclntab. ELF and Mach-O files also have a .symtab\r\nsymbol table that contains a runtime.pclntab symbol but do not rely on it to locate the pclntab. To locate the\r\n.symtab in ELF files, look for a section named .symtab of type SH_SYMTAB. In Mach-O files, the .symtab is\r\nreferenced by an LC_SYMTAB load command. The following is a list of relevant symbols present in the .symtab:\r\nruntime.pclntab\r\nruntime.epclntab\r\nruntime.symtab\r\nruntime.esymtab\r\npclntab\r\nepclntab\r\nsymtab\r\nesymtab\r\nSymbols without the runtime prefix are legacy symbols used instead of the runtime-prefixed ones. These won’t be\r\nreferenced going forward but note that if the runtime-prefixed symbols are looked up and don’t exist, the legacy\r\nones are usually attempted as a fallback. Symbols with an e prefix (e.g., epclntab) denote the end of a\r\ncorresponding table.\r\nThe runtime.symtab symbol points to a second, Go-specific symbol table that is no longer filled with symbols as\r\nof Go 1.3. In ELF and Mach-O files, the runtime.symtab symbol points to a named section – .gosymtab and\r\n__gosymtab, respectively – that stores this legacy table. Despite no longer being filled with symbols, many tools\r\nexpect the symbol and section pointed to by this symbol to exist. The Go runtime, without modification, will\r\nrefuse to parse binaries without this legacy symbol table.\r\nThe graphical overview in Figure 5 illustrates how the pclntab, the .symtab, and this legacy symbol table are\r\nrelated.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 5 of 11\n\nFigure 5: Layout of a Go binary's symbol tables\r\nWhen a Go binary is stripped, the .symtab symbol table is zeroed out or not present. This removes the ability to\r\nfind symbols such as runtime.pclntab. However, the data those symbols point to, such as the pclntab itself, is still\r\npresent in the binary. For ELF and Mach-O files, the named sections (e.g., .gopclntab) are also still present and\r\ncan be used to locate the pclntab . Therefore, manual location of the pclntab for both stripped and unstripped\r\nbinaries can be performed in one of three ways, with earlier steps being preferred:\r\n1. Locate the .symtab symbols and resolve runtime.pclntab\r\n2. For ELF and Mach-O files, locate the .gopclntab or __gopclntab section\r\n3. Scan the binary to locate the pclntab manually\r\nThe last option is the ultimate fallback mechanism for locating the table. Byte scanning is always required for\r\nstripped PE files and occasionally for ELF and Mach-O binaries with mutated section names. The byte scanning\r\nmethod involves searching for the pclntab header structure shown in Figure 6.\r\ntype pcHeader struct { // header of pclntab\r\n magic uint32\r\n pad1, pad2 uint8 // 0,0\r\n …\r\n}\r\nFigure 6: pclntab header structure\r\nThe start of the pclntab is always a version-specific magic number. This number dictates table parsing behavior for\r\neach layout format, which changes at a different versioning cadence than the rest of the runtime. Valid magic\r\nnumbers are found on this GitHub page. As a result, we can perform a linear byte scan for these magic numbers to\r\nidentify the pclntab.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 6 of 11\n\nSome packers such a UPX change section names and sizes. When scanning packed binaries, care must be taken to\r\nmerge all section data prior to performing a scan. For some packed binaries, the pclntab spans two sections. If\r\neach section is scanned individually, only a partial pclntab may be discovered.\r\nGoReSym includes the following changes to the Go runtime code to locate the pclntab:\r\nModification of the file format parser to continue if the runtime.symtab symbol is missing. Since Go 1.3,\r\nthis symbol, and the legacy table it points to, is not used but the runtime code validates it always exists,\r\nwhich is unnecessary.\r\nWhen symbols are located, validate they point to plausibly correct data\r\nIf standard symbol or section-based location fails, perform byte scanning to locate the pclntab\r\nReturn additional information about the pclntab such as its virtual address\r\nBeyond storing important function metadata, the pclntab can also be used to locate a second metadata structure\r\nnamed moduledata.\r\nMODULEDATA\r\nThe moduledata structure is an internal runtime table that stores information about the layout of the file as well as\r\nruntime information used to support core features such as garbage collection and reflection. Unlike the pclntab,\r\nthis structure cannot be found via symbols. Its layout also changes much more frequently.\r\nTo locate the structure, we first must examine how it is defined. There are two definitions that depend on the Go\r\nruntime version. In modern versions, the pclntab is found via an offset in the pcHeaderstructure shown in Figure\r\n7.\r\ntype moduledata struct {\r\n pcHeader *pcHeader\r\n …\r\n}\r\ntype moduledata struct {\r\n pclntable []byte\r\n …\r\n}\r\nFigure 7: Two variants of moduledata's structure\r\nIn older Go versions, the pclntab was directly embedded as a byte array or \"slice\" rather than referenced by an\r\noffset. The in-memory layout of both, however, is similar if we examine the format of a Go slice (Figure 8).\r\ntype GoSlice struct {\r\n data *byte;\r\n len int;\r\n capacity int;\r\n}\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 7 of 11\n\nFigure 8: Go slice structure\r\nSince a slice holds its data pointer as the first member, both versions of moduledata start as if they were defined\r\nlike Figure 9.\r\ntype moduledata struct {\r\n pclntable *byte\r\n ...starts to differ...\r\n}\r\nFigure 9: Common moduledata memory layout\r\nTherefore, to locate the moduledata table, we can do a linear byte scan for the virtual address of the pclntab (i.e.,\r\nsearch for a structure that holds a pointer to this address as the first member). Alternatively, moduledata can also\r\nbe found by disassembling the function runtime.moduledataverify, which holds a pointer to the moduledata and\r\nwalks it.\r\nUnlike the pclntab, the runtime does not include a generic moduledata parser that works across all Go versions. To\r\novercome this, GoReSym includes parsing logic for each supported runtime version. Despite the moduledata\r\nlayout changing frequently, field encodings such as strings are more stable. As a result, it’s easy to write utility\r\nroutines that handle common encodings. This eases the transition to a new runtime version as only a few utility\r\nroutines and structure layouts need changed.\r\nWithin the moduledata array there is a list named typelinks. It stores type information for types defined in a Go\r\nprogram and is used for reflection and garbage collection. The structure stored in this list is named rtype and\r\nvaries between runtime versions; however, in each format the name of the type can be extracted. This can be\r\nuseful for situations like channels where types are passed around. Without the type name it's not possible to know\r\nthe type of data being passed over the channel.\r\nHere’s an example before type recovery where the runtime_makechan function takes a pointer to an interface:\r\nAfter type recovery we can name the interface:\r\nThis indicates the channel has Boolean values passed through it. Examining the Go source code for this call\r\nconfirms the type:\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 8 of 11\n\nEach rtype can have additional fields depending on the type it represents. For channels, the extended structure\r\nresembles the following, where the base rtype is embedded as the first member (Figure 10).\r\ntype chantype struct {\r\n typ _type\r\n elem *_type\r\n dir uintptr\r\n}\r\nFigure 10: Extended channel rtype structure\r\nThe dir field within this extended type structure allows us to recognize the direction of a channel (i.e., send,\r\nreceive, or both). GoReSym handles all extended type structures and extracts additional relevant information. For\r\ntypes such as interface and struct, this extended information lists methods as well as fields and their offsets, which\r\nfacilitates reconstruction of both internal and user-defined structures.\r\nBUILDINFO\r\nStarting in Go 1.18, additional metadata is provided in a table named buildinfo. This table is emitted by default but\r\ncan be easily omitted with compiler flags. When present, the table can provide the following: compiler and linker\r\nflags, values of the environment variables GOOS and GOARCH, git information, and information about the\r\npackage name of both the main and dependency packages. In previous Go versions, finding data such as the\r\nruntime version had to be done by linear scanning for string fragments like \"go1.\". This scan was required because\r\nthe global value runtime.buildVersion is not referenced by any symbols, so locating it is difficult.\r\nThe buildinfo structure has a named section in ELF and Mach-O files but the runtime disregards this information\r\nduring the location process. To identify this information, the runtime reads the first 64KB of a file, performs a\r\nlinear scan for the magic string \"\\xff Go buildinf:\", and parses the data immediately after. When this information is\r\npresent, GoReSym uses it and only relies on byte scanning techniques when the GOOS and GOVERSION values\r\nare missing.\r\nGoReSym Usage\r\nGoReSym is a standalone application that can be executed on the command line or incorporated into a larger batch\r\nprocessing script. It doesn’t perform additional binary analysis outside of Go’s symbols. As a result, data\r\nextraction typically finishes in 1-5 seconds for even the most complex binaries.\r\nBy default, GoReSym prints concise output instead of all extracted information. Various flags can be used to alter\r\nGoReSym's behavior. The -t flag instructs GoReSym to emit type and interface lists, which are useful when\r\nreversing channels and other routines that accept types. In some cases, not all types present in the disassembly are\r\nlisted in the typelinks array. Channel objects are a good example of this is. Figure 11 shows an unreferenced type\r\nbeing loaded into the rax register prior to the runtime_newobject call.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 9 of 11\n\nFigure 11: Unreferenced type 0x48D2A0\r\nIn these instances the -m flag and the virtual address of the type can be passed to GoReSym as shown in Figure\r\n12. Note the -human flag is used to emit flat output rather than JSON.\r\n./GoReSym -m 0x48D2A0 -human ../gotests/example\r\n \r\n-TYPE STRUCTURES-VA: 0x48d2a0\r\ntype struct {\r\n .F uintptr\r\n .autotmp_11 int\r\n .autotmp_13 chan int Direction: (Both)\r\n}\r\nFigure 12: Manual rtype structure extraction\r\nThe type at the provided address is parsed by GoReSym and printed, allowing us to see it’s related to the\r\nallocation of the channel. Other useful flags include -p to print file paths present in a binary and the -d flag to print\r\ninformation that has been classified as part of a default package.\r\nFinally, some obfuscators remove Go runtime version information. Because types are version-specific, this\r\nprevents GoReSym from parsing types. To overcome this, the -v flag can be provided to suggest a runtime\r\nversion. GoReSym will attempt to parse types using structures from the suggested version. Trial and error can be\r\nused to guess the correct version. Also, the pclntab version can be used as a hint for where to start; however, recall\r\nthe pclntab version differs from the runtime version.\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 10 of 11\n\nExisting Tools\r\nThere are public and commercial tools that support some, or perhaps all, of the type parsing logic addressed in\r\nGoReSym. Prior works such as Redress and IDAGolangHelper are excellent and should be celebrated. The\r\nprimary difference between GoReSym and existing tools is that GoReSym is based on the Go runtime source\r\ncode. This helps counter the rapid pace of Go internal structure evolution. Additionally, because the runtime is\r\nalready cross-architecture, GoReSym is too. All new parsing logic takes care to correctly support 32 and 64-bit big\r\nand little-endian architectures. Care was also taken to handle unpacked or partially corrupted samples where\r\npossible, something other tools may struggle with. Overall, GoReSym is designed to work in cases where other\r\ntools fail and offers a way to ease tool maintenance as Go evolves.\r\nReferences and Credits\r\nThe original package classification ideas, but not code, behind redress is used in GoReSym and the recursive type\r\nparsing idea from the JEB blog post helped when implementing typelink enumeration. Thanks to these authors for\r\nmaking their work publicly available.\r\nRedress GitHub\r\nIDAGolangHelper GitHub\r\nAnalyzing Golang Executables post on the JEB Decompiler Blog\r\nNow go check out GoReSym!\r\nPosted in\r\nThreat Intelligence\r\nSecurity \u0026 Identity\r\nSource: https://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nhttps://www.mandiant.com/resources/blog/golang-internals-symbol-recovery\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://www.mandiant.com/resources/blog/golang-internals-symbol-recovery"
	],
	"report_names": [
		"golang-internals-symbol-recovery"
	],
	"threat_actors": [],
	"ts_created_at": 1775439004,
	"ts_updated_at": 1775791236,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/ec337ae7b43eab03d4de8059fe206af1544dff67.pdf",
		"text": "https://archive.orkl.eu/ec337ae7b43eab03d4de8059fe206af1544dff67.txt",
		"img": "https://archive.orkl.eu/ec337ae7b43eab03d4de8059fe206af1544dff67.jpg"
	}
}