1. Flow of Golang code being run up by the OS
1.1. Compilation
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.
- Compiler:
*.go
source code is generated as plan9 assembly code for*.s
by 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
*.s
assembly language into machine code and writes the final target program*.o
file, src/cmd/internal/obj package implements the go assembler. - Linker: The assembler generates a
*.o
target file that is linked to obtain the final executable, src/cmd/link/internal/ld package implements the linker.
1.2. Running
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.
main.go
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.
src/runtime/rt0_linux_amd64.s
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 src/runtime/asm_amd64.s
.
The _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-args
function to handle process parameters and environment variables, callruntime-osinit
function to initialize cpu count, callruntime-schedinit
to initialize scheduler, callruntime-newproc
to create the first goroutine to execute the main function, callruntime-mstart
to 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-osinit
function 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-schedinit
function 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. - Call
runtime-newproc
to create a goroutine for the main function. - call
runtime-mstart
to 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.
3. Summary
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.