What is a managed language?
What is a Managed Language?
A managed language, unlike an unmanaged language—which performs operations without significantly deviating from the logic authored by the programmer—is a language that executes tasks such as GC (Garbage Collection), runtime optimization, green threads, and concurrency handling at runtime, thereby relieving the user from the necessity of performing hazardous low-level management.
While such languages offer the advantage of allowing developers to immerse themselves in development by focusing solely on business logic, they may occasionally necessitate sophisticated runtime tuning, as the actual program execution may differ from the programmer’s intuition.
First, we will examine the Go language, which remains the most faithful to a minimalist philosophy among managed languages and produces transparent assembly.
Binary Structure of the Go Language
| .text | .data | .gopclntab, .typelink, etc. |
|---|---|---|
| Executable machine code | Data to be stored | Language runtime sections |
Because the Go language does not translate code into machine language on a 1:1 basis as input by the user, 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 assists developers in avoiding manual management.
Examining the main Function in Go
To begin, 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}
Subsequently, build it as follows:
1go build main.go
Go provides the go tool for facilitate easier low-level debugging.
To view only the assembly corresponding to the main function within the main package using go tool, enter the following command:
1go tool objdump -s "main\.main" ./main
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)
- After comparing whether the current thread has been entered using CMPQ, it jumps to Entrypoint 0x468f95 if the condition is met.
- The entry point is pushed onto the stack using
PUSHQ BP. - By designating the start of the stack at the beginning of the function in the SP register (where data was most recently loaded), the entry point for local variable references is fixed.
- Subsequently, 16 bytes of local variable stack space are reserved (
SUBQ $0x10, SP), and NOPL is utilized to fill multiple bytes for CPU cache alignment. - The Go Runtime locks the output of the string buffer by calling
runtime.printlock(SB). - Using the LEAQ instruction, the starting address of the allocated string is stored in AX, the accumulator register used for data storage among general-purpose registers.
- Then, the string length of 11 is stored in the BX register, which is used for arithmetic assistance and temporary data storage (
MOVL $0xb, BX). - Accumulator information is output to SB via
runtime.printstring(SB). - A single blank line is also written to SB using
runtime.printnl(SB). - The string buffer is released via
runtime.printunlock(SB). - The 16-byte stack memory borrowed is returned with
ADDQ $0x10, SP. Since the entry point was initially provided by being pushed onto the stack, the entry point is now removed from the stack usingPOPQ BP, followed by a return signal. - Afterward, as expected from a managed language,
runtime.morestack_noctxt.abi0(SB)is called to allocate sufficient stack space and set up the runtime, including GC. - Execution moves to the managed
main.main(SB)address.
As demonstrated, the assembly for the business logic is quite clear, with only lightweight runtime management added.
Without Optimization
The aforementioned form is the result of the Go compiler automatically inlining and optimizing 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:
1 go build -gcflags="-l" main.go
Printing the results in the shell reveals redundant assembly code.
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)
27yjlee@elegant:~/compare-assembly/go$ go tool objdump -s "main\.sayHello" ./main
28TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
29 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
30 main.go:3 0x468f64 7636 JBE 0x468f9c
31 main.go:3 0x468f66 55 PUSHQ BP
32 main.go:3 0x468f67 4889e5 MOVQ SP, BP
33 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
34 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
35 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
36 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
37 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
38 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
39OVQ 0x20(SP), AX
40 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
41 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
42 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
43 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
44 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
45 main.go:5 0x468f9a 5d POPQ BP
46 main.go:5 0x468f9b c3 RET
47 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
48 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
49 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
50 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
51 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
52 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
53yjlee@elegant:~/compare-assembly/go$
In essence, it has been confirmed that the compiler optimizes targets such as these redundant operations and inefficient loop unrolling.
Next Session
In the next session, we will cover the if and switch statements in the Go language. If time permits in the future, we also plan to analyze the Go runtime sections.