GoSuda

Runtime Architecture of Managed Languages and Analysis of Go Binary AMD64 Assembly

By Lee Yunjin views ...

What Is a Managed Language?

A managed language is a language that, unlike unmanaged languages (in which the programmer's code is executed with little deviation from the written logic), executes GC, runtime optimizations, green threads, concurrency handling, and similar mechanisms within the runtime, so that the user does not need to perform dangerous low-level management. Languages of this kind have the advantage that developers can focus on business logic and immerse themselves in development; on the other hand, the programmer's intuition and the actual behavior of the program may diverge, so fine-grained runtime tuning is sometimes required. First, we will examine the Go language, which is the most faithful to a minimalist philosophy among managed languages and whose assembly is straightforward.

Binary Structure of Go Language

.text.data.gopclntab, .typelink, etc.
Machine code to be runData to be storedLanguage runtime sections

Because the Go language does not translate user input into machine code on a strict 1:1 basis, the logic in the .text section is closely related to the language runtime sections. In addition, functions such as runtime.printnl() that the user did not write explicitly are added to the assembly of the .text section. Through this automatic code insertion, the Go language helps developers be freed from manual management.

Viewing Only the main Function in Go

First, let us write a simple example source file main.go and examine main on an AMD64 machine.

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

Then build it as follows.

1go build main.go
2

Go supports the go tool for convenient low-level debugging. To view only the assembly of the main function in the main package, enter the following command.

1go tool objdump -s "main\.main" ./main
2

Assembly

 1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
 2  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:7             0x468f64                762f                    JBE 0x468f95
 4  main.go:7             0x468f66                55                      PUSHQ BP
 5  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
 6  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
 7  main.go:8             0x468f6e                90                      NOPL
 8  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
 9  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
10  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
11  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
12  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
13  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
14  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
15  main.go:9             0x468f93                5d                      POPQ BP
16  main.go:9             0x468f94                c3                      RET
17  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
18  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)
19
  • The CMPQ instruction compares the stack guard value inside the goroutine control block register (R14) with the current stack pointer (SP) to check whether there is enough space for the current goroutine stack frame. If the space is insufficient, it jumps (JBE) to address 0x468f95, the entry point for stack expansion.
  • PUSHQ BP pushes the previous base pointer onto the stack to save it.
  • The current stack pointer (SP) is copied into the base pointer (BP) register, fixing the stack reference point at the start of the function.
  • Afterward, 16 bytes of local variable stack space are allocated (SUBQ $0x10, SP), and NOPL is used to fill with a no-op instruction to perform CPU cache alignment.
  • To synchronize the internal string standard output in the Go runtime, runtime.printlock(SB) is called to acquire the lock.
  • The LEAQ instruction stores the starting address of the constant string ("Hello World") into AX, the register used as the first parameter according to the Go ABI specification, among the general-purpose registers.
  • Next, the value representing the string length is stored in BX, the second parameter register (MOVL $0xb, BX, that is, 11 in decimal).
  • runtime.printstring(SB) is called to print to the console based on the AX (data address) and BX (length) information passed in.
  • runtime.printnl(SB) is called to handle the line break.
  • Since output is complete, the lock is released through runtime.printunlock(SB).
  • ADDQ $0x10, SP restores the 16-byte stack memory that was allocated.
  • POPQ BP restores the previous base pointer.
  • RET returns control to the point from which the function was called.
  • If the initial stack check found insufficient space, runtime.morestack_noctxt.abi0(SB) at address 0x468f95 is called to dynamically expand the stack runtime, in keeping with managed language practice.
  • Once stack expansion is complete, control returns (JMP) to the entry point of main.main(SB). As shown, the assembly of the business logic is quite clear, with only a thin layer of runtime management added.

When Optimization Is Absent

The above form is the result of the Go compiler automatically inlining and optimizing the two separate functions. However, for the purposes of study, we will prevent sayHello from being inlined in this case. To do this, compile the source with the following flag.

1go build -gcflags="-l" main.go
2

When the output is printed in the shell, duplicated assembly code can be found.

 1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
 2
 3go tool objdump -s "main\.sayHello" ./main
 4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
 5  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
 6  main.go:3             0x468f64                7636                   JBE 0x468f9c
 7  main.go:3             0x468f66                55                     PUSHQ BP
 8  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
 9  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
10  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
11  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
12  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
13  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
14  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
15  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
16  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
17  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
18  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
19  main.go:5             0x468f9a                5d                     POPQ BP
20  main.go:5             0x468f9b                c3                     RET
21  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
22  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
23  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
24  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
25  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
26  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)
27

When inlining is disabled, MOVQ instructions are inserted that reload the values into stack pointer offsets such as 0x20(SP) to preserve the parameters (AX, BX) in accordance with the function call convention. In other words, it has been confirmed that the target of compiler optimization is these unnecessary memory movement operations and call overhead.

Next Time

Next time, we will cover the if and switch statements in Go. If time permits in the future, we also plan to analyze the Go runtime sections.