@@ -9,28 +9,18 @@ path: /blog/posts/managed-languages-intro-656b7470
99---
1010
1111## 매니지드 언어란 무엇인가?
12-
13- 매니지드 언어는 언매니지드 언어, 즉 프로그래머가 짠 로직에서 크게 벗어나지 않고 수행만 하는 언어와 달리 GC, 런타임 최적화, 그린 쓰레드, 동시성 처리 등을 런타임에서 실행하여서 사용자가 위험한 저수준 관리를 할 필요 없게 만들어 주는 언어이다.
14-
12+ 매니지드 언어는 언매니지드 언어, 즉 프로그래머가 짠 로직에서 크게 벗어나지 않고 수행만 하는 언어와 달리 GC, 런타임 최적화, 그린 쓰레드, 동시성 처리 등을 런타임에서 실행하여 사용자가 위험한 저수준 관리를 할 필요 없게 만들어 주는 언어이다.
1513이러한 언어의 경우, 비즈니스 로직에만 집중하여 개발에 몰입할 수 있다는 장점이 있지만, 반면 프로그래머의 직관과 실제 프로그램이 다르게 동작할 수 있어서 정교한 런타임 튜닝이 필요할 때도 있다.
16-
17- 먼저, 매니지드 언어 중에 가장 미니멀리스트 철학에 충실하고 어셈블리가 정직한 Go언어를 보도록 하겠다.
18-
14+ 먼저, 매니지드 언어 중에 가장 미니멀리스트 철학에 충실하고 어셈블리가 정직한 Go 언어를 보도록 하겠다.
1915### Go 언어의 바이너리 구조
20-
21- | .text | .data | .gopclntab, .typelink 등 |
22- | ----------------- | ------------ | --------- |
16+ | .text | .data | .gopclntab, .typelink 등 |
17+ | ---| ---| ---|
2318| 실행될 기계어 코드 | 저장될 데이터 | 언어 런타임 섹션 |
24-
25- Go언어는 사용자가 입력한대로 1:1로 기계어 번역하지 않기 때문에, .text 섹션의 로직은 언어 런타임 섹션과도 긴밀하게 연관되어 있다.
26-
27- 또한, 사용자가 따로 작성하지 않은 rumtime.printnl()같은 함수들이 .text 섹션 어셈블리에 추가된다.
19+ Go 언어는 사용자가 입력한 대로 1:1로 기계어 번역하지 않기 때문에, .text 섹션의 로직은 언어 런타임 섹션과도 긴밀하게 연관되어 있다.
20+ 또한, 사용자가 따로 작성하지 않은 runtime.printnl() 같은 함수들이 .text 섹션 어셈블리에 추가된다.
2821이러한 자동적인 코드 삽입을 통해 Go 언어는 수동 관리로부터 개발자를 벗어나게 돕는다.
29-
3022### Go에서 main 함수 부분만 보기
31-
32- 우선, 간단한 예시 소스 ` main.go ` 를 작성해서 main부터 ** AMD64 머신에서** 보도록 하자.
33-
23+ 우선, 간단한 예시 소스 main.go를 작성해서 main부터 ** AMD64 머신에서** 보도록 하자.
3424``` go
3525package main
3626
@@ -41,23 +31,20 @@ func sayHello(msg string) {
4131func main () {
4232 sayHello (" Hello World" )
4333}
44- ```
4534
35+ ```
4636이후 이렇게 빌드한다.
47-
4837``` bash
4938go build main.go
50- ```
51-
52- Go는 쉬운 저수준 디버깅을 위해 ` go tool ` 을 지원한다.
53- ` go tool ` 에서 메인 패키지에서 메인 함수만큼의 어셈블리만 보기 위해서 이 구문을 입력한다.
5439
40+ ```
41+ Go는 쉬운 저수준 디버깅을 위해 go tool을 지원한다.
42+ go tool에서 메인 패키지에서 메인 함수만큼의 어셈블리만 보기 위해서 이 구문을 입력한다.
5543``` bash
5644go tool objdump -s " main\.main" ./main
57- ```
5845
46+ ```
5947### 어셈블리
60-
6148``` go
6249TEXT main.main (SB) /home/yjlee/compare-assembly/go /main.go
6350 main.go :7 0x468f60 493b6610 CMPQ SP , 0x10 (R14)
@@ -77,36 +64,32 @@ TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
7764 main.go :9 0x468f94 c3 RET
7865 main.go :7 0x468f95 e8e6afffff CALL runtime.morestack_noctxt .abi0 (SB)
7966 main.go :7 0x468f9a ebc4 JMP main.main (SB)
80- ```
81-
82- - 현재 쓰레드에 진입한지 CMPQ로 비교한 후, 맞다면 Entrypoint 0x468f95로 점프한다.
83- - 진입점을 ` PUSHQ BP ` 로 스택에 삽입한다.
84- - 가장 최근에 데이터가 적재된 레지스터 SP에 함수 시작 시 스택 시작 지점을 지정하여 지역 변수 참조 시의 진입점을 고정한다.
85- - 이후 16바이트만큼의 로컬 변수 스택을 예약하고 (` SUBQ $0x10, SP ` ), NOPL을 이용해서 여러 바이트를 채워 CPU 캐시 정렬을 한다.
86- - Go Runtime에서 스트링 버퍼의 출력 락을 ` runtime.printlock(SB) ` 를 호출하여 건다.
87- - LEAQ 명령을 이용해서 할당한 문자열의 시작 주소를 범용 레지스터 중 데이터 저장에 쓰는 누산기 주소인 AX에 저장한다.
88- - 이후 연산 보조 및 임시 데이터 저장에 쓰는 BX 레지스터에 문자열 길이 11을 저장한다. (` MOVL $0Xb, BX ` )
89- - runtime.printstring(SB)로 SB 쪽으로 누산기 정보를 출력한다.
90- - 한 줄 공백도 rumtime.printnl(SB)로 SB쪽으로 쓴다.
91- - 스트링 버퍼를 runtime.printunlock(SB)로 해제한다.
92- - ADDQ $0x10, SP로 빌린 16바이트 스택 메모리를 돌려준다. - 처음에 진입점을 스택에 넣어 알려 줬으니 이제 POPQ BP로 스택에서 진입점을 뺀 후 반환 시그널을 준다.
93- - 이후 runtime.morestack_noctxt.abi0(SB)로 매니지드 언어답게 충분한 스택을 할당, GC 등의 런타임을 셋업한다.
94- - 관리된 main.main(SB) 주소로 이동한다.
9567
68+ ```
69+ * 현재 고루틴 스택 프레임 공간이 충분한지 고루틴 제어 블록 레지스터(R14) 내부의 스택 가드 값과 현재 스택 포인터(SP)를 CMPQ로 비교한 후, 부족하다면 스택 확장을 위한 Entrypoint인 0x468f95 주소로 점프(JBE)한다.
70+ * 이전 베이스 포인터를 저장하기 위해 PUSHQ BP로 스택에 삽입한다.
71+ * 베이스 포인터(BP) 레지스터에 현재 스택 포인터(SP)를 복사하여 함수 시작 시의 스택 기준점을 고정한다.
72+ * 이후 16바이트만큼의 로컬 변수 스택 공간을 할당하고 (SUBQ $0x10, SP), NOPL을 이용해 가상 명령어를 채워 CPU 캐시 정렬을 수행한다.
73+ * Go 런타임에서 내부 문자열 표준 출력의 동기화를 위해 runtime.printlock(SB)를 호출하여 락을 건다.
74+ * LEAQ 명령을 이용해 상수로 할당된 문자열("Hello World")의 시작 주소를 범용 레지스터 중 Go ABI 규격에 따라 첫 번째 매개변수로 쓰이는 AX에 저장한다.
75+ * 이후 문자열 길이를 나타내는 값을 두 번째 매개변수 레지스터인 BX에 저장한다. (MOVL $0xb, BX, 즉 10진수로 11)
76+ * runtime.printstring(SB)를 호출하여 전달된 AX(데이터 주소)와 BX(길이) 정보를 기반으로 콘솔에 출력한다.
77+ * 줄바꿈 처리를 위해 runtime.printnl(SB)를 호출한다.
78+ * 출력이 완료되었으므로 runtime.printunlock(SB)를 통해 락을 해제한다.
79+ * ADDQ $0x10, SP로 할당했던 16바이트 스택 메모리를 복구한다.
80+ * POPQ BP로 기존의 베이스 포인터를 복원한다.
81+ * RET를 통해 함수를 호출한 지점으로 제어권을 반환한다.
82+ * 만약 최초 스택 검사에서 공간이 부족했다면 0x468f95 주소의 runtime.morestack_noctxt.abi0(SB)를 호출하여 매니지드 언어답게 스택 런타임을 동적으로 확장한다.
83+ * 스택 확장이 완료되면 다시 main.main(SB)의 진입점으로 복귀(JMP)한다.
9684보기와 같이 비즈니스 로직의 어셈블리는 꽤 명확하고, 얇은 런타임 관리만 덧붙여진 형태이다.
97-
9885### 최적화가 없을 때
99-
100- 위의 형태는 Go 컴파일러에서 따로 떨어져 있는 두 함수를 자동으로 인라이닝해 최적화한 결과이다. 그러나, 우리는 학습을 위해 이 경우에는 ` sayHello ` 를 인라이닝하지 않게 할 것입니다.
101-
86+ 위의 형태는 Go 컴파일러에서 따로 떨어져 있는 두 함수를 자동으로 인라이닝해 최적화한 결과이다. 그러나, 우리는 학습을 위해 이 경우에는 sayHello를 인라이닝하지 않게 할 것입니다.
10287이렇게 하기 위해 다음 플래그로 소스를 컴파일한다.
103-
10488``` bash
105- go build -gcflags=" -l" main.go
106- ```
89+ go build -gcflags=" -l" main.go
10790
91+ ```
10892쉘에서 결과를 찍어보면 중복되는 어셈블리가 발견된다.
109-
11093``` bash
11194yjlee@elegant:~ /compare-assembly/go$ go build -gcflags=" -l" main.go
11295
@@ -134,37 +117,9 @@ TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
134117 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
135118 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
136119 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
137- yjlee@elegant:~ /compare-assembly/go$ go tool objdump -s " main\.sayHello" ./main
138- TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
139- main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
140- main.go:3 0x468f64 7636 JBE 0x468f9c
141- main.go:3 0x468f66 55 PUSHQ BP
142- main.go:3 0x468f67 4889e5 MOVQ SP, BP
143- main.go:3 0x468f6a 4883ec10 SUBQ $0 x10, SP
144- main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
145- main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
146- main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
147- main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
148- main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
149- OVQ 0x20(SP), AX
150- main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
151- main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
152- main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
153- main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
154- main.go:5 0x468f96 4883c410 ADDQ $0 x10, SP
155- main.go:5 0x468f9a 5d POPQ BP
156- main.go:5 0x468f9b c3 RET
157- main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
158- main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
159- main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
160- main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
161- main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
162- main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
163- yjlee@elegant:~ /compare-assembly/go$
164- ```
165-
166- 즉 컴파일러가 최적화하는 것은 이러한 중복 연산, 비효율적인 루프 언롤링 등의 대상임이 확인되었다.
167120
121+ ```
122+ 인라이닝을 해제하면, 함수 호출 규격에 맞춰 매개변수(AX, BX)를 보존하기 위해 스택 포인터 오프셋인 0x20(SP) 등에 값을 다시 적재하는 MOVQ 연산들이 삽입된다.
123+ 즉 컴파일러가 최적화하는 것은 이러한 불필요한 메모리 이동 연산 및 호출 오버헤드의 대상임이 확인되었다.
168124### 다음 시간
169-
170125다음 시간에는 Go 언어에서의 if문, switch 문을 다루도록 하겠다. 추후 시간이 난다면 Go 런타임 섹션들도 분석할 예정이다.
0 commit comments