理解Go程序进程的内存空间布局

理解Go程序进程的内存空间布局

2021-10-30
Go

Linux进程内存分布简介 #

在Linux系统中,一个程序进程在内存布局上遵循一定规律,进程的内存空间布局由高地址到低地址大致可分为以下几段:

  • 栈(stack): 用户态的栈,栈的大小是固定的,其大小可以使用ulimit -s查看和调整,一般默认为8Mb,栈从高地址向低地址增长(函数调用)
  • 堆(heap): 动态分配的内存空间,程序在运行时动态分配和释放,堆内存的分配不是连续的,整体上是从低地址向高地址增长
  • bss(未初始化数据区): 未初始化数据区bss, 存放全局的未初始化赋值的变量
  • data(初始化数据区): 存放已经初始化的全局变量数据
  • text: 存放程序代码

memory-layout-c.jpg

ELF文件格式介绍 #

ELF全称“Executable and Linkable Format”,即可执行可链接文件格式,Linux上的可执行文件就是采用的这个格式。 我们以Go程序代码在Linux下编译的可执行文件为例进行分析,示例的hello.go代码如下:

 1package main
 2import "fmt"
 3var (
 4   fooVar = "hello"
 5   barVar string
 6)
 7func main() {
 8  barVar = "hellob"
 9  fmt.Println(fooVar, barVar)
10}

编译hello.go源码文件得到二进制文件hello:

1go build -gcflags "-N -l" hello.go

使用-gcflags "-N -l"禁用了编译优化和内联

