golang 以脚本运行Go程序

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 以脚本运行Go程序相关的知识,希望对你有一定的参考价值。

//usr/bin/env go run $0 "$@"; exit
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Hello world!")
	cwd, _ := os.Getwd()
	fmt.Println("cwd:", cwd)
	fmt.Println("args:", os.Args[1:])
}

Golang内部构件,第5部分:运行时引导程序

引导过程是了解Go运行时如何工作的关键。如果您想继续使用Go,学习它是必不可少的。因此,我们的Golang Internals系列的第五部分专门讨论Go运行时,尤其是Go引导过程。这次您将了解:

  • 自举
  • 可调整大小的堆栈实现
  • 内部TLS实施

请注意,这篇文章包含许多汇编代码,您至少需要一些基础知识才能继续(这里是Go汇编程序的快速指南)。

寻找一个切入点

首先,我们需要找到启动Go程序后立即执行的功能。为此,我们将编写一个简单的Go应用。

package main

func main() {
    print(123)
}

然后,我们需要对其进行编译和链接。

go tool compile -N -l -S main.go

这将6.out在您的当前目录中创建一个名为的可执行文件。下一步涉及objdump工具,该工具特定于Linux。Windows和Mac用户可以找到类似物或完全跳过此步骤。现在,运行以下命令。

objdump -f 6.out

您应该获得输出,其中将包含起始地址。

6.out:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x000000000042f160

接下来,我们需要反汇编可执行文件,并找到哪个函数位于该地址。

objdump -d 6.out > disassemble.txt

然后,我们需要打开disassemble.txt文件并搜索42f160。我们得到的输出如下所示

000000000042f160 <_rt0_amd64_linux>:
  42f160:    48 8d 74 24 08               lea    0x8(%rsp),%rsi
  42f165:    48 8b 3c 24                  mov    (%rsp),%rdi
  42f169:    48 8d 05 10 00 00 00     lea    0x10(%rip),%rax        # 42f180 <main>
  42f170:    ff e0                            jmpq   *%rax

很好,我们找到了!我的操作系统和体系结构的入口点是一个名为的函数_rt0_amd64_linux

起始顺序

现在,我们需要在Go运行时源中找到此函数。它位于rt0_linux_amd64.s文件中。如果查看Go运行时程序包,则可以找到许多带有与操作系统和体系结构名称相关的后缀的文件名。构建运行时程序包时,仅选择与当前OS和体系结构相对应的文件。其余的被跳过。让我们仔细看一下rt0_linux_amd64.s。

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    LEAQ    8(SP), SI // argv
    MOVQ    0(SP), DI // argc
    MOVQ    $main(SB), AX
    JMP    AX

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

_rt0_amd64_linux功能是非常简单的。它调用main函数并将参数(argcargv)保存在寄存器(DISI)中。参数位于堆栈中,并且可以通过SP(堆栈指针)寄存器进行访问。主要功能也很简单。它调用的runtime.rt0_go函数更长且更复杂,因此我们将其分成小部分并分别描述。第一部分是这样的。

MOVQ    DI, AX        // argc
MOVQ    SI, BX        // argv
SUBQ    $(4*8+7), SP        // 2args 2auto
ANDQ    $~15, SP
MOVQ    AX, 16(SP)
MOVQ    BX, 24(SP)

在这里,我们将一些先前保存的命令行参数值放入AXBX减少堆栈指针。我们还为另外两个四字节变量添加了空间,并将其调整为16位对齐。最后,我们将参数移回堆栈。

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ    $runtime·g0(SB), DI
LEAQ    (-64*1024+104)(SP), BX
MOVQ    BX, g_stackguard0(DI)
MOVQ    BX, g_stackguard1(DI)
MOVQ    BX, (g_stack+stack_lo)(DI)
MOVQ    SP, (g_stack+stack_hi)(DI)

第二部分比较棘手。首先,我们将全局runtime.g0变量的地址加载到DI寄存器中。此变量在proc1.go文件中定义,并且属于该runtime,g类型。将为goroutine系统中的每个变量创建此类型的变量。如您所料,runtime.g0 描述了root goroutine。然后,我们初始化描述root堆栈的字段goroutine。的意义stack.lostack.hi应当明确。这些是指向current的堆栈开始和结束的指针goroutine,但是stackguard0andstackguard1字段是什么?为了理解这一点,我们需要搁置对该runtime.rt0_go函数的研究,并仔细研究Go中的堆栈增长。

