What is a managed language?
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 executed | Data to be stored | Language 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.