Featured image of post 从Golang的二进制程序中获取version等信息

从Golang的二进制程序中获取version等信息

从Golang的二进制程序中获取version等信息。

    说来“从Golang的二进制程序中获取version”不过是一件很小的事情。大概是去年在学Go和使用eBPF捕获GoTLS加密数据时的一些小tips:

  • 为什么会有这个问题和需求?

  • Golang的编译后的二进制文件中存在哪些信息?

  • 使用readelf工具读取这些信息、使用C++编写代码读取这些信息。

前·问题

    在软件开发的历史长河中,有一个问题是所有人无法躲避的–兼容性。一种是你自己很NB不用,只需要自己兼容自己即可,一种是依赖别人,只要别人有所变动自己也要跟着改动。无论是哪种,能想象得到的,兼容适配的工作并不讨喜。

    一个程序对于非开发人员而言,其语义化版本(Semantic Versioning)尤其重要,只有知道了一个01的文件对应的版本号,我们才能知道其变动了什么,特性是什么。

    那,如何从Golang的二进制文件中获取其对应的版本号呢?

中·Golang

    首先,我们知道有go version的命令可以查看版本号,比如:

1
2
3
4
ls -lh https_server
-rwxrwxr-x 1 xxx xxx 6.7M 12月 21 16:03 https_server
go version ./https_server
./https_server: go1.20.7    

或者,我们粗暴点用strings命令查看:

1
2
3
strings https_server|grep -E "^go[0-9]+\.[0-9]+\.[0-9]+"
go1.20.7
go1.20.7

elf & readelf

     但是,如果想要更深层次的了解这些信息是存放在哪里的,那么就需要了解一下Executable and Linkable Format即ELF文件类型,在Linux下主要有如下三种文件:可执行文件(.out)、可重定位文件(.o)和共享目标文件(.so),ELF文件结构大致如下。

图源 - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format

   在 Linux 下 Go 编译出来的程序其实就是 ELF 文件类型的。因此,想要剖析 Go 的二进制文件还需要知道大概如下关于 ELF 的知识:

  • ELF header: 包含了magic、大小端、架构是32位还是64位……

  • Section Header Table: 存储了有关section信息,每个section对应section header table中的一个条目;

  • Program Header Table:存储了有关segment的信息,每个segment由一个或多个segment组成,内科在运行时用到这些信息;

  • section & segment区别 :

    • 在可执行文件格式(如ELF文件)中,section是指文件的一个逻辑分段,它包含了特定类型的数据。

    • segment通常指的是程序被加载到内存时的一个物理或逻辑单元。在某些可执行文件格式中(如ELF),“segment"更具体地指由一个或多个section组成的,用于描述程序的内存映像的部分。

    • section主要用于编译和链接过程中的组织和管理;segment的划分是为了满足运行时的需求,比如内存保护、共享和权限管理。

  • .text : 存放程序的执行代码,只可读;

  • .data: 用于存放程序中已初始化的全局变量和静态变量。这些变量在程序启动之前由编译器赋予初始值;

  • .rodata: 即read-only-data,用于存放只读数据,比如字符串常量和其他任何程序运行时不需要修改的常量数据。

runtime.buildVersion

