文章目录
- go 编译命令 ldflags -w -s的作用和问题
- 使用 `file` 命令查看文件类型
- go 语言逆向参考
- go ID
- 版本
- GOROOT和GOPATH
- GOROOT
- GOPATH
- GOROOT和GOPATH的关系
- 示例
- go build和 go mod
- pclntab (Program Counter Line Table 程序计数器行数映射表)
- Moduledata
- 程序启动
go 编译命令 ldflags -w -s的作用和问题
https://blog.csdn.net/Kevin_Gates/article/details/130107710
-ldflags 参数可以用来向编译器传递额外的参数。其中,-w 和 -s 是两个常用的参数。
- w:去掉 dwarf 调试信息。会减小可执行文件的大小。
- s:去掉符号表信息。会进一步减小可执行文件的大小。
在编译可执行文件时使用了 -ldflags "-w -s"
参数后,你可以通过以下几种方式来检查生成的可执行文件是否去掉了调试信息和符号表信息。
使用 file
命令查看文件类型
file
命令可以显示可执行文件的基本信息,如果去掉了调试信息,file
的输出中应该不会提到调试符号(debugging symbols)。
go 语言逆向参考
https://www.cnblogs.com/lordtianqiyi/articles/16315905.html
https://forum.butian.net/share/1874
https://jiayu0x.com/2020/09/28/go-binary-reverse-engineering-tips-and-example/
【技术推荐】正向角度看Go逆向
Golang逆向资料
Go 语言设计与实现
go ID
Build ID 是 Go 二进制文件中的元信息之一
llk@ubuntu:~/Desktop/go-reverse/basic$ go tool buildid basic
-VH5aOSX9mVrJNbHxruF/FwnNtemK3U4RER9TE7_8/5x300ikDrpjLHazn79N3/ftLrOMaMAByiPF3chfMM
无论是gccgo还是go编译的,file
或者readelf -n
都能看到BuildID
版本
一、查看版本号
go version xx.exe
二、查看地址以及依赖库
go version -m xxx.exe
GOROOT和GOPATH
GOROOT
: 主要是Go语言工具链所在的位置,通常不需要手动修改。GOPATH
: 用于管理和组织用户的Go项目,可以根据需要设置和修改,方便管理不同的工作空间。
GOROOT
- 定义:
GOROOT
是Go语言的安装目录。它指向Go工具链、标准库源码和编译器等内容所在的目录。 - 默认值: 通常在安装Go时,
GOROOT
会自动设置为安装路径。例如,如果你通过官方安装包安装Go,GOROOT
可能会是/usr/local/go
(在类Unix系统上)或C:\Go
(在Windows上)。 - 作用:
- 存放Go编译器和工具链。
- 包含标准库源码。
- Go工具使用
GOROOT
来找到编译器、链接器等工具,以及标准库的源码。
GOPATH
- 定义:
GOPATH
是Go项目的工作目录。它指向开发者的工作空间,可以包含多个目录。 - 默认值: 如果没有设置
GOPATH
,默认值通常是用户的主目录下的go
目录(如$HOME/go
)。 - 结构:
GOPATH
目录通常包含三个子目录:src
: 存放源代码。pkg
: 存放已编译的包对象文件。bin
: 存放编译后生成的可执行文件。
- 作用:
- 用于存储和组织用户的Go代码。
go get
命令会下载依赖包到GOPATH
的src
目录中。- 编译时,Go工具会从
GOPATH
中查找包的源码。
GOROOT和GOPATH的关系
GOROOT
是Go工具链的安装目录,包含标准库。GOPATH
是用户工作空间的目录,包含用户的代码和第三方库。
示例
假设你在$HOME/go_projects
下开发一个项目:
-
你可以设置
GOPATH
为$HOME/go_projects
:export GOPATH=$HOME/go_projects
-
你的项目目录结构可能如下:
$HOME/go_projects/ src/ github.com/ yourusername/ yourproject/ main.go pkg/ bin/
-
编译你的项目:
cd $HOME/go_projects/src/github.com/yourusername/yourproject go build
编译后的可执行文件将会出现在$HOME/go_projects/bin
目录中。
go build和 go mod
go build命令用来启动编译,它可以将Go语言程序与相关依赖编译成一个可执行文件,其语法格式如下。
go build fileName -o 可执行程序名
其中 fileName 为所需要的参数,可以是一个或者多个 Go 源文件名(当有多个参数时需要使用空格将两个相邻的参数隔开),也可以省略不写。
使用 go build 命令进行编译时,不同参数的执行结果也是不同的。
编译go程序也是编译与链接的一个流程
golang包管理工具----go mod
一个 Module 中可以包含多个不同的 Package,而每个 Package 中可以包含多个目录和很多的源码文件。
Module :moduledata 里面有pclntab 全名是 Program Counter Line Table
pclntab (Program Counter Line Table 程序计数器行数映射表)
对应到源码为pcHeader结构体,源码路径在src/runtime/symtab.go
// pcHeader 包含 pclntab 查找使用的数据。
type pcHeader struct {
magic uint32 // 0xFFFFFFF1: 用于识别 pcHeader 结构的魔数。
pad1, pad2 uint8 // 0,0: 对齐结构在内存中的填充字节。
minLC uint8 // 架构的最小指令大小。
ptrSize uint8 // 指针的字节大小(取决于 32 位或 64 位架构)。
nfunc int // 模块中的函数数量。
nfiles uint // 文件表中的条目数量(模块中的源文件数量)。
textStart uintptr // 功能入口 PC 偏移的基地址,等于 moduledata.text。
funcnameOffset uintptr // 从 pcHeader 开始到 funcnametab 变量的偏移量,包含函数名。
cuOffset uintptr // 从 pcHeader 开始到 cutab 变量的偏移量,用于编译单元信息。
filetabOffset uintptr // 从 pcHeader 开始到 filetab 变量的偏移量,包含文件名。
pctabOffset uintptr // 从 pcHeader 开始到 pctab 变量的偏移量,用于 PC 数据表。
pclnOffset uintptr // 从 pcHeader 开始到 pclntab 变量的偏移量,用于 PC 到行号的映射。
}
-
cu_offset:
cu_offset
是编译单元(Compilation Unit)的偏移量。- 在
gopclntab
中,编译单元是一个大块,包含了函数信息、行号表等。 cu_offset
指的是当前函数在编译单元中的偏移位置。
-
pctab:
pctab
是一个表格,存储了程序计数器(PC)和行号(line number)之间的映射关系。- 这种映射关系使得在运行时可以根据PC值找到源代码中的行号,帮助调试和错误处理。
-
runtime_pclntab:
runtime_pclntab
是整个gopclntab
的起始点或基地址。- 这是一个全局表格,包含了所有函数的PC和函数信息的映射。
-
dq offset cu_offset - offset runtime_pclntab:
- 这表示当前函数的
cu_offset
减去全局runtime_pclntab
的偏移量,得到一个具体的偏移值。 - 这个偏移值用来定位当前函数在整个
gopclntab
结构中的位置。
- 这表示当前函数的
-
dq offset pctab - offset runtime_pclntab:
- 这表示当前函数的
pctab
减去全局runtime_pclntab
的偏移量。 - 这个偏移值用来定位
pctab
在整个gopclntab
结构中的位置。
- 这表示当前函数的
函数表
函数地址偏移是和函数表第一个函数地址的偏移
源码文件表
Moduledata
Module 是比 Package 更高层次的概念,具体表现在一个 Module 中可以包含多个不同的 Package,而每个 Package 中可以包含多个目录和很多的源码文件。
相应地,Moduledata 在 Go 二进制文件中也是一个更高层次的数据结构,它包含很多其他结构的索引信息
根据 Moduledata 的定义,源码路径在src/runtime/symtab.go
type moduledata struct {
sys.NotInHeap // 仅用于静态数据
// 指向pcHeader结构的指针,包含程序计数器的头信息
pcHeader *pcHeader
funcnametab []byte // 函数名称表
cutab []uint32 // 编译单元表
filetab []byte // 文件表
pctab []byte // 程序计数器表
pclntable []byte // PC到行号的映射表
ftab []functab // 函数表
findfunctab uintptr // findfunctab函数的起始地址
minpc, maxpc uintptr // 代码段的起始和结束地址
text, etext uintptr // 代码段的起始和结束地址
noptrdata, enoptrdata uintptr // 不包含指针的数据段的起始和结束地址
data, edata uintptr // 数据段的起始和结束地址
bss, ebss uintptr // BSS段的起始和结束地址
noptrbss, enoptrbss uintptr // 不包含指针的BSS段的起始和结束地址
covctrs, ecovctrs uintptr // 覆盖计数器的起始和结束地址
end, gcdata, gcbss uintptr // 结束地址,GC数据,GC BSS
types, etypes uintptr // 类型信息的起始和结束地址
rodata uintptr // 只读数据段的起始地址
gofunc uintptr // go.func.*
textsectmap []textsect // 文本段映射
typelinks []int32 // 类型链接信息,存储类型偏移
itablinks []*itab // 接口表链接
ptab []ptabEntry // P 表
pluginpath string // 插件路径
pkghashes []modulehash // 包哈希信息
// 这个切片记录了启动程序所需的初始化任务。由链接器构建。
inittasks []*initTask
modulename string // 模块名
modulehashes []modulehash // 模块哈希信息
hasmain uint8 // 如果模块包含main函数,则值为1,否则为0
gcdatamask, gcbssmask bitvector // GC数据和BSS段的位向量
typemap map[typeOff]*_type // 前一个模块中从偏移量到*_rtype的映射
bad bool // 模块加载失败,应被忽略
next *moduledata // 指向下一个模块的指针
}
Moduledata 是可以串成链表的形式的,而一个完整的可执行 Go 二进制文件中,只有一个 firstmoduledata 包含如上完整的字段
程序启动
Golang 程序启动过程
rt0_amd64_linux->rt0_amd64->runtime_rt0_go
rt0_go 代码比较长,可分为两个部分,第一部分是系统参数获取和运行时检查。第二部分是 go 程序启动的核心,总体启动流程如下
执行 runtime.main, 主要进行了
1.启动系统后台监控sysmon 线程
2.执行 runtime 包内 init
3.启动gc
4.用户包依赖 init 的执行
5.执行用户main.mian 方法
-
rt0_go
函数- 当程序启动时,Go运行时的入口点是
rt0_go
函数。这个函数负责初始化运行时环境。
- 当程序启动时,Go运行时的入口点是
-
schedinit
:初始化运行时组件rt0_go
调用runtime_schedinit_0
来初始化调度器、内存分配器和垃圾回收器等等。- 调度器初始化:初始化调度器,准备管理Goroutines的创建、调度和执行。
- 内存分配器初始化:初始化堆内存管理,为Goroutines和程序数据分配内存。
- 垃圾回收器初始化:初始化垃圾回收器,自动管理内存的分配和回收。
-
newproc
:创建主Goroutinert0_go
调用runtime_newproc_0
创建主Goroutine。主Goroutine的入口函数是runtime.main
,而不是用户的main
函数。- 创建Goroutine:
newproc
函数负责创建一个新的Goroutine,并将其加入调度器的队列中。这个Goroutine的入口函数是runtime.main
。
-
mstart
:启动调度器的调度循环rt0_go
调用runtime_mstart
启动主线程。主线程会执行runtime.main
函数。- 调度循环:
mstart
函数启动调度器的调度循环,开始执行队列中的Goroutines。第一个执行的Goroutine是入口方法为runtime.main
的G。
-
runtime.main
:执行初始化和用户main
函数runtime.main
函数首先执行一些必要的初始化操作,例如设置全局变量、初始化标准库等。- 然后,
runtime.main
调用用户的main
函数。