Skip to main content

CPython 인터프리터 빌드부터 실행까지의 과정을 코드 레벨로 이해하기

글에서 다루는 내용

  • CPython 개발 환경 구축
  • CPython 인터프리터 빌드부터 실행까지 (코드 레벨)

들어가며

2024년 3월부터 가짜연구소 CPython 파헤치기 스터디에 8기 러너로 참여했다. 주로 사용하고 있는 파이썬의 내부 구조를 배우고 싶어서 시작했다. 스터디는 [CPython 파헤치기] 라는 책으로 스터디를 진행하고 있다. 이번 글은 스터디를 1-3주차까지 진행하며 학습했던 내용을 바탕으로 적었다. 책 기준으로 Chapter.1 ~ Chapter.5 까지의 내용이다. 원서(CPYTHON INTERNALS) 기준으로는 Chapter.1 ~ Chapter.6 까지의 내용이다. CPython 인터프리터 빌드부터 실행까지의 과정을 코드 레벨로 이해하는 것에 집중해서 정리했다. 참고로, macOS를 기준으로 작성되었다.

개발 환경 구축 (macOS)

info

[ OS / Hardware ]

  • Apple M2 Pro
  • macOS Sonoma 14.2.1

[ Version ]

  • cpython == 3.9

깃허브에서 소스코드 다운로드

git clone https://github.com/python/cpython.git --branch 3.9
cd cpython

CPython 빌드

  1. C 컴파일러 툴킷 설치

    xcode-select --install
  2. 의존성 설치

    brew install openssl xz zlib gdbm sqlite
  3. configure 스크립트 실행 (=Makefile 생성)

    CPPFLAGS="-I$(brew --prefix zlib)/include" \
    LDFLAGS="-L$(brew --prefix zlib)/lib" \
    ./configure --with-openssl=$(brew --prefix openssl) --with-pydebug
    • configure를 통해 생성된 목록

      configure: creating ./config.status
      config.status: creating Makefile.pre
      config.status: creating Misc/python.pc
      config.status: creating Misc/python-embed.pc
      config.status: creating Misc/python-config.sh
      config.status: creating Modules/ld_so_aix
      config.status: creating pyconfig.h
      creating Modules/Setup.local
      creating Makefile
  4. Makefile 실행

    make -j2 -s
    • -j [N]: 동시에 실행할 작업 개수를 정하는 옵션. CPU 코어가 2개 이상이라면 작업을 더 빨리 수행할 수 있음.

    • -s: 실행한 명령들을 출력하지 않는 옵션

    • error: 'lzma.h' file not found 에러가 발생한다면, configure 스크립트를 실행할 때, xz 라이브러리의 경로를 추가하고 다시 빌드해보자.

      # xz 경로 추가
      CPPFLAGS="-I$(brew --prefix zlib)/include -I$(brew --prefix xz)/include" \
      LDFLAGS="-L$(brew --prefix zlib)/lib -L$(brew --prefix xz)/lib" \
      ./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

      # 빌드
      make -j2 -s
  5. CPython 디버그 바이너리 실행

    ./python.exe
    • make 명령어를 통해 CPython을 빌드한 결과로 CPython 바이너리(=executable interpreter)가 생성된다.
    • 바이너리를 순수하게 실행하면(=옵션이나 인자가 없는 상태), REPL(Read-Eval-Print-Loop)이 실행된다.

인터프리터 빌드 과정

인터프리터를 빌드하는 과정은 위 섹션(개발 환경 구축)에서 봤듯이, 두 단계로 나뉜다.

  1. configure 파일을 이용해 Makefile 생성하기
  2. Makefile을 이용해 CPython 인터프리터 빌드하기

두 번째 단계에서 사용한 명령어는 make -j2 -s 인데, 해당 명령어의 동작에 대한 코드 레벨의 설명은 다음과 같다.

