理解Go程序进程的内存空间布局
2021-10-30
Linux进程内存分布简介 #
在Linux系统中,一个程序进程在内存布局上遵循一定规律,进程的内存空间布局由高地址到低地址大致可分为以下几段:
- 栈(stack): 用户态的栈,栈的大小是固定的,其大小可以使用
ulimit -s
查看和调整,一般默认为8Mb,栈从高地址向低地址增长(函数调用) - 堆(heap): 动态分配的内存空间,程序在运行时动态分配和释放,堆内存的分配不是连续的,整体上是从低地址向高地址增长
- bss(未初始化数据区): 未初始化数据区bss, 存放全局的未初始化赋值的变量
- data(初始化数据区): 存放已经初始化的全局变量数据
- text: 存放程序代码
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由:
- 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和无指针.noptrdata
section,.bss
同样也分为有指针和无指针两个section。
ELF文件的Section Headers中通过Flags字段给出了各个Section的权限:
section | 权限 |
---|---|
.text | AX 可读可执行 |
.rodata | A 可读 |
.data | WA 读写 |
.noptrdata | WA 读写 |
.bss | WA 读写 |
.noptrbss | WA 读写 |
Go程序进程的内存空间布局 #
在执行程序时,ELF文件被加载到内存中,相同权限的Section会被对应到一个Segment中,大致是下图的内存加载结构:
下面我们使用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