1.5 Go程序是如何运行的
想了解Go程序在计算机上是如何运行的,就得了解该程序运行可能涉及的步骤,此步骤一般包括编译、连接和执行等环节。
对源码进行编译后,可执行文件如何由操作系统加载到内存中并运行呢?事实上,操作系统已将整个内存划分为多个区域,每个区域用于执行不同的任务。内存区域的名称与作用如表1-2所示。
表1-2 内存区域的名称与作用
Go源码文本转换为二进制可执行文件涉及以下两个步骤。
(1)编译:将文本代码编译为目标文件(.o、.a)。
(2)连接:将目标文件合并为可执行文件。
可以使用命令go build -x main.go查看Go源码的编译和连接过程,具体如下,请注意其中的关键字compile和link。
go build -x main.go
WORK=/var/folders/dw/hlkj1z4166l8ml089msv27q40000gp/T/go-build3987574679
mkdir -p $WORK/b038/
...
cd ../golang-1/1-intro-golang/helloworld/v1
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b038/_pkg_.a -trimpath "$WORK/b038=>" -p golang-1/1-intro-golang/helloworld/v1/mytask -lang=go1.17 -complete -buildid _AQjJb_ oKnpP5p-Roetq/_AQjJb_oKnpP5p-Roetq -goversion go1.17.1 -importcfg $WORK/b038/importcfg -pack -c=4 ./mytask/mystruct.go ./mytask/taskprocess.go
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b038/_pkg_.a # internal
cp $WORK/b038/_pkg_.a /Users/makesure10/Library/Caches/go-build/c2/c23aaafabe1f90ef18 755a4aa4a017ae2a3b5cd48fcb5e65a77722b1a47f262f-d # internal
mkdir -p $WORK/b001/
...
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/ importcfg.link -buildmode=exe -buildid=-hKqHVTOB_jOb7Jh11JS/HTWG1gA5dVlOMQ10XUm4/ jng5Q6XZtNSFKpOfgseT/-hKqHVTOB_jOb7Jh11JS -extld=clang $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/
以Linux系统为例,生成二进制可执行文件后,操作系统执行该文件的步骤为解析EFL Hearder,加载文件内容到内存中,从Entry point处开始执行代码。
在Linux系统中,我们可以使用工具readelf查找程序的入口地址。通过关键字Entry point找到Go进程的执行入口后,就可以知道Go进程开始的位置。示例代码如下。
[root ~]# readelf -h main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x45c220 //程序的入口地址
Start of program headers: 64 (bytes into file)
Start of section headers: 456 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 23
Section header string table index: 3
从上面的结果可以看到,程序的入口地址是0x45c220。接着使用dlv调试器查看汇编代码。
# dlv exec ./main
2022-03-04T16:26:02+08:00 error layer=deBugger can't find build-id note on binary
Type 'help' for list of commands.
(dlv) b *0x45c220
Breakpoint 1 set at 0x45c220 for _rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8
可以看到,与地址0x45c220对应的汇编代码是rt0_linux_amd64.s(此入口文件因平台而异)。下面这段代码显示了汇编代码rt0_linux_amd64.s涉及的一些指令。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
rt0_go的功能可分为两部分。第一部分是获取系统参数和检查runtime,第二部分则是启动Go程序,大致启动流程为从rt0_linux_amd64.s中进入程序,创建主协程,创建runtime.main,调用main.main。
汇编语言是高级语言与操作系统之间的桥梁,所有的汇编指令都可以转换为二进制机器码序列,以被CPU理解。Go语言在编译时也会转换成汇编语言,所以了解一些汇编知识可以更好地帮助我们深入理解Go语言的一些底层机制。