Golang program startup process

Posted by kestasjk on Sun, 28 Nov 2021 18:14:12 +0100

go run main.go a go program starts. However, how does the operating system execute the go code behind this? What does go do to run the user's main function?

One compilation

  • go build main.go

The go code we write is compiled into an executable file to be directly executed on the machine. It is on the linux platform ELF Format executable file, linux can directly execute this file.

  • Compiler: generate. s assembly code from go code. plan9 assembly is used in go
  • Assembly: convert assembly code into machine code, i.e. object program. o file
  • Linker: merge and link multiple. o files to get the final executable
graph LR
    0(Write code)--go program--> 1(compiler)--Assembly code --> 2(Assembler)--.o Target program-->3(Linker)--Executable file-->4(end)

II. Operating system loading

  • ./main

After the executable file is generated through the above steps, the binary file will go through the following stages when it is loaded and run by the operating system:

  1. Read the executable program into memory from disk;
  2. Create process and main thread;
  3. Allocate stack space for the main thread;
  4. Copy the parameters entered by the user on the command line to the stack of the main thread;
  5. Put the main thread into the running queue of the operating system and wait for the scheduled execution to run;

START_ Thread (elf_ex, regs, elf_entry, bprm - > P) the starting thread passed elf_ Entry parameter, which is the entry address of the program.

This Elf_ The entry is written in the header of the elf executable

$ 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:               0x45d430
  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:         25
  Section header string table index: 3

And by decompiling the executable file, you can see that the address corresponds to_ rt0_amd64_linux.

$ objdump ./main -D > tmp
$ grep tmp '45d430'
000000000045d430 <_rt0_amd64_linux>:
  45d430:    e9 2b c4 ff ff           jmpq   459860 <_rt0_amd64>

From then on, we entered the start-up process of Go program

Go program start

Go program Start position , store the entry parameters on the stack in the register, and then jump to rt0_go startup function

TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI    // argc
    LEAQ    8(SP), SI    // argv
    JMP    runtime·rt0_go(SB)

rt0_ The go code is relatively long and can be divided into two parts. The first part is system parameter acquisition and runtime check. The second part is the core of Go program startup. Only the second part is introduced in detail here. The overall startup process is as follows


go runtime core:

  1. schedinit: initialize various runtime components, including the initialization of our scheduler, memory allocator and collector
  2. newproc: it is responsible for creating execution units that can be scheduled at runtime according to the main goroutine (i.e. main) entry address. Here, main is not the user's main function, but runtime.main
  3. mstart: start the scheduling cycle of the scheduler, and the entry method in the execution queue is G of runtime.main
TEXT runtime·rt0_go(SB),NOSPLIT,$0
    (...)
    // Scheduler initialization
    CALL    runtime·schedinit(SB)

    // Create a new goroutine to start the program
    MOVQ    $runtime·mainPC(SB), AX
    PUSHQ    AX
    PUSHQ    $0            // Parameter size
    CALL    runtime·newproc(SB)
    POPQ    AX
    POPQ    AX

    // Start this M, mstart should never return
    CALL    runtime·mstart(SB)
    (...)
    RET

shedinit includes the initialization of all core components

// src/runtime/proc.go
func schedinit() {
    _g_ := getg()
    (...)

    // Stack, memory allocator, scheduler related initialization
    sched.maxmcount = 10000    // Limit the maximum number of system threads
    stackinit()            // Initialize execution stack
    mallocinit()        // Initialize memory allocator
    mcommoninit(_g_.m)    // Initializes the current system thread
    (...)

    gcinit()    // Garbage collector initialization
    (...)

    // Create P
    // The number of P is determined by the number of CPU cores and GOMAXPROCS environment variable
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    procresize(procs)
    (...)
}

Execute runtime.main, mainly

  1. Start the sysmon thread for system background monitoring
  2. Execute init in runtime package
  3. Start gc
  4. The user package depends on the execution of init
  5. Execute the user main.mian method
// The main goroutine.
func main() {
    g := getg()

    ...
    // Maximum limit of execution stack: 1GB (64 bit system) or 250MB (32-bit system)
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }
    ...

    // Start system background monitoring (regular garbage collection, preemption scheduling, etc.)
    systemstack(func() {
        newm(sysmon, nil)
    })

    ...
    // Let goroute monopolize the current thread, 
    // For the usage of runtime.lockOSThread, see http://xiaorui.cc/archives/5320
    lockOSThread()

    ...
    // The init function inside the runtime package executes
    runtime_init() // must be before defer

    // Defer unlock so that runtime.Goexit during init does the unlock too.
    needUnlock := true
    defer func() {
        if needUnlock {
                unlockOSThread()
        }
    }()
    // Start GC
    gcenable()

    ...
    // init execution of user package
    main_init()
    ...

    needUnlock = false
    unlockOSThread()

    ...
    // Execute the main function of the user
    main_main()
    
    ...
    // sign out
    exit(0)
    for {
        var x *int32
        *x = 0
    }
}

summary

When starting a go program, first load the operating system and find the Go program startup entry through the address recorded in the Entry point address in the executable file:_ rt0_ amd64 -> rt0_go. rt0_ In go, the runtime of Go program is initialized first, including scheduler, stack, heap memory space initialization and garbage collector initialization. Finally, runtime.main is executed through newproc and mstart scheduling to complete a series of initialization processes, and then the user's main function is executed.

reference resources

  1. https://www.bookstack.cn/read...
  2. https://eddycjy.com/posts/go/...
  3. https://loulan.me/post/golang...
  4. https://juejin.cn/post/694250...

Topics: Go