GoSuda

매니지드 언어란 무엇인가?

By Lee Yunjin
views ...

매니지드 언어란 무엇인가?

매니지드 언어는 언매니지드 언어, 즉 프로그래머가 짠 로직에서 크게 벗어나지 않고 수행만 하는 언어와 달리 GC, 런타임 최적화, 그린 쓰레드, 동시성 처리 등을 런타임에서 실행하여 사용자가 위험한 저수준 관리를 할 필요 없게 만들어 주는 언어이다. 이러한 언어의 경우, 비즈니스 로직에만 집중하여 개발에 몰입할 수 있다는 장점이 있지만, 반면 프로그래머의 직관과 실제 프로그램이 다르게 동작할 수 있어서 정교한 런타임 튜닝이 필요할 때도 있다. 먼저, 매니지드 언어 중에 가장 미니멀리스트 철학에 충실하고 어셈블리가 정직한 Go 언어를 보도록 하겠다.

Go 언어의 바이너리 구조

.text.data.gopclntab, .typelink 등
실행될 기계어 코드저장될 데이터언어 런타임 섹션
Go 언어는 사용자가 입력한 대로 1:1로 기계어 번역하지 않기 때문에, .text 섹션의 로직은 언어 런타임 섹션과도 긴밀하게 연관되어 있다.
또한, 사용자가 따로 작성하지 않은 runtime.printnl() 같은 함수들이 .text 섹션 어셈블리에 추가된다.
이러한 자동적인 코드 삽입을 통해 Go 언어는 수동 관리로부터 개발자를 벗어나게 돕는다.

Go에서 main 함수 부분만 보기

우선, 간단한 예시 소스 main.go를 작성해서 main부터 AMD64 머신에서 보도록 하자.

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

이후 이렇게 빌드한다.

1go build main.go
2

Go는 쉬운 저수준 디버깅을 위해 go tool을 지원한다. go tool에서 메인 패키지에서 메인 함수만큼의 어셈블리만 보기 위해서 이 구문을 입력한다.

1go tool objdump -s "main\.main" ./main
2

어셈블리

 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
  • 현재 고루틴 스택 프레임 공간이 충분한지 고루틴 제어 블록 레지스터(R14) 내부의 스택 가드 값과 현재 스택 포인터(SP)를 CMPQ로 비교한 후, 부족하다면 스택 확장을 위한 Entrypoint인 0x468f95 주소로 점프(JBE)한다.
  • 이전 베이스 포인터를 저장하기 위해 PUSHQ BP로 스택에 삽입한다.
  • 베이스 포인터(BP) 레지스터에 현재 스택 포인터(SP)를 복사하여 함수 시작 시의 스택 기준점을 고정한다.
  • 이후 16바이트만큼의 로컬 변수 스택 공간을 할당하고 (SUBQ $0x10, SP), NOPL을 이용해 가상 명령어를 채워 CPU 캐시 정렬을 수행한다.
  • Go 런타임에서 내부 문자열 표준 출력의 동기화를 위해 runtime.printlock(SB)를 호출하여 락을 건다.
  • LEAQ 명령을 이용해 상수로 할당된 문자열("Hello World")의 시작 주소를 범용 레지스터 중 Go ABI 규격에 따라 첫 번째 매개변수로 쓰이는 AX에 저장한다.
  • 이후 문자열 길이를 나타내는 값을 두 번째 매개변수 레지스터인 BX에 저장한다. (MOVL $0xb, BX, 즉 10진수로 11)
  • runtime.printstring(SB)를 호출하여 전달된 AX(데이터 주소)와 BX(길이) 정보를 기반으로 콘솔에 출력한다.
  • 줄바꿈 처리를 위해 runtime.printnl(SB)를 호출한다.
  • 출력이 완료되었으므로 runtime.printunlock(SB)를 통해 락을 해제한다.
  • ADDQ $0x10, SP로 할당했던 16바이트 스택 메모리를 복구한다.
  • POPQ BP로 기존의 베이스 포인터를 복원한다.
  • RET를 통해 함수를 호출한 지점으로 제어권을 반환한다.
  • 만약 최초 스택 검사에서 공간이 부족했다면 0x468f95 주소의 runtime.morestack_noctxt.abi0(SB)를 호출하여 매니지드 언어답게 스택 런타임을 동적으로 확장한다.
  • 스택 확장이 완료되면 다시 main.main(SB)의 진입점으로 복귀(JMP)한다. 보기와 같이 비즈니스 로직의 어셈블리는 꽤 명확하고, 얇은 런타임 관리만 덧붙여진 형태이다.

최적화가 없을 때

위의 형태는 Go 컴파일러에서 따로 떨어져 있는 두 함수를 자동으로 인라이닝해 최적화한 결과이다. 그러나, 우리는 학습을 위해 이 경우에는 sayHello를 인라이닝하지 않게 할 것입니다. 이렇게 하기 위해 다음 플래그로 소스를 컴파일한다.

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

쉘에서 결과를 찍어보면 중복되는 어셈블리가 발견된다.

 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

인라이닝을 해제하면, 함수 호출 규격에 맞춰 매개변수(AX, BX)를 보존하기 위해 스택 포인터 오프셋인 0x20(SP) 등에 값을 다시 적재하는 MOVQ 연산들이 삽입된다. 즉 컴파일러가 최적화하는 것은 이러한 불필요한 메모리 이동 연산 및 호출 오버헤드의 대상임이 확인되었다.

다음 시간

다음 시간에는 Go 언어에서의 if문, switch 문을 다루도록 하겠다. 추후 시간이 난다면 Go 런타임 섹션들도 분석할 예정이다.