ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pwndbg 调试 Go 程序:go-dump 与 go-type 命令实战指南

Pwndbg 调试 Go 程序:go-dump 与 go-type 命令实战指南 Pwndbg 调试 Go 程序go-dump 与 go-type 命令实战指南【免费下载链接】pwndbgExploit Development and Reverse Engineering with GDB LLDB Made Easy项目地址: https://gitcode.com/GitHub_Trending/pw/pwndbg导读Go 的运行时内存布局切片、map、interface、字符串等与 C 完全不同导致传统 GDB 调试手段很难直接读懂 Go 程序的内存内容。Pwndbg 专门提供了go-dump与go-type两条命令分别用于按类型解释并打印某个地址处的 Go 值和解析 Go 编译器生成的运行时反射类型对象。本文将以 docs/tutorials/go-debugging.md 为主线结合 pwndbg/aglib/godbg.py、pwndbg/commands/godbg.py 及测试样例 tests/binaries/host/gosample.native.go完整讲解类型语法、命令行参数、运行原理与实战技巧帮助你像读源码一样直接读取 Go 内存中的任意值。一、go-dump按类型解释内存中的 Go 值go-dump命令的职责是从指定地址读取内存并按照你给出的 Go 类型把它解析、打印成 Go 字面量形式。其命令定义位于 pwndbg/commands/godbg.py用法为usage: go-dump [-h] [-x] [-f [DECIMALS]] [-d] [-p] ty address它接收两个位置参数位置参数含义ty要解释的 Go 类型例如map[string]int也可以是某个运行时类型对象的地址如0x408860此时会在运行时解析类型address要读取的值所在的内存地址支持表达式例如x命令别名是god。该命令只能在程序运行中OnlyWhenRunning装饰器使用因为读取目标进程内存必须处于活动会话中。1.1 支持的完整类型语法go-dump的第一个参数可以使用与 Go 语言几乎一致的语法由 pwndbg/aglib/godbg.py 中的递归下降解析器_parse_type、_parse_basic_ty、_parse_slice_ty、_parse_pointer_ty、_parse_array_ty、_parse_map_ty、_parse_struct_ty、_parse_runtime_ty逐层解析。支持的类型包括整型int、int8、int16、int32、int64、int128以及对应的uint系列含uint8~uint64、uintptr。其中int/uint/uintptr的字长与当前架构的 Go word size 一致x86-64 为 8 字节i386 为 4 字节见 word_size杂项类型bool、rune、uintptr、string。注意字符串在 Go 内存中是一个指针 长度的两字结构打印时按此布局读取浮点类型float32、float64复数类型complex64、complex128复数在内存中按实部、虚部两个浮点数排列接口类型any表示空接口interface{}interface表示所有带方法集的非空接口函数类型funcptr表示所有函数类型按闭包结构解析指针*ELEM切片[]ELEM按指针 长度 容量三字结构解析数组[LEN]ELEM按连续数据解析映射map[KEY][VAL]。关于 map 有一个非常重要的坑Go 中的 map 变量本质上是指向内部 hmap/Map 结构的指针而go-dump的map[KEY]VAL类型直接打印的是内部 map 本体因此要打印一个 map 值通常需要写成*map[KEY]VAL。这一点在解析器的 MapType 实现 中有明确注释Go maps are actually pointers, but the map here is not。1.2 结构体的专用语法结构体类型的写法与 Go 略有不同其目的是避免让用户手工计算字段偏移同时允许在只知道部分字段信息的情况下也能打印。语法为struct(SIZE){}内部是用分号分隔的OFFSET:FIELD_NAME:TYPE三元组struct(24){0:foo:string;16:bar:int64}这条语法表示一个 24 字节的 Go 结构体struct { foo string; bar int64 }foo位于偏移 0string在 64 位下占 16 字节指针 8 长度 8bar位于偏移 16int64占 8 字节。解析逻辑见 _parse_struct_ty。1.3 完整示例原文档中的三个示例可以直接在 GDB 会话中复现pwndbg go-dump map[string]int 0xc0000b20f0 {a: 1, b: 2, c: 3} pwndbg go-dump any 0xc0000ace40 ([]struct { a int; b string }) [struct {a: 1, b: first}, struct {a: 2, b: second}] pwndbg go-dump struct(24){0:a:int;8:b:string} 0xc000108120 struct {a: 1, b: first}第二个例子值得注意当以any空接口为类型时go-dump会自动读取接口中携带的动态类型信息并解析出真实的动态类型——这里它识别出该接口实际装的是一个[]struct { a int; b string }切片并逐个打印切片元素。这正是运行时类型解析能力的体现详见本文第三节。1.4 命令行可选参数go-dump提供四个实用的可选参数定义于 pwndbg/commands/godbg.py参数长选项作用-x--hex用十六进制显示非指针整数-f DECIMALS--decimals设置浮点数显示的小数位数-d--debug开启调试输出显示 dump 中所有内容的内存地址切片/映射元素地址、切片容量、字符串指针等-p--pretty开启美化打印pretty printing这些选项最终被组装进FormatOpts见 FormatOpts并贯穿整个 dump 过程-x控制fmt_int的输出格式-f控制fmt_float的格式化精度-d通过fmt_debug在所有值前面附加括号包裹的地址信息-p通过fmt_elems决定切片、映射、结构体等容器元素是单行输出还是换行缩进输出。此外还有两个与go-dump美化打印相关的配置项见 docs/configuration/config.mdgo-dump-indent-amount默认4美化打印时的缩进空格数go-dump-line-width默认80美化打印的软行宽当一行内容超过该宽度时会自动换行缩进。它们分别对应源码中的 indent_amount 与 line_width 两个参数定义。二、go-type解析 Go 运行时反射类型对象Go 编译器会为程序中使用到的每一个类型在二进制中发射一个运行时类型对象runtime reflection type。这些对象记录了类型名称、kind、大小、对齐方式以及结构体字段/切片元素/映射键值等全部元信息。go-type命令的作用就是解析并展示这些对象。其命令定义为 pwndbg/commands/godbg.py用法非常简单usage: go-type [-h] address别名是goty只接受一个类型对象地址作为参数输出该类型的完整元信息。例如pwndbg go-type 0x49fbc0 Name: struct { a int; b string } Kind: STRUCT Size: 24 (0x18) Align: 8 Parse: struct(24){0:a:int;8:b:string} Field a: Offset: 0 (0x0) Type name: int Type addr: 0x498ce0 Field b: Offset: 8 (0x8) Type name: string Type addr: 0x498aa0go-type的 Parse 行非常有用它输出一个可以直接回填给go-dump的类型语法字符串也就是struct(24){0:a:int;8:b:string}。这意味着你可以先go-type弄清结构体布局再把得到的Parse串直接用于go-dump打印任意实例。2.1 从源码看类型对象的解码过程go-type的输出由 decode_runtime_type 及其内部实现 _inner_decode_runtime_type 产生。解码依据的是 Go 标准库src/internal/abi/type.go中Type结构体的布局源码中留有完整注释type Type struct { Size_ uintptr PtrBytes uintptr Hash uint32 TFlag TFlag Align_ uint8 FieldAlign_ uint8 Kind_ Kind Equal func(unsafe.Pointer, unsafe.Pointer) bool GCData *byte Str NameOff PtrToThis TypeOff }解码过程的关键步骤包括类型名称读取Str字段是一个相对于 types 区段起始地址的偏移NameOff需要先确定 type 区段的基地址才能拼出名称。基地址的确定优先级是先遍历runtime.firstmoduledata符号要求目标有调试符号得到 types 区间失败则回退到启发式方法——优先用type:*符号地址否则假设类型位于.rodata段开头见 get_type_start名称编码兼容Go 1.17 之后类型名称是1 字节 bitfield varint 长度前缀字符串1.17 之前则是1 字节 bitfield 2 字节大端长度前缀read_type_name 会根据 get_go_version 推断出的 Go 版本自动选择解码方式kind 掩码Kind_低 5 位为真正的类型种类对应 GoTypeKind 枚举BOOL1、INT2、…、STRUCT25、UNSAFEPOINTER26高位的标志位还包含 direct interface 等信息递归解析子类型对于 struct 字段、slice 元素、map 键值、数组元素、函数参数/返回值、接口方法集等会通过类型对象中的指针/偏移递归解码并用缓存cache机制处理type a []a这类递归类型BackrefType。三、运行时类型解析让 go-dump 自动识别动态类型Go 程序运行时对类型信息的依赖正是go-dump能智能工作的底层基础。原文档指出编译器为每个类型都发射了类型对象这带来了两方面的能力接口值的自动解析dump 一个any接口时go-dump会从接口的内存布局中读取动态类型指针解码出真实类型并递归打印。这也是前文go-dump any 0xc0000ace40能自动展开成切片的原因。对应实现见 BasicType.dump 中 any/interface 分支按类型地址直接 dumpgo-dump的第一个参数不仅可以是类型名字符串还可以直接是一个类型对象地址。当参数能被int(ty, 0)解析为数字时命令会优先走运行时类型解码路径pwndbg go-dump 0x49fbc0 0xc000108120 struct {a: 1, b: first}此处0x49fbc0就是前面go-type解析出的struct { a int; b string }类型对象的地址。命令层面对这一逻辑的处理在 pwndbg/commands/godbg.py先尝试把ty当作十六进制地址解码为运行时类型失败才回退到parse_type字符串解析。3.1 如何定位一个类型对象要在实战中利用按类型地址 dump首先得拿到类型对象地址。原文档给出的定位方法是找到传给堆分配函数的类型指针。Go 的堆分配入口如runtime.newobject、runtime.makeslice的第一个参数正是要分配对象的类型指针。你可以在这些函数下断点观察参数寄存器/栈上保存的地址再用go-type展开查看pwndbg b runtime.newobject pwndbg c pwndbg go-type $rdi # x86-64 下第一个参数寄存器对于makeslice这类函数第一个参数同样是元素类型指针其余参数为长度与容量。拿到类型地址后go-type输出的Parse字段即可直接转化为go-dump的类型语法实现类型已知 → 任意实例可读的完整链路。四、底层实现各类型的布局与解析细节go-dump对每种类型的解析都严格遵循 Go 的内存布局约定这一点在 pwndbg/aglib/godbg.py 中体现得十分细致理解这些有助于判断输出结果的正确性也能帮你排查解析失败的原因。4.1 基础类型与字符串BasicType实现负责基础类型的 dumpstring在内存中是一个指针 长度共 2 个 worddump 时读取指针指向的字节并解码为字符串bool非零即true复数按实部、虚部两个浮点输出函数指针则读取闭包对象地址并解析出真实函数入口配合fmt_ptr还能自动附加符号信息。基础类型的字节大小在__post_init__中按 Go 规范确定例如int64/uint64/float64/complex64为 8 字节、complex128为 16 字节、string/any/interface为 2 个 word。4.2 切片与数组切片SliceType实现内存布局为数据指针、长度、容量三个 word。dump 时按长度逐元素解析元素之间按元素大小步进开启-d后还会额外显示容量与每个元素的地址数组ArrayType实现连续存储直接按元素个数与元素大小顺序读取。4.3 映射map兼容新旧两种布局map 是 Go 调试中最复杂的部分因为 Go 1.24 起引入了 Swiss Table 新实现。MapType实现通过 is_swiss 判断当无法确定 Go 版本或版本 1.24 时走 Swiss 布局否则走经典 hmap 布局。经典 hmap 1.24解析count/B/buckets/oldbuckets/extra等字段按每个 bucket 8 个槽位、tophash 标记、溢出桶链表overflow逐步遍历所有条目并兼容增量扩容期间旧桶未迁移完毕的情况见 dump_noswissSwiss Table 1.24解析dirPtr/dirLen目录结构、表table的 groups 引用、8 槽一组的 group 与 control bytes最高位为 0 表示槽位占用见 dump_swiss。两条路径最终都会把(键, 值)条目汇总后按键排序输出格式为{k: v, ...}。需要注意map 类型在解码时会被包成*map[...]...指针见 MAP 分支这与第一节提到的map 实际是指针完全对应。4.4 结构体与递归类型StructType实现按字段偏移逐个 dump字段可以是嵌套类型也可以是指针引用的递归类型。对于type a []a这类自引用类型解码器用缓存 BackrefType占位符解决环的问题decode_runtime_type需要序列化此类类型时则使用RuntimeTyperuntime(SIZE)ADDR语法实现。五、测试验证go-dump 的自动化保障仓库为 Go 调试功能提供了专门的集成测试 tests/library/dbg/tests/test_go.py测试样例源码为 tests/binaries/host/gosample.native.go分别编译出 x86-64 与 i386 两个版本。测试覆盖了go-dump的典型场景对map[uint8]uint64、map[string]int、[]struct { a int; b string }的any接口 dump逐一断言输出精确等于预期字面量含动态类型名如(map[string]int) {a: 1, b: 2, c: 3}使用-f 1参数验证浮点小数位数控制例如将[3]complex64中的复数打印为(1.1 2.2i)格式测试以GOMAXPROCS1环境启动、断点位于gosample.native.go:6即testFunc内部并通过x表达式直接取接口变量的地址作为go-dump的第二个参数。值得注意的是该测试在 LLDB 后端下会跳过Go tests are not supported in LLDB说明go-dump/go-type目前主要在 GDB 后端下受支持代码中大量使用pwndbg.aglib.symbol.lookup_symbol、pwndbg.aglib.elf等 aglib 抽象实际效果依赖后端的符号与内存能力。六、实战小结与注意事项综合原文档与源码在 GDB 中使用 Pwndbg 调试 Go 程序的核心流程可以归纳为确定类型对象在runtime.newobject/runtime.makeslice等分配函数下断点抓取类型指针或用go-type addr解析任意已知地址处的类型确认布局阅读go-type输出的Size/Align/Field偏移必要时直接复制其Parse字段按类型 dump用go-dump type-syntax address或go-dump type-addr address打印任意内存地址处的值接口值用anymap 值记得加*按需调整输出-p美化、-x十六进制、-f控制浮点精度、-d暴露所有内部地址用于人工核验。使用中需要留意的几个关键点map 需要解引用map[KEY]VAL打印的是内部 map 本体对 map 变量应使用*map[KEY]VAL地址参数支持表达式go-dump/go-type的地址参数由pwndbg.commands.AddressExpr解析可直接写x、$rdi等 GDB 表达式结构体语法可只写部分字段OFFSET:NAME:TYPE三要素允许只声明你关心的字段其余字段会被跳过这在只有部分类型信息时非常实用后端限制Go 相关命令目前不支持 LLDB 后端且类型解析依赖调试符号与.rodata段可用性当无法解析 moduledata 时源码会打印警告并回退到启发式定位get_type_start。结合命令参考文档 go-dump、go-type 以及配置项文档 docs/configuration/config.md你可以快速上手并在此基础上扩展出针对真实 Go 二进制含 strip 符号、不同 Go 版本的调试方案。【免费下载链接】pwndbgExploit Development and Reverse Engineering with GDB LLDB Made Easy项目地址: https://gitcode.com/GitHub_Trending/pw/pwndbg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表