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语言的一些底层机制。