使用readelf -e命令分析二进制文件hello这个ELF文件的组成:

  1readelf -e hello
  2ELF Header:
  3  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  4  Class:                             ELF64
  5  Data:                              2's complement, little endian
  6  Version:                           1 (current)
  7  OS/ABI:                            UNIX - System V
  8  ABI Version:                       0
  9  Type:                              EXEC (Executable file)
 10  Machine:                           Advanced Micro Devices X86-64
 11  Version:                           0x1
 12  Entry point address:               0x45c1a0
 13  Start of program headers:          64 (bytes into file)
 14  Start of section headers:          456 (bytes into file)
 15  Flags:                             0x0
 16  Size of this header:               64 (bytes)
 17  Size of program headers:           56 (bytes)
 18  Number of program headers:         7
 19  Size of section headers:           64 (bytes)
 20  Number of section headers:         23
 21  Section header string table index: 3
 22
 23Section Headers:
 24  [Nr] Name              Type             Address           Offset
 25       Size              EntSize          Flags  Link  Info  Align
 26  [ 0]                   NULL             0000000000000000  00000000
 27       0000000000000000  0000000000000000           0     0     0
 28  [ 1] .text             PROGBITS         0000000000401000  00001000
 29       000000000007d39e  0000000000000000  AX       0     0     32
 30  [ 2] .rodata           PROGBITS         000000000047f000  0007f000
 31       0000000000034eac  0000000000000000   A       0     0     32
 32  [ 3] .shstrtab         STRTAB           0000000000000000  000b3ec0
 33       000000000000017a  0000000000000000           0     0     1
 34  [ 4] .typelink         PROGBITS         00000000004b4040  000b4040
 35       00000000000004d8  0000000000000000   A       0     0     32
 36  [ 5] .itablink         PROGBITS         00000000004b4520  000b4520
 37       0000000000000058  0000000000000000   A       0     0     32
 38  [ 6] .gosymtab         PROGBITS         00000000004b4578  000b4578
 39       0000000000000000  0000000000000000   A       0     0     1
 40  [ 7] .gopclntab        PROGBITS         00000000004b4580  000b4580
 41       0000000000058710  0000000000000000   A       0     0     32
 42  [ 8] .go.buildinfo     PROGBITS         000000000050d000  0010d000
 43       0000000000000020  0000000000000000  WA       0     0     16
 44  [ 9] .noptrdata        PROGBITS         000000000050d020  0010d020
 45       00000000000105c0  0000000000000000  WA       0     0     32
 46  [10] .data             PROGBITS         000000000051d5e0  0011d5e0
 47       0000000000007810  0000000000000000  WA       0     0     32
 48  [11] .bss              NOBITS           0000000000524e00  00124e00
 49       000000000002ef28  0000000000000000  WA       0     0     32
 50  [12] .noptrbss         NOBITS           0000000000553d40  00153d40
 51       0000000000005360  0000000000000000  WA       0     0     32
 52  [13] .zdebug_abbrev    PROGBITS         000000000055a000  00125000
 53       0000000000000119  0000000000000000           0     0     1
 54  [14] .zdebug_line      PROGBITS         000000000055a119  00125119
 55       000000000001ae04  0000000000000000           0     0     1
 56  [15] .zdebug_frame     PROGBITS         0000000000574f1d  0013ff1d
 57       0000000000005421  0000000000000000           0     0     1
 58  [16] .debug_gdb_s[...] PROGBITS         000000000057a33e  0014533e
 59       0000000000000028  0000000000000000           0     0     1
 60  [17] .zdebug_info      PROGBITS         000000000057a366  00145366
 61       0000000000031048  0000000000000000           0     0     1
 62  [18] .zdebug_loc       PROGBITS         00000000005ab3ae  001763ae
 63       000000000001906f  0000000000000000           0     0     1
 64  [19] .zdebug_ranges    PROGBITS         00000000005c441d  0018f41d
 65       0000000000008b8b  0000000000000000           0     0     1
 66  [20] .note.go.buildid  NOTE             0000000000400f9c  00000f9c
 67       0000000000000064  0000000000000000   A       0     0     4
 68  [21] .symtab           SYMTAB           0000000000000000  00197fa8
 69       000000000000c378  0000000000000018          22   120     8
 70  [22] .strtab           STRTAB           0000000000000000  001a4320
 71       000000000000b002  0000000000000000           0     0     1
 72Key to Flags:
 73  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
 74  L (link order), O (extra OS processing required), G (group), T (TLS),
 75  C (compressed), x (unknown), o (OS specific), E (exclude),
 76  l (large), p (processor specific)
 77
 78Program Headers:
 79  Type           Offset             VirtAddr           PhysAddr
 80                 FileSiz            MemSiz              Flags  Align
 81  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
 82                 0x0000000000000188 0x0000000000000188  R      0x1000
 83  NOTE           0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
 84                 0x0000000000000064 0x0000000000000064  R      0x4
 85  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
 86                 0x000000000007e39e 0x000000000007e39e  R E    0x1000
 87  LOAD           0x000000000007f000 0x000000000047f000 0x000000000047f000
 88                 0x000000000008dc90 0x000000000008dc90  R      0x1000
 89  LOAD           0x000000000010d000 0x000000000050d000 0x000000000050d000
 90                 0x0000000000017e00 0x000000000004c0a0  RW     0x1000
 91  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
 92                 0x0000000000000000 0x0000000000000000  RW     0x8
 93  LOOS+0x5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
 94                 0x0000000000000000 0x0000000000000000         0x8
 95
 96 Section to Segment mapping:
 97  Segment Sections...
 98   00
 99   01     .note.go.buildid
100   02     .text .note.go.buildid
101   03     .rodata .typelink .itablink .gosymtab .gopclntab
102   04     .go.buildinfo .noptrdata .data .bss .noptrbss
103   05
104   06

经过简化后Go二进制文件ELF格式大致如下图所示:

go-elf-fmt.png

Go二进制文件ELF由:

  • EFL Header: ELF Header中包含架构、ABI版本等基础信息,需要注意Entry point address就是Go程序的入口地址
  • Section Headers
  • Program headers
  • Sections

主要有以下几个Section:

  • .text存放的是程序代码
  • .rodata存放的是常量数据,例如程序中的字面量编译时会被放到这个Section中
  • .data.noptrdata存放的是在编译时已经初始化赋值的全局变量,在Go中应该就是已经初始化的各个包级别的变量
  • .bss.noptrbss存放的是未初始化的全局变量,在Go中应该就是未初始化的各个包级别的变量

Go是有运行时的垃圾回收语言,.data分为有指针.data section和无指针.noptrdatasection,.bss同样也分为有指针和无指针两个section。

ELF文件的Section Headers中通过Flags字段给出了各个Section的权限:

section权限
.textAX 可读可执行
.rodataA 可读
.dataWA 读写
.noptrdataWA 读写
.bssWA 读写
.noptrbssWA 读写

