Go 프로그램 부트스트랩
go 프로그램 시작 과정은 다음과 같다.
_rt0 (Entry Point)
↓
_main
↓
runtime.rt0_go (스케줄러 및 첫 고루틴 설정)
↓
runtime.main
├→ runtime.init
├→ main.init
└→ main.main (우리가 작성한 코드)
↓
exit(0)
핵심 개념은 다음과 같다.
- 우리가 작성하는 main.main()은 진짜 시작점이 아님
- Go 런타임이 먼저 초기화 작업 수행:
- 스케줄러 설정
- 메모리 할당자 초기화
- GC 설정
- 고루틴 스택 준비
진입점이 main 위에서 시작한다는 점이 중요하다. 진짜 시작점은 _rt0 이다. main()은 런타임 초기화의 끝 에 해당한다.
이게 왜 중요할까
panic과 defer의 내부 구조
panic은 함수가 아니다
grep -r "func panic\b" .
./src/builtin/builtin.go:func panic(v any)
./doc/go_spec.html:func panic(interface{})
grep -r "func gopanic\b" .
./src/runtime/panic.go:func gopanic(e any) {
panic은 built-in 함수로(컴파일러 내장) 실제 구현은 runtime.gopanic (어셈블리)에서 수행된다.
컴파일러가 panic() 호출을 runtime.gopanic() 호출로 변환한다.
gopanic 구현
// runtime/panic.go
func gopanic(e any) {
gp := getg() // 현재 고루틴 가져오기
if gp.m.curg != gp {
// 시스템 스택에서는 panic 불가
throw("panic on system stack")
}
// panic 체인에 추가
// defer 함수들 실행
// recover() 체크
// 스택 트레이스 출력
}
panic 실행 흐름
panic() 호출
↓
Run defers (역순으로 실행) → recover() 발견 시 복구
↓
Print stack traces (복구 안 되면)
↓
Die (프로그램 종료)
defer의 동작 방식
사용자 코드
func main() {
defer fmt.Println("cleanup")
// ... 작업
}
컴파일러 변환
func main() {
runtime.deferproc(size, funcptr)
// ... 작업
runtime.deferreturn()
}
deferproc 구현
func deferproc(siz int32, fn *funcval) {
if getg().m.curg != getg() {
throw("defer on system stack")
}
// defer 구조체 생성
d := newdefer(siz)
d.fn = fn
d.link = gp._defer // 기존 defer 체인 연결
gp._defer = d // 새 defer를 head로
}
defer 체인 구조
gp._defer → [defer 3] → [defer 2] → [defer 1] → nil
(최근) (오래된)
함수 종료 시 deferreturn()이 LIFO 순서로 실행한다.
고루틴(g) 구조체와 getg()
g 구조체
type g struct {
stack stack // 스택 메모리 정보
stackguard0 uintptr // 스택 오버플로우 감지
_panic *_panic // panic 연결 리스트
_defer *_defer // defer 연결 리스트
m *m // 현재 실행 중인 OS 스레드(M)
sched gobuf // 스케줄링 정보 (PC, SP 등)
atomicstatus uint32 // 고루틴 상태
goid int64 // 고루틴 ID
preempt bool // 선점 신호
// ... 더 많은 필드들
}
주요 필드
- _panic: 현재 panic 체인 (중첩 panic 지원)
- _defer: defer 함수 체인
- m: 이 고루틴을 실행하는 머신(OS 스레드)
- stack: 고루틴의 스택 메모리 영역
getg() - 초고속 구현
어셈블리 구현
TEXT ·GetG(SB),NOSPLIT,$0-8
get_tls(CX) // TLS 주소를 CX에
MOVQ g(CX), AX // g 포인터를 AX로
MOVQ AX, ret+0(FP) // 리턴값에 저장
RET
매크로 정의
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(CTLS*1)
- TLS (Thread Local Storage)에서 직접 g 포인터 가져옴
- 함수 호출 오버헤드 없음 (인라인 어셈블리)
- 레지스터 접근만으로 완료 → 극도로 빠름!
Stop The World 메커니즘
stopTheWorld 함수
// runtime/proc.go
func stopTheWorld(reason string) {
semacquire(&worldsema, false) // 전역 세마포어 획득
getg().m.preemptoff = reason // 선점 비활성화
systemstack(stopTheWorldWithSema) // 시스템 스택에서 실행
}
목적
- 모든 P(프로세서)가 고루틴 실행 중지
- GC safe point에서 모든 고루틴 인터럽트
- 현재 고루틴의 P만 실행 상태 유지
제약사항
- 시스템 스택에서 호출 불가
- worldsema를 이미 보유하면 안 됨
- 완료 후 반드시 startTheWorld() 호출
STW 사용 예시
func TestStop(t *testing.T) {
success := true
StopTheWorld("testing")
go func() {
success = false // 이 코드는 실행되지 않음!
}()
for j := 0; j < 100000000; j++ {
if !success {
t.Error("Other goroutine woke up")
return
}
}
StartTheWorld()
}