# Default target
all: build_all
build_all:	check-clean-src $(BUILDPYTHON) oldsharedmods sharedmods gdbhooks \
Programs/_testembed python-config
# Build the interpreter
$(BUILDPYTHON): Programs/python.o $(LIBRARY) $(LDLIBRARY) $(PY3LIBRARY) $(EXPORTSYMS)
$(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(BLDLIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS)
Programs/python.o: $(srcdir)/Programs/python.c
$(MAINCC) -c $(PY_CORE_CFLAGS) -o $@ $(srcdir)/Programs/python.c

인터프리터 빌드 라는 관점으로, 동작 흐름을 살펴보자.

  1. make -j2 -s에는 target을 기입하지 않았다. 따라서, default target을 호출한다.
  2. default target은 all다.
  3. allbuild_all을 호출한다.
  4. build_all은 여러개의 target으로 구성되어 있다. 그 중, $(BUILDPYTHON)이 인터프리터 빌드와 관련된 target이다.
  5. $(BUILDPYTHON) 또한 여러개의 target으로 구성되어 있다. 그 중, Programs/python.o./Programs/python.c 파일을 컴파일하는 명령어이다.

따라서, CPython 인터프리터를 실행할 때, entrypoint 파일은 Programs/python.c 이다.

참고로, CPython 인터프리터 파일은 윈도우, 맥 모두 python.exe로 생성된다. MakefileBUILDEXE 변수를 통해 확장자를 변경할 수 있다.

# Executable suffix (.exe on Windows and Mac OS X)
EXE=
BUILDEXE= .exe

인터프리터 실행 과정

인터프리터를 실행(=./python.exe)하면, Programs/python.c으로 진입한다.

Programs/python.c

/* Minimal main program -- everything is loaded from the library */

#include "Python.h"

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
return Py_BytesMain(argc, argv); // macOS
}
#endif

macOS의 경우 mainPy_BytesMain 함수로 진입한다.

Modules/main.c: L723-L732

int
Py_BytesMain(int argc, char **argv)
{
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 1,
.bytes_argv = argv,
.wchar_argv = NULL};
return pymain_main(&args);
}

Py_BytesMain 함수는 인터프리터를 실행할 때, 함께 넘겨준 인자를 파싱해서 args에 할당한다. 이후, pymain_main 함수를 실행한다.

Modules/main.c: L695-L708

static int
pymain_main(_PyArgv *args)
{
PyStatus status = pymain_init(args);
if (_PyStatus_IS_EXIT(status)) {
pymain_free();
return status.exitcode;
}
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}

return Py_RunMain();
}

pymain_main 함수에서는 크게 두 가지 동작을 한다. pymain_init 함수를 통해 인터프리터에서 사용할 configuration을 설정하고, 문제가 없다면 Py_RunMain 함수를 통해 파이썬 코드를 실행한다. 부가적으로 PyStatus 타입을 갖는 status 변수의 값을 체크해서, 실행을 종료하거나 에러를 반환하기도 한다. 참고로, PyStatus는 3가지의 타입을 갖는다. 궁금하다면 아래 코드를 확인하자.

PyStatus 구현체

Include/cpython/initconfig.h: L10-L19

typedef struct {
enum {
_PyStatus_TYPE_OK=0,
_PyStatus_TYPE_ERROR=1,
_PyStatus_TYPE_EXIT=2
} _type;
const char *func;
const char *err_msg;
int exitcode;
} PyStatus;

인터프리터 빌드부터 실행까지 정리

  1. Makefile을 통해 인터프리터를 빌드한다. 인터프리터의 entrypoint는 Programs/python.c이다.
  2. Programs/python.c에서는 운영 체제에 따른 main 함수를 선택한다. macOS의 경우 mainPy_BytesMain을 호출한다.
  3. Py_BytesMain는 인터프리터를 실행할 때 입력한 인자(argument) 정보를 함수 실행 정보에 포함시키고, pymain_main을 호출한다.
  4. pymain_main은 인터프리터의 전체 동작 흐름을 제어한다. pymain_init을 통해 인터프리터에서 사용할 configuration을 설정하고, Py_RunMain을 통해 파이썬 코드를 실행시킨다.

정리

파이썬이 실행되는 소스 코드를 보고 나니까, 파이썬 실행 과정을 보다 입체적으로 이해할 수 있었다. 이번 포스팅에서 다루지는 않았지만, 이번 스터디를 통해서 인터프리터에서 사용하는 configuration에는 어떤 것들이 있으며, 파이썬을 실행할 수 있는 다양한 방법(ex> 파이썬 파일 실행, -c 옵션을 통한 문자열 실행, -m을 통한 모듈 실행)들이 어떻게 분기처리가 되는지 등등을 학습하고 있다. 가짜연구소에 처음 참여했는데, 생각 이상으로 많이 배우고 있어서 만족스럽다.

요즘 보면 LLM(ex> ChatGPT, Cluade)를 통해 예전보다 훨씬 빠르게 배울 수 있고, 새로운 기술들도 계속 나온다. 다양한 것을 배우는 것도 중요하고, 직접 적용해서 무엇인가를 만드는 것도 중요하지만, “기본기”를 쌓는 것도 역시나 중요하다고 생각한다. 오히려, 가장 중요하지 않을까 싶기도 한다. 아무튼, 이번 스터디를 통해 파이썬 인터널에 대해 잘 배워둬야겠다. 이후에도 자주 사용하는 기술들에 대한 인터널을 공부해보는 것도 좋을 것 같다.