go version是什么?
-
可以用来查看当前的
go
版本> go version go version go1.16.2 darwin/amd64
-
在go1.13之后,还支持对二进制和目录进行操作
The
go
version
command now accepts arguments naming executables and directories. When invoked on an executable,go
version
prints the version of Go used to build the executable. If the-m
flag is used,go
version
prints the executable's embedded module version information, if available. When invoked on a directory,go
version
prints information about executables contained in the directory and its subdirectories.对于可执行二进制,会打印用于构建可执行文件的go版本。
如果使用了
-m
标志,则go version
将打印可执行文件的嵌入式module版本信息(如果有)。当在目录上调用go版本时,它会打印有关该目录及其子目录中包含的可执行文件的信息。
go version是如何实现的?
go 二进制是通过 $GOROOT/src/cmd/go/main.go
这个文件编译生成的。
go version
的主要实现逻辑在 $GOROOT/src/cmd/go/internal/version/version.go
在Goland中可以这样设置进行调试该功能,这种方式同样适用于调试go的编译器等工具。
除去注释和错误检查的后的代码如下
func runVersion(ctx context.Context, cmd *base.Command, args []string) {
// go vervion后面无参数时的处理逻辑
if len(args) == 0 {
fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
return
}
// go version后面有参数时的处理逻辑
for _, arg := range args {
info, err := os.Stat(arg)
if info.IsDir() {
scanDir(arg) // 递归扫描文件夹
} else {
scanFile(arg, info, true) // 扫描二进制文件
}
}
}
go vervion后面无参数时的处理逻辑
如上节所示,会打印三个信息:
- 版本号 go1.16.2
- 操作系统 darwin
- 处理器架构 amd64
后面两个是写在指定文件中的,跟随官方源码一起发布的,重点关注runtime.Version()
// $GOROOT/src/runtime/extern.go
func Version() string {
return sys.TheVersion
}
// $GOROOT/src/runtime/internal/sys/zversion.go
// Code generated by go tool dist; DO NOT EDIT.
package sys
const TheVersion = `go1.16.2`
const Goexperiment = ``
const StackGuardMultiplierDefault = 1
这样看来,这个也是固定写到文件中的?
别急,再观察一下,这个文件是不被git管理的,而且在官方代码库中这个文件也是不存在的。
// $GOROOT/src/cmd/dist/build.go
// gentab records how to generate some trivial files.
var gentab = []struct {
nameprefix string
gen func(string, string)
}{
{"zdefaultcc.go", mkzdefaultcc},
{"zosarch.go", mkzosarch},
{"zversion.go", mkzversion},
{"zcgo.go", mkzcgo},
// not generated anymore, but delete the file if we see it
{"enam.c", nil},
{"anames5.c", nil},
{"anames6.c", nil},
{"anames8.c", nil},
{"anames9.c", nil},
}
// $GOROOT/src/cmd/dist/buildruntime.go
func mkzversion(dir, file string) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "// Code generated by go tool dist; DO NOT EDIT.\n")
fmt.Fprintln(&buf)
fmt.Fprintf(&buf, "package sys\n")
fmt.Fprintln(&buf)
fmt.Fprintf(&buf, "const TheVersion = `%s`\n", findgoversion())
fmt.Fprintf(&buf, "const Goexperiment = `%s`\n", os.Getenv("GOEXPERIMENT"))
fmt.Fprintf(&buf, "const StackGuardMultiplierDefault = %d\n", stackGuardMultiplierDefault())
writefile(buf.String(), file, writeSkipSame)
}
这个文件实际上是在编译go的过程中自动生成的。
和TheVersion
相关的是findgoversion
函数
// $GOROOT/src/cmd/dist/build.go
// findgoversion determines the Go version to use in the version string.
func findgoversion() string {
// 查找 $GOROOT/VERSION文件是否存在,如果存在直接返回该文件中的信息
// 该文件在正式有tag号的版本中都是存在的,但是对于master和一些实验性分支是没有的
path := pathf("%s/VERSION", goroot)
if isfile(path) {
b := chomp(readfile(path))
if b != "" {
if strings.HasPrefix(b, "devel") {
// 看注释是交叉编译相关的处理,暂不关心
if hostType := os.Getenv("META_BUILDLET_HOST_TYPE"); strings.Contains(hostType, "-cross") {
fmt.Fprintf(os.Stderr, "warning: changing VERSION from %q to %q\n", b, "builder "+hostType)
b = "builder " + hostType
}
}
return b
}
}
// 这是一个缓存文件,生成该文件的逻辑在下方,避免每次都调用git去重新查找版本信息
path = pathf("%s/VERSION.cache", goroot)
if isfile(path) {
return chomp(readfile(path))
}
// 预期$GOROOT应该是一个git仓库
if !isGitRepo() {
fatalf("FAILED: not a Git repo; must put a VERSION file in $GOROOT")
}
// 调用git获取当前分支
branch := chomp(run(goroot, CheckExit, "git", "rev-parse", "--abbrev-ref", "HEAD"))
// What are the tags along the current branch?
tag := "devel"
precise := false
// 如果当前是release-branch.XXX分支,寻找该分支上最近的一个tag号,但是当前仓库中release-branch分支也都是有VERSION这个文件的,理论上当前这是一个不会走到的if分支
if strings.HasPrefix(branch, "release-branch.") {
tag, precise = branchtag(branch)
}
// 对于master等分支,使用git信息生成版本号
if !precise {
// Tag does not point at HEAD; add hash and date to version.
tag += chomp(run(goroot, CheckExit, "git", "log", "-n", "1", "--format=format: +%h %cd", "HEAD"))
}
// 写入版本信息到缓存文件,上方会优先读取缓存文件中的版本号
writefile(tag, path, 0)
return tag
}
对于master分支,go version
的输出如下
go version devel go1.17-4a7effa418 Wed Apr 28 14:01:59 2021 +0000 darwin/amd64
go version后面有参数时的处理逻辑
> go version gin-vue-admin
gin-vue-admin: go1.16.2
go version -m
的输出,会将go mod的依赖也一些打印
> go version -m gin-vue-admin
gin-vue-admin: go1.16.2
path gin-vue-admin
mod gin-vue-admin (devel)
dep github.com/360EntSecGroup-Skylar/excelize/v2 v2.3.2 h1:MHu5KWWt28FzRGQgc4Ryj/lZT/W/by4NvsnstbWwkkY=
dep github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
dep github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
dep github.com/PuerkitoBio/purell v1.1.1
……
如何从文件中查找版本信息?
// $GOROOT/src/cmd/go/internal/version/version.go
func scanFile(file string, info fs.FileInfo, mustPrint bool) {
// 需要是一个可执行文件
if !isExe(file, info) {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: not executable file\n", file)
}
return
}
// 打开该文件
x, err := openExe(file)
if err != nil {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: %v\n", file, err)
}
return
}
defer x.Close()
// 查找version和module信息
vers, mod := findVers(x)
if vers == "" {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: go version not found\n", file)
}
return
}
// 打印版本信息
fmt.Printf("%s: %s\n", file, vers)
// 如果开启了-m,打印module信息
if *versionM && mod != "" {
fmt.Printf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
}
}
// buildInfo section 的魔数信息
var buildInfoMagic = []byte("\xff Go buildinf:")
// findVers finds and returns the Go version and module version information
// in the executable x.
func findVers(x exe) (vers, mod string) {
// Read the first 64kB of text to find the build info blob.
text := x.DataStart() // 对于elf格式的文件,查找.go.buildinfo section的起始地址
data, err := x.ReadData(text, 64*1024) // 从起始地址读取64KB信息
if err != nil {
return
}
// 找到 "\xff Go buildinf:" 所在位置
for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
if len(data) < 32 {
return
}
}
// Decode the blob.
ptrSize := int(data[14]) // 读取指针大小
bigEndian := data[15] != 0 // 大小端
var bo binary.ByteOrder
if bigEndian {
bo = binary.BigEndian
} else {
bo = binary.LittleEndian
}
var readPtr func([]byte) uint64
if ptrSize == 4 {
readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
} else {
readPtr = bo.Uint64
}
// 从data[16:16+ptrSize] 读取记录version字符串metadata信息的地址 metadataAddr
// 从metadataAddr中读取实际字符串的地址和长度信息
// 根据上方读到的信息读取真实字符串
vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
if vers == "" {
return
}
// 从data[16+ptrSize:16+2*ptrSize] 读取module字符串metadata信息的地址
// 与上方读取version字符串的流程一致
mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
// Strip module framing.
mod = mod[16 : len(mod)-16]
} else {
mod = ""
}
return
}
// readString returns the string at address addr in the executable x.
func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
// 从指定的Addr向后读取两个指针大小的数据
hdr, err := x.ReadData(addr, uint64(2*ptrSize))
if err != nil || len(hdr) < 2*ptrSize {
return ""
}
// 解析字符串真实地址
dataAddr := readPtr(hdr)
// 解析字符串的长度
dataLen := readPtr(hdr[ptrSize:])
// 读取字符串数据
data, err := x.ReadData(dataAddr, dataLen)
if err != nil || uint64(len(data)) < dataLen {
return ""
}
// 转换为string并返回
return string(data)
}
// $GOROOT/src/encoding/binary/binary.go
// 大端情况下,获取地址信息
func (bigEndian) Uint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}
// 小端情况下,获取地址信息
func (littleEndian) Uint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
跟随上方流程,使用readelf
和objdump
来手动查找该信息
Note:只关注有用的信息,mac 的可执行文件为 Mach-O 而非 ELF,这里的内容是在Linux中操作。
-
查看该二进制都有哪些Section
> readelf -S ./gin-vue-admin There are 36 section headers, starting at offset 0x270: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align …… [15] .gopclntab PROGBITS 000000000135c160 00f5c160 000000000050f118 0000000000000000 A 0 0 32 [16] .go.buildinfo PROGBITS 000000000186c000 0146c000 0000000000000020 0000000000000000 WA 0 0 16 [17] .dynamic DYNAMIC 000000000186c020 0146c020 0000000000000130 0000000000000010 WA 10 0 8 ……
-
Dump .go.buildinfo Section的信息
> objdump -s -j .go.buildinfo ./gin-vue-admin ./gin-vue-admin: file format elf64-x86-64 Contents of section .go.buildinfo: 186c000 ff20476f 20627569 6c64696e 663a0800 . Go buildinf:.. 186c010 f06fd002 00000000 3070d002 00000000 .o......0p......
根据源码可知
- [0:14)区间内应为Magic信息 :
"\xff Go buildinf:"
- 第 15 个字符为指针大小:08,指针大小为8
- 第 16 个字符为大小端信息:00,不为0是大端,为0是小端,当前为小端
- [16:16+ptrSize) 记录version字符串metadata信息的地址:ptrSize在上方已找出,为8,且为小端。原始信息为
f06fd002
,对应的地址信息应为0x02d06ff0
- [16+ptrSize:16+2*ptrSize) 记录module字符串metadata信息的地址:ptrSize在上方已找出,为8,且为小端。原始信息为
3070d002
,对应的地址信息应为0x02d07030
- [0:14)区间内应为Magic信息 :
-
Dump 字符串metadata信息
从
0x02d06ff0
向后dump16个字节的信息,0x02d06ff0 + 0x10 = 0x02d07000
> objdump -s --start-address 0x02d06ff0 --stop-address 0x02d07000 ./gin-vue-admin ./gin-vue-admin: file format elf64-x86-64 Contents of section .data: 2d06ff0 95340b01 00000000 08000000 00000000 .4..............
对应字符串的地址信息为
95340b01
,根据小端原则,字符串真实地址为0x010b3495
对应字符串的长度信息为
08000000
,长度为8> objdump -s --start-address 0x010b3495 --stop-address 0x010b349d ./gin-vue-admin ./gin-vue-admin: file format elf64-x86-64 Contents of section .rodata: 10b3495 676f31 2e31362e 32 go1.16.2
获取到了版本信息为
go1.16.2
对于module信息,方法一致
> objdump -s --start-address 0x02d07030 --stop-address 0x02d07040 ./gin-vue-admin ./gin-vue-admin: file format elf64-x86-64 Contents of section .data: 2d07030 60e40f01 00000000 c0200000 00000000 > objdump -s --start-address 0x010fe460 --stop-address 0x1100520 ./gin-vue-admin ontents of section .rodata: 10fe460 3077af0c 92740802 41e1c107 e6d618e6 0w...t..A....... 10fe470 70617468 0967696e 2d767565 2d61646d path.gin-vue-adm 10fe480 696e0a6d 6f640967 696e2d76 75652d61 in.mod.gin-vue-a 10fe490 646d696e 09286465 76656c29 090a6465 dmin.(devel)..de 10fe4a0 70096769 74687562 2e636f6d 2f333630 p.github.com/360 10fe4b0 456e7453 65634772 6f75702d 536b796c EntSecGroup-Skyl 10fe4c0 61722f65 7863656c 697a652f 76320976 ar/excelize/v2.v 10fe4d0 322e332e 32096831 3a4d4875 354b5757 2.3.2.h1:MHu5KWW 10fe4e0 74323846 7a524751 67633452 796a2f6c t28FzRGQgc4Ryj/l 10fe4f0 5a542f57 2f627934 4e76736e 73746257 ZT/W/by4NvsnstbW 10fe500 776b6b59 3d0a6465 70096769 74687562 wkkY=.dep.github ……
该信息实际上是从二进制中读取的,那么还有最后一个问题,是何时写入的?
这里实际上是go的链接器做的事情了
// $GOROOT/src/cmd/link/internal/ld/main.go
// Main is the main entry point for the linker code.
func Main(arch *sys.Arch, theArch Arch) {
// 开始写入buildinfo Section
bench.Start("buildinfo")
ctxt.buildinfo()
}
// $GOROOT/src/cmd/link/internal/ld/data.go
func (ctxt *Link) buildinfo() {
if ctxt.linkShared || ctxt.BuildMode == BuildModePlugin {
// -linkshared and -buildmode=plugin get confused
// about the relocations in go.buildinfo
// pointing at the other data sections.
// The version information is only available in executables.
return
}
ldr := ctxt.loader
s := ldr.CreateSymForUpdate(".go.buildinfo", 0)
// On AIX, .go.buildinfo must be in the symbol table as
// it has relocations.
s.SetNotInSymbolTable(!ctxt.IsAIX())
s.SetType(sym.SBUILDINFO)
s.SetAlign(16)
// The \xff is invalid UTF-8, meant to make it less likely
// to find one of these accidentally.
const prefix = "\xff Go buildinf:" // 14 bytes, plus 2 data bytes filled in below
data := make([]byte, 32)
// 写入Magic
copy(data, prefix)
// 写入指针大小
data[len(prefix)] = byte(ctxt.Arch.PtrSize)
data[len(prefix)+1] = 0
// 写入大小端信息
if ctxt.Arch.ByteOrder == binary.BigEndian {
data[len(prefix)+1] = 1
}
s.SetData(data)
s.SetSize(int64(len(data)))
r, _ := s.AddRel(objabi.R_ADDR)
r.SetOff(16)
r.SetSiz(uint8(ctxt.Arch.PtrSize))
// 写入 runtime.buildVersion 符号的地址
r.SetSym(ldr.LookupOrCreateSym("runtime.buildVersion", 0))
r, _ = s.AddRel(objabi.R_ADDR)
r.SetOff(16 + int32(ctxt.Arch.PtrSize))
r.SetSiz(uint8(ctxt.Arch.PtrSize))
// 写入 runtime.modinfo 符号的地址
r.SetSym(ldr.LookupOrCreateSym("runtime.modinfo", 0))
}
// $GOROOT/src/runtime/proc.go
var buildVersion = sys.TheVersion
知道了原理能做什么?
> strip gin-vue-admin
> readelf -S ./gin-vue-admin
There are 27 section headers, starting at offset 0x2942388:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[15] .go.buildinfo PROGBITS 000000000186c000 0146c000
0000000000000020 0000000000000000 WA 0 0 16
可以看到,在使用strip
去除二进制的调试信息后,.go.buildinfo Section
仍然是存在的。
那么如果你拿到了一个没有符号表的Go二进制,就可以通过上方的流程,来进行”逆向“,不过go version
也可以直接操作去除符号信息的二进制,哈哈哈。
那么,就这?
定制 Go 版本信息
那么理解了原理,就可以开始魔改了,如果定制Go版本信息,可以在那里修改呢?
在 $GOROOT/src/runtime/internal/sys/zversion.go
中修改?
这个文件每次重新编译都会重新生成的,所以是无法生效的。
在 runVersion
中修改?
这个是依赖于运行 go verison
时的go二进制的,可以改变go version
无参数时的输出信息。
该信息不会被写入到二进制中,所以编译出的二进制,使用正常go发行版去检查版本信息时,无法达到预期的效果。
我选择在 $GOROOT/src/cmd/dist/buildruntime.go
的mkzversion
中进行修改
func mkzversion(dir, file string) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "// Code generated by go tool dist; DO NOT EDIT.\n")
fmt.Fprintln(&buf)
fmt.Fprintf(&buf, "package sys\n")
fmt.Fprintln(&buf)
// 修改这一行
fmt.Fprintf(&buf, "const TheVersion = `%s Powered by LeonardWang`\n", findgoversion())
fmt.Fprintf(&buf, "const Goexperiment = `%s`\n", os.Getenv("GOEXPERIMENT"))
fmt.Fprintf(&buf, "const StackGuardMultiplierDefault = %d\n", stackGuardMultiplierDefault())
writefile(buf.String(), file, writeSkipSame)
}
> go version
go version go1.16.2 Powered by LeonardWang darwin/amd64
> go build -o main ./main.go
> go version main
main: go1.16.2 Powered by LeonardWang
参考资料
本文由 LeonardWang 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Mar 31,2022