1. Flow of Golang code being run up by the OS
The go source code is first compiled into an executable file by go build, which is an ELF format executable file on linux platform, and the compilation stage will go through three processes: compiler, assembler, and linker to finally generate an executable file.
*.gosource code is generated as plan9 assembly code for
*.sby the go compiler, the go compiler entry is compile/internal/gc/main.go file for the main function.
- Assembler: The go assembler converts the compiler-generated
*.sassembly language into machine code and writes the final target program
*.ofile, src/cmd/internal/obj package implements the go assembler.
- Linker: The assembler generates a
*.otarget file that is linked to obtain the final executable, src/cmd/link/internal/ld package implements the linker.
After the go source code has been generated as an executable through the above steps, the binary file will go through the following stages when loaded and run by the operating system.
- Reading the executable from disk into memory.
- Creating the process and the main thread.
- Allocating stack space for the main thread.
- copying the parameters entered by the user on the command line to the main thread’s stack.
- placing the main thread into the operating system’s run queue to wait to be scheduled to execute it.
2. Golang program startup flow analysis
2.1. Analyze the program startup process through gdb debugging
Here a simple go program is debugged in a single step to analyze its startup process.
Compile the program and use gdb to debug it. When debugging with gdb, first set a breakpoint at the program entry point, and then perform single-step debugging to see the code execution flow during the program startup.
By single-step debugging, you can see that the program entry function is at line 8 of the
runtime/rt0_linux_amd64.s file, which eventually executes the
CALL runtime-mstart(SB) instruction and outputs “hello world” and then the program exits. .
The function calls in the startup process flow are shown below.
2.2. golang startup process analysis
The previous section has seen through gdb debugging golang program in the startup process will execute a series of assembly instructions, this section will specifically analyze the meaning of each instruction in the process of starting the program, to understand these to understand the golang program in the startup process of the operations performed.
The first execution is line 8,
JMP _rt0_amd64, which runs under the amd64 platform, and the
_rt0_amd64 function is located in the file
_rt0_amd64 function saves the argc and argv arguments to the DI and SI registers and then jumps to the
rt0_go function, the main purpose of the
rt0_go function is as follows.
- Copy argc, argv arguments to the main thread stack.
- Initialize the global variable g0, allocate about 64K stack space on the main thread stack for g0, and set the stackguard0, stackguard1, stack fields of g0.
- Execute the CPUID instruction to probe for CPU information.
- Execute the nocpuinfo block to determine if the cgo needs to be initialized.
- Execute the needtls code block to initialize tls and m0.
- Execute ok block, first bind m0 to g0, then call
runtime-argsfunction to handle process parameters and environment variables, call
runtime-osinitfunction to initialize cpu count, call
runtime-schedinitto initialize scheduler, call
runtime-newprocto create the first goroutine to execute the main function, call
runtime-mstartto start the main thread, which will execute the first goroutine to run the main function, and will block here until the process exits.
After the execution of the above instructions, the process memory space layout is as follows.
Then start executing instructions to get cpu information and related to cgo initialization, this code can be ignored for now.
The following is the execution of
needtls code block, initialize tls and m0, tls is the thread local storage, in the golang program running process, each m needs to be associated with a work thread, so how does the work thread know its associated m, at this time will use the thread local storage, thread local storage is the thread private global variable, through the thread local storage can be for Each thread can initialize a private global variable m, and then each thread can use the same global variable name to access a different m structure object. As will be analyzed later, each worker thread m actually uses thread-local storage to implement a private global variable for that worker thread that points to an instance of the m structure object just before it is created and enters the scheduling loop.
In the code analysis later, you will often see calls to the
getg function. The
getg function will fetch the currently running g from the thread local store, in this case the g0 associated with m.
The tls address will be written to m0, and m0 will be bound to g0, so you can get g0 directly from tls.
Continuing with the ok code block, the main logic is.
- Bind m0 to g0 and start the main thread.
- Calling the
runtime-osinitfunction to initialize the number of cpu’s, the scheduler needs to know how many CPU cores the system currently has when it initializes.
- Calling the
runtime-schedinitfunction initializes the m0 and p objects and also sets the maxmcount member of the global variable sched to 10000, limiting the maximum number of OS threads that can be created out of work to 10000.
runtime-newprocto create a goroutine for the main function.
runtime-mstartto start the main thread and execute the main function.
The process memory space layout at this point is shown below.
2.3. View ELF binary file structure
You can view the structure of the ELF binary file by using the readelf command. You can see the contents of the code area and data area in the binary file, global variables are stored in the data area and functions are stored in the code area.
This article mainly introduces the key code in the Golang program startup process, the main code of the startup process is written through Plan9 assembly, if you have not done the underlying related things look very difficult, the author of some of the details are not fully understood, if interested in discussing some of the details of the implementation in private, there are some hard-coded numbers as well as the operating system and hardware The specification is relatively difficult to understand. The analysis of several components in Golang runtime will be written one after another.