Skip to content

Commit d31c014

Browse files
authored
managed-languages-intro.md 업데이트
1 parent 081b785 commit d31c014

1 file changed

Lines changed: 34 additions & 79 deletions

File tree

root/blog/managed-languages-intro.md

Lines changed: 34 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3525
package main
3626

@@ -41,23 +31,20 @@ func sayHello(msg string) {
4131
func main() {
4232
sayHello("Hello World")
4333
}
44-
```
4534

35+
```
4636
이후 이렇게 빌드한다.
47-
4837
```bash
4938
go build main.go
50-
```
51-
52-
Go는 쉬운 저수준 디버깅을 위해 `go tool`을 지원한다.
53-
`go tool`에서 메인 패키지에서 메인 함수만큼의 어셈블리만 보기 위해서 이 구문을 입력한다.
5439

40+
```
41+
Go는 쉬운 저수준 디버깅을 위해 go tool을 지원한다.
42+
go tool에서 메인 패키지에서 메인 함수만큼의 어셈블리만 보기 위해서 이 구문을 입력한다.
5543
```bash
5644
go tool objdump -s "main\.main" ./main
57-
```
5845

46+
```
5947
### 어셈블리
60-
6148
```go
6249
TEXT 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
11194
yjlee@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 $0x10, 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 $0x10, 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

Comments
 (0)