Go中可调整大小的堆栈实现

Go语言使用可调整大小的堆栈。每一个都goroutine从一个小的堆栈开始,并且每当达到某个阈值时,它的大小就会更改。显然,有一种方法可以检查我们是否已达到此阈值。实际上,检查是在每个功能的开始执行的。为了了解它的工作原理,让我们用该-S标志再编译一次示例程序(这将显示生成的汇编代码)。主要功能的开始看起来像这样。

"".main t=1 size=48 value=0 args=0x0 locals=0x8
0x0000 00000 (test.go:3)    TEXT    "".main+0(SB),$8-0
0x0000 00000 (test.go:3)    MOVQ    (TLS),CX
0x0009 00009 (test.go:3)    CMPQ    SP,16(CX)
0x000d 00013 (test.go:3)    JHI ,22
0x000f 00015 (test.go:3)    CALL    ,runtime.morestack_noctxt(SB)
0x0014 00020 (test.go:3)    JMP ,0
0x0016 00022 (test.go:3)    SUBQ    $8,SP

首先,我们将值从线程本地存储(TLS)加载到CX寄存器(我们已经在上一篇文章中解释了TLS是什么)。此值始终包含一个指向runtime.g与current对应的结构的指针goroutine。然后,我们将堆栈指针与runtime.g结构中位于16个字节偏移处的值进行比较。我们可以轻松地计算出这对应于该stackguard0字段。

因此,这就是我们检查是否已达到堆栈阈值的方式。如果尚未达到,则检查失败。在这种情况下,我们将runtime.morestack_noctxt反复调用该函数,直到为堆栈分配了足够的内存为止。该stackguard1字段的工作方式与极为相似stackguard0,但是它在C堆栈增长序言中使用,而不是在Go中使用。的内部运作runtime.morestack_noctxt方式也是一个非常有趣的话题,但我们将在稍后进行讨论。现在,让我们回到引导过程。

Go自举的进一步调查

我们将通过查看runtime.rt0_go函数中代码的下一部分来开始启动序列。

// find out information about the processor we\'re on
    MOVQ    $0, AX
    CPUID
    CMPQ    AX, $0
    JE    nocpuinfo

    // Figure out how to serialize RDTSC.
    // On Intel processors LFENCE is enough. AMD requires MFENCE.
    // Don\'t know about the rest, so let\'s do MFENCE.
    CMPL    BX, $0x756E6547  // "Genu"
    JNE    notintel
    CMPL    DX, $0x49656E69  // "ineI"
    JNE    notintel
    CMPL    CX, $0x6C65746E  // "ntel"
    JNE    notintel
    MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

    MOVQ    $1, AX
    CPUID
    MOVL    CX, runtime·cpuid_ecx(SB)
    MOVL    DX, runtime·cpuid_edx(SB)
nocpuinfo:

这部分对于理解Go的主要概念不是至关重要的,因此我们将对其进行简要介绍。在这里,我们试图找出正在使用的处理器。如果是Intel,则设置runtime·lfenceBeforeRdtsc变量。该runtime·cputicks方法是唯一使用此变量的地方。此方法利用不同的汇编器指令来获取cpu ticks依赖于的值runtime·lfenceBeforeRdtsc。最后,我们调用CPUID汇编程序指令,执行该指令,然后将结果保存在runtime·cpuid_ecxruntime·cpuid_edx变量中。这些在alg.go文件中使用,以选择计算机体系结构本身支持的适当哈希算法。

好的,让我们继续检查代码的另一部分。

// if there is an _cgo_init, call it.
MOVQ    _cgo_init(SB), AX
TESTQ    AX, AX
JZ    needtls
// g0 already in DI
MOVQ    DI, CX    // Win64 uses CX for first parameter
MOVQ    $setg_gcc<>(SB), SI
CALL    AX

