GoSuda

What is a managed language?

By Lee Yunjin
views ...

What is a Managed Language?

A managed language is a language that, unlike unmanaged languages—which perform only the logic programmed by the programmer without significant deviation—executes processes such as GC, runtime optimization, green threads, and concurrency handling at runtime, thereby eliminating the need for users to engage in hazardous low-level management. While such languages offer the advantage of allowing developers to focus exclusively on business logic, they can also result in the program behaving differently from the programmer’s intuition, occasionally necessitating sophisticated runtime tuning. First, we will examine the Go language, which is the most faithful to the minimalist philosophy among managed languages and possesses transparent assembly.

Binary Structure of the Go Language

.text.data.gopclntab, .typelink, etc.
Machine code to be executedData to be storedLanguage runtime sections
Because the Go language does not perform a 1:1 translation of machine code exactly as the user inputs it, the logic in the .text section is closely linked to the language runtime sections.
Furthermore, functions such as runtime.printnl(), which are not explicitly written by the user, are added to the .text section assembly.
Through this automatic code insertion, the Go language helps developers avoid manual management.

Observing Only the Main Function in Go

First, let us create a simple example source file, main.go, and examine it starting from 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 easy low-level debugging. To view only the assembly for the main function within the main package using the go tool, input this 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 current stack pointer (SP) is compared with the stack guard value within the goroutine control block register (R14) using CMPQ to check if there is sufficient goroutine stack frame space; if insufficient, it jumps (JBE) to the address 0x468f95, which is the entry point for stack expansion.
  • The previous base pointer is saved by PUSHQ BP to the stack.
  • The current stack pointer (SP) is copied to the base pointer (BP) register to fix the stack reference point at the start of the function.
  • Subsequently, 16 bytes of local variable stack space are allocated (SUBQ $0x10, SP), and NOPL is used to fill virtual instructions for CPU cache alignment.
  • runtime.printlock(SB) is called to acquire a lock for the synchronization of internal string standard output in the Go runtime.
  • Using the LEAQ instruction, the starting address of the constant string ("Hello World") is stored in AX, which is used as the first parameter according to the Go ABI specification among general-purpose registers.
  • Then, the value representing the string length is stored in the second parameter register, BX. (MOVL $0xb, BX, i.e., 11 in decimal)
  • runtime.printstring(SB) is called to print to the console based on the AX (data address) and BX (length) information provided.
  • runtime.printnl(SB) is called for newline processing.
  • Since the output is complete, the lock is released via runtime.printunlock(SB).
  • The 16-byte stack memory that was allocated is restored with ADDQ $0x10, SP.
  • The existing base pointer is restored with POPQ BP.
  • Control is returned to the point where the function was called via RET.
  • If there was insufficient space during the initial stack check, runtime.morestack_noctxt.abi0(SB) at address 0x468f95 is called to dynamically expand the stack runtime, as is characteristic of a managed language.
  • Once stack expansion is complete, it returns (JMP) to the entry point of main.main(SB). As demonstrated, the assembly of the business logic is quite clear, with only lightweight runtime management added.

In the Absence of Optimization

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

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

When checking the results in the shell, redundant assembly is discovered.

 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 operations are inserted to reload values into stack pointer offsets such as 0x20(SP) to preserve parameters (AX, BX) according to the function call specification. In other words, it has been confirmed that what the compiler optimizes are these unnecessary memory movement operations and call overheads.

Next Time

In the next session, we will cover if and switch statements in the Go language. If time permits in the future, I plan to analyze the Go runtime sections as well.