使用 readelf 可以看到Go二进制文件的section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
readelf -S https_server
There are 36 section headers, starting at offset 0x270:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       00000000002372df  0000000000000000  AX       0     0     32
  [ 2] .plt              PROGBITS         00000000006382e0  002382e0
       0000000000000210  0000000000000010  AX       0     0     16
  [ 3] .rodata           PROGBITS         0000000000639000  00239000
       00000000000de3f3  0000000000000000   A       0     0     32
  [ 4] .rela             RELA             00000000007173f8  003173f8
       0000000000000018  0000000000000018   A      11     0     8
  [ 5] .rela.plt         RELA             0000000000717410  00317410
       0000000000000300  0000000000000018   A      11     2     8
  [ 6] .gnu.version      VERSYM           0000000000717720  00317720
       000000000000004a  0000000000000002   A      11     0     2
  [ 7] .gnu.version_r    VERNEED          0000000000717780  00317780
       0000000000000060  0000000000000000   A      10     1     8
  [ 8] .hash             HASH             00000000007177e0  003177e0
       00000000000000b8  0000000000000004   A      11     0     8
  [ 9] .shstrtab         STRTAB           0000000000000000  003178a0
       00000000000001d9  0000000000000000           0     0     1
  [10] .dynstr           STRTAB           0000000000717a80  00317a80
       0000000000000215  0000000000000000   A       0     0     1
  [11] .dynsym           DYNSYM           0000000000717ca0  00317ca0
       0000000000000378  0000000000000018   A      10     1     8
  [12] .typelink         PROGBITS         0000000000718020  00318020
       0000000000001438  0000000000000000   A       0     0     32
  [13] .itablink         PROGBITS         0000000000719460  00319460
       0000000000000808  0000000000000000   A       0     0     32
  [14] .gosymtab         PROGBITS         0000000000719c68  00319c68
       0000000000000000  0000000000000000   A       0     0     1
  [15] .gopclntab        PROGBITS         0000000000719c80  00319c80
       000000000013ce70  0000000000000000   A       0     0     32
  [16] .go.buildinfo     PROGBITS         0000000000857000  00457000
       0000000000000130  0000000000000000  WA       0     0     16
  [17] .got.plt          PROGBITS         0000000000857140  00457140
       0000000000000118  0000000000000008  WA       0     0     8
  [18] .dynamic          DYNAMIC          0000000000857260  00457260
       0000000000000120  0000000000000010  WA      10     0     8
  [19] .got              PROGBITS         0000000000857380  00457380
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .noptrdata        PROGBITS         00000000008573a0  004573a0
       000000000002fdf8  0000000000000000  WA       0     0     32
  [21] .data             PROGBITS         00000000008871a0  004871a0
       000000000000bdb0  0000000000000000  WA       0     0     32
  [22] .bss              NOBITS           0000000000892f60  00492f60
       00000000000300a0  0000000000000000  WA       0     0     32
  [23] .noptrbss         NOBITS           00000000008c3000  004c3000
       000000000000def0  0000000000000000  WA       0     0     32
  [24] .tbss             NOBITS           0000000000000000  00000000
       0000000000000008  0000000000000000 WAT       0     0     8
  [25] .debug_abbrev     PROGBITS         0000000000000000  00493000
       0000000000000133  0000000000000000   C       0     0     1
  [26] .debug_line       PROGBITS         0000000000000000  00493133
       0000000000060588  0000000000000000   C       0     0     1
  [27] .debug_frame      PROGBITS         0000000000000000  004f36bb
       000000000001349f  0000000000000000   C       0     0     1
  [28] .debug_gdb_s[...] PROGBITS         0000000000000000  00506b5a
       000000000000003a  0000000000000000           0     0     1
  [29] .debug_info       PROGBITS         0000000000000000  00506b94
       00000000000a5f3a  0000000000000000   C       0     0     1
  [30] .debug_loc        PROGBITS         0000000000000000  005acace
       0000000000080755  0000000000000000   C       0     0     1
  [31] .debug_ranges     PROGBITS         0000000000000000  0062d223
       000000000002078b  0000000000000000   C       0     0     1
  [32] .interp           PROGBITS         0000000000400fe4  00000fe4
       000000000000001c  0000000000000000   A       0     0     1
  [33] .note.go.buildid  NOTE             0000000000400f80  00000f80
       0000000000000064  0000000000000000   A       0     0     4
  [34] .symtab           SYMTAB           0000000000000000  0064d9b0
       00000000000272b8  0000000000000018          35   277     8
  [35] .strtab           STRTAB           0000000000000000  00674c68
       0000000000030483  0000000000000000           0     0     1

    在上面readelf读出的section中,其中.go.buildinfo包含了一些Go编译时的信息,比如Go的版本号、使用的module等,使用objdump查看.go.buildinfo的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
objdump -s -j .go.buildinfo https_server

https_server:     文件格式 elf64-x86-64

Contents of section .go.buildinfo:
 857000 ff20476f 20627569 6c64696e 663a0802  . Go buildinf:..
 857010 00000000 00000000 00000000 00000000  ................
 857020 08676f31 2e32302e 37fa0130 77af0c92  .go1.20.7..0w...
 857030 74080241 e1c107e6 d618e670 61746809  t..A.......path.
 857040 636f6d6d 616e642d 6c696e65 2d617267  command-line-arg
 857050 756d656e 74730a62 75696c64 092d6275  uments.build.-bu
 857060 696c646d 6f64653d 6578650a 6275696c  ildmode=exe.buil
 857070 64092d63 6f6d7069 6c65723d 67630a62  d.-compiler=gc.b
 857080 75696c64 0943474f 5f454e41 424c4544  uild.CGO_ENABLED
 857090 3d310a62 75696c64 0943474f 5f43464c  =1.build.CGO_CFL
 8570a0 4147533d 0a627569 6c640943 474f5f43  AGS=.build.CGO_C
 8570b0 5050464c 4147533d 0a627569 6c640943  PPFLAGS=.build.C
 8570c0 474f5f43 5858464c 4147533d 0a627569  GO_CXXFLAGS=.bui
 8570d0 6c640943 474f5f4c 44464c41 47533d0a  ld.CGO_LDFLAGS=.
 8570e0 6275696c 6409474f 41524348 3d616d64  build.GOARCH=amd
 8570f0 36340a62 75696c64 09474f4f 533d6c69  64.build.GOOS=li
 857100 6e75780a 6275696c 6409474f 414d4436  nux.build.GOAMD6
 857110 343d7631 0af93243 31861820 72008242  4=v1..2C1.. r..B 