// update stackguard after _cgo_init
MOVQ    $runtime·g0(SB), CX
MOVQ    (g_stack+stack_lo)(CX), AX
ADDQ    $const__StackGuard, AX
MOVQ    AX, g_stackguard0(CX)
MOVQ    AX, g_stackguard1(CX)

CMPL    runtime·iswindows(SB), $0
JEQ ok

该片段仅在cgo启用时执行。

下一个代码片段负责设置TLS。

needtls:
    // skip TLS setup on Plan 9
    CMPL    runtime·isplan9(SB), $1
    JEQ ok
    // skip TLS setup on Solaris
    CMPL    runtime·issolaris(SB), $1
    JEQ ok

    LEAQ    runtime·tls0(SB), DI
    CALL    runtime·settls(SB)

    // store through it, to make sure it works
    get_tls(BX)
    MOVQ    $0x123, g(BX)
    MOVQ    runtime·tls0(SB), AX
    CMPQ    AX, $0x123
    JEQ 2(PC)
    MOVL    AX, 0    // abort

我们之前已经提到过TLS。现在,是时候了解它是如何实现的了

内部TLS实施

如果仔细看一下前面的代码片段,您将很容易理解实际工作中仅有的几行。

LEAQ    runtime·tls0(SB), DI
CALL    runtime·settls(SB)

当您的操作系统不支持TLS设置时,所有其他所有内容都将用于跳过TLS设置,并检查TLS是否正常工作。上面的两行将runtime·tls0变量的地址存储在DI寄存器中并调用该runtime·settls函数。该功能的代码如下所示。

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
    ADDQ    $8, DI    // ELF wants to use -8(FS)

    MOVQ    DI, SI
    MOVQ    $0x1002, DI    // ARCH_SET_FS
    MOVQ    $158, AX    // arch_prctl
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS    2(PC)
    MOVL    $0xf1, 0xf1  // crash
    RET

从注释中,我们可以了解到此函数进行arch_prctl系统调用并ARCH_SET_FS 作为参数传递。我们还可以看到,该系统调用为FS段寄存器设置了基础。在我们的例子中,我们将TLS设置为指向runtime·tls0变量。

您还记得我们在主函数的汇编代码开头看到的指令吗?

0x0000 00000 (test.go:3)    MOVQ    (TLS),CX

前面我们已经解释过,它将runtime.g结构实例的地址加载到CX寄存器中。此结构描述了当前结构,goroutine并存储在线程本地存储中。现在,我们可以找到并了解如何将此指令转换为机器汇编程序。如果打开先前创建的disassembly.txt文件并查找该main.main函数,则其中的第一条指令应如下所示。

400c00:       64 48 8b 0c 25 f0 ff    mov    %fs:0xfffffffffffffff0,%rcx

本指令(%fs:0xfffffffffffffff0)中的冒号代表分段寻址(您可以在本教程中阅读更多内容)。

返回开始顺序

最后,让我们看一下runtime.rt0_go函数的最后两部分。

// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ    runtime·g0(SB), CX
MOVQ    CX, g(BX)
LEAQ    runtime·m0(SB), AX
// save m->g0 = g0
MOVQ    CX, m_g0(AX)
// save m0 to g0->m
MOVQ    AX, g_m(CX)

在这里,我们将TLS地址加载到BX寄存器中,并将runtime·g0变量的地址保存在TLS中。我们还初始化runtime.m0变量。如果runtime.g0代表root goroutine,则runtime.m0对应于用于运行root的根操作系统线程goroutine。我们可能需要仔细看看runtime.g0runtime.m0结构在即将到来的博客文章。

开始序列的最后一部分将初始化参数并调用不同的函数,但这是单独讨论的主题。因此,我们了解了引导过程的内部机制,并了解了如何实现堆栈。为了前进,我们需要分析开始序列的最后一部分,这将是我们下一篇博客文章的主题。

以上是关于golang 以脚本运行Go程序的主要内容,如果未能解决你的问题,请参考以下文章

golang 以脚本运行Go程序

Golang写的程序注入一些版本信息,Debug速度自然快几倍!

图解Golang的内存分配

Golang内部构件,第5部分:运行时引导程序

Golang入门到项目实战 golang中的if语句

Golang - “go run main.go”和编译之间的区别