Go程序进程的内存空间布局 #

在执行程序时,ELF文件被加载到内存中,相同权限的Section会被对应到一个Segment中,大致是下图的内存加载结构:

elf-mem-load.png

下面我们使用objdump这个命令对照hello.go这个简单的程序找一下代码区和数据区的各个段内容。

查看.text:

 1objdump -t -j .text hello
 2hello:     file format elf64-x86-64
 3
 4SYMBOL TABLE:
 50000000000401000 l     F .text  0000000000000000 runtime.text
 60000000000401d40 l     F .text  000000000000022d cmpbody
 70000000000401fa0 l     F .text  000000000000013e memeqbody
 80000000000402120 l     F .text  0000000000000117 indexbytebody
 90000000000457be0 l     F .text  0000000000000040 gogo
100000000000457c20 l     F .text  0000000000000035 callRet
110000000000457c60 l     F .text  000000000000002f gosave_systemstack_switch
120000000000457ca0 l     F .text  000000000000000d setg_gcc
130000000000457cc0 l     F .text  000000000000055a aeshashbody
140000000000458220 l     F .text  000000000000004b debugCall32
150000000000458280 l     F .text  000000000000004b debugCall64
1600000000004582e0 l     F .text  000000000000006f debugCall128
170000000000458360 l     F .text  0000000000000072 debugCall256
1800000000004583e0 l     F .text  0000000000000072 debugCall512
190000000000458460 l     F .text  0000000000000072 debugCall1024
2000000000004584e0 l     F .text  0000000000000072 debugCall2048
210000000000458560 l     F .text  0000000000000076 debugCall4096
2200000000004585e0 l     F .text  0000000000000076 debugCall8192
230000000000458660 l     F .text  0000000000000076 debugCall16384
2400000000004586e0 l     F .text  0000000000000076 debugCall32768
250000000000458760 l     F .text  0000000000000076 debugCall65536
26000000000047e39e l     F .text  0000000000000000 runtime.etext
270000000000401000 g     F .text  0000000000000059 internal/cpu.Initialize
280000000000401060 g     F .text  0000000000000625 internal/cpu.processOptions
2900000000004016a0 g     F .text  0000000000000445 internal/cpu.doinit
300000000000401b00 g     F .text  000000000000001b internal/cpu.cpuid.abi0
310000000000401b20 g     F .text  0000000000000011 internal/cpu.xgetbv.abi0
320000000000401b40 g     F .text  0000000000000087 type..eq.internal/cpu.option
330000000000401be0 g     F .text  0000000000000094 type..eq.[15]internal/cpu.option
340000000000401c80 g     F .text  000000000000006f runtime/internal/sys.OnesCount64
350000000000401d00 g     F .text  0000000000000022 internal/bytealg.init.0
360000000000401f80 g     F .text  000000000000000e runtime.cmpstring
3700000000004020e0 g     F .text  000000000000001b runtime.memequal
380000000000402100 g     F .text  000000000000001c runtime.memequal_varlen
390000000000402240 g     F .text  0000000000000018 internal/bytealg.IndexByteString.abi0
400000000000402260 g     F .text  0000000000000043 type..eq.internal/abi.RegArgs
4100000000004022c0 g     F .text  0000000000000043 runtime.memhash128
420000000000402320 g     F .text  000000000000004a runtime.strhashFallback
430000000000402380 g     F .text  00000000000000e5 runtime.f32hash

查看.rodata,根据hello.go中的字符串字面量"theFoolValue"和"theBarVarValue"应该是在.rodata,下面搜索一下:

1objdump -s -j .rodata hello | grep hello
2hello:     file format elf64-x86-64
3 494900 616e6863 68616e68 656c6c6f 696e6974  anhchanhelloinit
4 494aa0 656e6365 6572726e 6f206865 6c6c6f62  enceerrno hellob

包变量fooVar已经初始化位于.data,包变量barVar未初始化位于.bss

1objdump -t hello | grep main.fooVar
2000000000051dda0 g     O .data	0000000000000010 main.fooVar
3
4objdump -t hello | grep main.barVar
500000000005250f0 g     O .bss	0000000000000010 main.barVar

参考 #

© 2024 青蛙小白