其中\xff Go buildinf:是magic信息,共14个字节,在对应的Go源码中可以看到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// https://github.com/golang/go/blob/2f6a25f4478905db5e169019bf9dc39ab2a50f89/src/debug/buildinfo/buildinfo.go#L45-L49

// The build info blob left by the linker is identified by
// a 16-byte header, consisting of buildInfoMagic (14 bytes),
// the binary's pointer size (1 byte),
// and whether the binary is big endian (1 byte).
buildInfoMagic = []byte("\xff Go buildinf:")


// 数据解析见: src/debug/buildinfo/buildinfo.go#L180-L189
// Decode the blob.
// The first 14 bytes are buildInfoMagic.
// The next two bytes indicate pointer size in bytes (4 or 8) and endianness
// (0 for little, 1 for big).
// Two virtual addresses to Go strings follow that: runtime.buildVersion,
// and runtime.modinfo.
// On 32-bit platforms, the last 8 bytes are unused.
// If the endianness has the 2 bit set, then the pointers are zero
// and the 32-byte header is followed by varint-prefixed string data
// for the two string values we care about.

继续看.go.buildifo的内容:

  • 前14字节为magic,即\xff Go buildinf:

  • 15字节表示指针大小,即0x08

  • 第16字节的可能取值:0x000x010x02

    • 0x02表示,go的版本号信息紧跟其后,即为在后面的32个字节,其中前面16个字符为0忽略,从17个字节开始表示大小,在Go里为varint类型(见注释)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    # Go 1.20.7 为例
    ff20476f 20627569 6c64696e 663a    -> magic
    08                                 -> size of pointer
    02                                 -> follows the 32 bit header
    00000000 00000000 00000000 00000000 -> buildinfo header, ignore, skip.
    08                                  -> size of version string  
    676f31 2e32302e 37                  -> version, go1.20.7  
    
    # 版本字符串大小即08的解析方法见: src/encoding/binary/varint.go#L7-L25
    # This file implements "varint" encoding of 64-bit integers.
    # The encoding is:
    # - unsigned integers are serialized 7 bits at a time, starting with the
    #   least significant bits
    # - the most significant bit (msb) in each output byte indicates if there
    #   is a continuation byte (msb = 1)
    # - signed integers are mapped to unsigned integers using "zig-zag"
    #   encoding: Positive values x are written as 2*x + 0, negative values
    #   are written as 2*(^x) + 1; that is, negative numbers are complemented
    #   and whether to complement is encoded in bit 0.
    #
    # Design note:
    # At most 10 bytes are needed for 64-bit values. The encoding could
    # be more dense: a full 64-bit value needs an extra byte just to hold bit 63.
    # Instead, the msb of the previous byte could be used to hold bit 63 since we
    # know there can't be more than 64 bits. This is a trivial improvement and
    # would reduce the maximum encoding length to 9 bytes. However, it breaks the
    # invariant that the msb is always the "continuation bit" and thus makes the
    # format incompatible with a varint encoding for larger numbers (say 128-bit).
    
    • 当第16字节不为0x02时(这里以Go1.16为例),
      • 从第17字节起取一个指针(这里是8字节),这是指向version信息的指针0x00870260
      • 读地址的内容,得到指向字符串的指针即0x006bb617和字符串的长度0x06
      • 0x006bb617即可得到go1.16.

其实,翻看Go的源码可以发现,版本号信息其实就是runtime.buildVersion这个对象。所以,其实上述的步骤等价于:

  • 先找到runtime.buildVersion符号,该符号位于.data

  • 读出runtime.buildVersion的内容的到字符串指针指向的地址

  • 读取该地址的字符串,该地址位于.rodata

1
2
3
4
5
# Go1.16
# 也就是说其实17个字节拿到的是runtime.buildVersion对象的指针
# 然后读出该对象指向的内容
readelf -s https_server|grep -i 'runtime.buildVersion'
5025: 0000000000870260    16 OBJECT  GLOBAL DEFAULT   21 runtime.buildVersion

后·总结

    所以,从Go的二进制文件读取版本号信息的思路可以是:

  • 先找到runtime.buildVersion,有两个方法:

    • .go.buildinfo中按上述的步骤得到,具体为:

      • 先读取.go.buildinfo ,得到指针大小、大小端信息;

      • 如果大小端位的值为0x02则读取往后的32字节数据即为版本号信息,其中前16字节忽略,然后读取版本号信息的大小,最后读出对应大小的字符串;

      • 如果为非0x02则,先读取runtime.buildVersion

    • 直接在.data中查找.runtime.buildVersion的符号信息;

  • 然后,读出runtime.buildVersion指向的地址的内容,即为版本号信息,字符串位于.rodata

尾·链接

What Is an ELF File? | Baeldung on Linux

从 Go 的二进制文件中获取其依赖的模块信息 - 知乎 (zhihu.com)

Release History - The Go Programming Language (google.cn)

All releases - The Go Programming Language (google.cn)

哦吼是一首歌。
Built with Hugo
Theme Stack designed by Jimmy