본문 바로가기
CE

[Py] bytecode 분석 - dis 모듈

by ds31x 2025. 3. 11.

(main) script 부분

Python에서 "main script"는 프로그램 실행을 시작하는 주 진입점이 되는 Python 파일 또는 source code를 의미함.

  0           0 RESUME                   0
  1           2 LOAD_CONST               0 (<code object func at 0x103661890, file "./test.py", line 1>)
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (func)
  1. RESUME 0: 코드 실행 시작
  2. LOAD_CONST 0: 상수 테이블의 인덱스 0에서 func 함수의 코드 객체를 읽어와 스택에 Push
    • 이 코드 객체는 컴파일 단계에서 생성되어 상수 테이블에 저장되었음
  3. MAKE_FUNCTION 0: 스택에서 코드 객체를 Pop하여 호출 가능한 함수 객체 생성스택에 Push
  4. STORE_NAME 0 (func): 스택에서 함수 객체를 하여 전역 네임스페이스 딕셔너리의 'func' 키에 저장

  6           8 LOAD_CONST               1 (10)
             10 STORE_NAME               1 (a)
  7          12 LOAD_CONST               2 (999999)
             14 STORE_NAME               2 (b)
  1. LOAD_CONST 1 (10): 상수 테이블의 인덱스 1에서 값 10을 읽어와 스택에 Push
  2. STORE_NAME 1 (a): 스택에서 값을 Pop하여 전역 네임스페이스의 'a' 키에 저장
  3. LOAD_CONST 2 (999999): 상수 테이블의 인덱스 2에서 값 999999를 읽어와 스택에 Push
  4. STORE_NAME 2 (b): 스택에서 값을 Pop하여 전역 네임스페이스의 'b' 키에 저장

  8          16 PUSH_NULL
             18 LOAD_NAME                0 (func)
             20 LOAD_NAME                1 (a)
             22 LOAD_NAME                2 (b)
             24 CALL                     2
             32 STORE_NAME               3 (c)
  1. PUSH_NULL: 예외 처리용 null을 스택에 Push
  2. LOAD_NAME 0 (func): 전역 네임스페이스에서 'func' 키의 값(함수 객체)을 읽어와 스택에 Push
  3. LOAD_NAME 1 (a): 전역 네임스페이스에서 'a' 키의 값(10)을 읽어와 스택에 Push
  4. LOAD_NAME 2 (b): 전역 네임스페이스에서 'b' 키의 값(999999)을 읽어와 스택에 Push
  5. CALL 2: 스택에서 함수와 2개의 인자를 Pop하여 함수 호출, 함수 실행 후 결과를 스택에 Push
  6. STORE_NAME 3 (c): 스택에서 함수 호출 결과를 Pop하여 전역 네임스페이스의 'c' 키에 저장

 10          34 PUSH_NULL
             36 LOAD_NAME                4 (print)
             38 LOAD_NAME                3 (c)
             40 CALL                     1
             48 POP_TOP
             50 RETURN_CONST             3 (None)
  1. PUSH_NULL: 예외 처리용 null을 스택에 Push
  2. LOAD_NAME 4 (print): 내장(builtins) 네임스페이스에서 'print' 키의 값(내장 함수)을 읽어와 스택에 Push
  3. LOAD_NAME 3 (c): 전역 네임스페이스에서 'c' 키의 값을 읽어와 스택에 Push
  4. CALL 1: 스택에서 'print' 함수와 1개의 인자를 Pop하여 함수 호출, 결과(None)를 스택에 Push
  5. POP_TOP: 스택 맨 위의 값(None)을 Pop하여 버림
  6. RETURN_CONST 3 (None): 상수 테이블의 인덱스 3에서 None을 읽어와 반환하고 프로그램 종료

func 함수 부분

  1           0 RESUME                   0
  2           2 LOAD_FAST                0 (a)
              4 LOAD_FAST                1 (b)
              6 BINARY_OP                0 (+)
             10 STORE_FAST               2 (c)
  3          12 LOAD_FAST                2 (c)
             14 RETURN_VALUE
  1. RESUME 0: 함수 실행 시작
  2. LOAD_FAST 0 (a): 지역 변수 배열의 인덱스 0에서 매개변수 'a'의 값을 읽어와 스택에 Push
    • 함수 호출 시 전달된 인자가 이 배열에 저장됨
  3. LOAD_FAST 1 (b): 지역 변수 배열의 인덱스 1에서 매개변수 'b'의 값을 읽어와 스택에 Push
  4. BINARY_OP 0 (+): 스택에서 두 값을 Pop하여 더한 후 결과를 스택에 Push
    • 0은 덧셈 연산을 나타내는 오퍼랜드
  5. STORE_FAST 2 (c): 스택에서 결과 값을 Pop하여 지역 변수 배열의 인덱스 2('c')에 저장
  6. LOAD_FAST 2 (c): 지역 변수 배열의 인덱스 2에서 'c'의 값을 읽어와 스택에 Push
  7. RETURN_VALUE: 스택에서 값을 하여 함수의 반환 값으로 사용, 호출자의 스택에 Push됨

데이터 저장소 요약

  1. 상수 테이블(co_consts):
    • 컴파일 시점에 결정되는 모든 상수 값(숫자, 문자열, 코드 객체 등)을 저장
    • LOAD_CONST 명령어가 여기서 값을 읽어옴
  2. 전역 네임스페이스:
    • 모듈 수준의 변수들을 저장하는 딕셔너리
    • STORE_NAME이 여기에 값을 저장하고, LOAD_NAME이 여기서 값을 읽어옴
  3. 지역 변수 배열:
    • 함수 내의 지역 변수와 매개변수를 저장하는 배열
    • STORE_FAST가 여기에 값을 저장하고, LOAD_FAST가 여기서 값을 읽어옴
    • 인덱스 기반 접근으로 딕셔너리 조회보다 빠름
  4. 내장 네임스페이스:
    • print(), len() 등의 내장 함수를 저장
    • LOAD_NAME이 전역 네임스페이스에서 찾지 못한 이름을 여기서 검색

위의 bytescode 결과에 대응하는 Python 코드 소스 파일 test.py는 다음과 같음

def func(a,b):
    c = a+b
    return c


a = 10
b = 999999
c = func(a,b)

print(c)

 

위의 결과를 얻어내는 커맨드는 다음임:

❯ python -m dis test.py

 

또는 소스코드를 사용하는 compile을 이용하는 방식도 있음

#dis_test.py
import dis

with open('./test.py', 'r') as f:
    source = f.read()


code_obj = compile(source, "./test.py", "exec")
dis.dis(code_obj)

 

아니면 __pycache__.pyc 바이트코드 파일을 직접사용하는 방법도 있음

import dis
import marshal
import importlib.util
import sys

# .pyc 파일 경로
pyc_path = "__pycache__/my_module.cpython-310.pyc"  # 실제 파일 경로로 변경

# .pyc 파일의 헤더 크기 (Python 버전에 따라 다름)
# Python 3.7+ 에서는 일반적으로 16바이트
# Magic number (4bytes) + Bit field (4bytes) + Timestamp (4bytes) + Size (4bytes)
HEADER_SIZE = 16

# .pyc 파일 읽기
with open(pyc_path, 'rb') as f:
    # 헤더 건너뛰기
    f.seek(HEADER_SIZE)
    
    # 바이트코드 로드
    code_object = marshal.load(f)
    
    # 바이트코드 디스어셈블
    dis.dis(code_object)