본문으로 건너뛰기
AZ Tools

Python .pyc 바이트코드 분석기

.pyc는 CPython이 소스 옆 __pycache__에 쓰는 컴파일 결과이고, 구조는 두 부분뿐입니다. 앞의 16바이트는 헤더입니다. 4바이트 매직 넘버가 이 파일을 쓴 CPython 릴리스를 정확히 가리키고(지금 설치된 파이썬이 아니라 컴파일한 그 파이썬입니다), 이어서 플래그 워드가 옵니다. 플래그의 0번 비트가 무효화 방식을 정합니다. 0이면 다음 8바이트가 소스의 수정 시각과 크기이고, 둘 중 하나라도 디스크의 .py와 어긋나는 순간 캐시는 버려집니다. 1이면 PEP 552 방식으로 그 8바이트가 소스 해시가 되고, 1번 비트는 import마다 다시 확인할지 아니면 그냥 믿을지를 뜻합니다. 재현 가능한 빌드가 원하는 것이 바로 뒤쪽입니다. 헤더 뒤에는 marshal로 직렬화된 코드 객체 하나가 있고, 모듈 안의 함수·람다·컴프리헨션·클래스 본문이 모두 그 상수 목록 안에 다시 코드 객체로 들어 있습니다. 여기 트리가 여러 단계로 깊어지는 이유입니다. 각 코드 객체마다 참조하는 이름, 지역·셀·자유 변수, 인자 개수, 스택 크기, 해석한 플래그 비트, repr()가 출력하는 그대로의 상수, 그리고 EXTENDED_ARG를 뒤 명령에 접어 넣고 점프를 절대 오프셋으로 바꾼 디스어셈블리를 보여 줍니다. 옵코드 번호는 릴리스마다 바뀌므로 매직 넘버로 표를 고르고 화면에 그 버전을 밝힙니다. 3.7부터 3.13까지 지원하며, 그 밖의 버전은 헤더만 읽고 본문은 잘못된 표로 해석하지 않고 그대로 둡니다. 소스를 되돌려 주지는 않습니다. 디컴파일러가 아니어서 이름·독스트링·상수는 그대로 남지만, 보이는 흐름은 여러분이 쓴 반복문이 아니라 바이트코드이고 주석과 서식은 이 파일이 만들어지기 전에 이미 사라졌습니다. 줄 번호 표와 3.11부터의 예외 표는 건너뛰기만 하고 해석하지 않습니다.

사용법

  1. __pycache__ 폴더의 .pyc를 놓으세요. 이름은 보통 module.cpython-311.pyc 같은 모양입니다.
  2. 먼저 “작성한 버전”을 보세요. 이 파일을 컴파일한 CPython 릴리스이며, 매직 넘버가 다른 인터프리터는 이 캐시를 무시하고 소스에서 다시 컴파일합니다.
  3. 무효화 방식을 확인하세요. 타임스탬프 방식은 소스의 수정 시각과 크기를, 해시 방식은 소스 해시를 담고, 검사하지 않는 쪽은 이후 소스와 다시 비교되지 않습니다.
  4. 트리에서 코드 객체를 고르세요. 맨 위가 모듈이고 함수·클래스 본문·람다·(3.12 이전의) 컴프리헨션이 각각 정의된 자리 아래에 붙습니다.
  5. 선택한 객체의 디스어셈블리를 읽으세요. 해석 결과 열이 이름·지역 변수·상수를 풀어 주고, >> 는 점프가 도착하는 명령을 뜻합니다.

자주 묻는 질문

소스를 고쳤는데 파이썬이 예전 코드를 그대로 실행합니다. 왜 그런가요?
타임스탬프 방식 .pyc는 소스의 수정 시각을 초 단위로, 크기를 바이트로 기록하고 둘 다 그대로면 캐시를 재사용합니다. 이를 뚫는 상황이 둘 있습니다. 기록된 시각과 같은 초에 편집했고 길이까지 똑같으면(예: 생성된 파일에서 한 글자만 바꾼 경우) 달라진 것이 없어 보여 낡은 캐시가 쓰입니다. 또 타임스탬프를 보존하는 도구로 파일을 복원하거나 시각을 되돌리는 브랜치를 체크아웃하면, 더 이상 맞지 않는 소스보다 새것인 .pyc가 남을 수 있습니다. __pycache__ 폴더를 지우면 둘 다 해결되고, -B 옵션이나 PYTHONDONTWRITEBYTECODE로 아예 쓰지 않게 할 수도 있습니다.
해시 방식에서 검사함과 검사 안 함은 무엇이 다른가요?
PEP 552는 타임스탬프 대신 소스의 8바이트 해시를 넣어 빌드가 파일 수정 시각에 의존하지 않게 했습니다. 같은 소스는 어느 기계에서든 바이트까지 같은 .pyc가 되고, 재현 가능한 빌드가 필요로 하는 성질입니다. 플래그의 1번 비트가 동작을 정합니다. 검사함은 import마다 소스를 해시해 다르면 다시 컴파일하고, 검사 안 함은 소스를 아예 보지 않습니다. 후자는 한 번에 만들어지는 컨테이너 이미지처럼 파이썬 밖에서 최신성이 보장되는 경우를 위한 것이고, 그 밖에서는 함정입니다. 손으로 다시 만들기 전까지 소스를 고쳐도 아무 효과가 없기 때문입니다.
.pyc에서 원래 소스를 되찾을 수 있나요?
이 도구로는 안 되고, 어떤 도구로도 완전히는 안 됩니다. 컴파일러는 실행에 필요한 것을 모두 남깁니다. 모든 이름과 상수, 독스트링, 인자 이름, 각 함수의 시작 줄 번호까지 남으므로 .pyc는 생각보다 훨씬 많은 것을 노출하며 난독화 수단이 될 수 없습니다. 반대로 텍스트 자체는 남지 않습니다. 주석·빈 줄·서식과 정확한 식의 모양은 사라집니다. uncompyle6이나 decompyle3 같은 디컴파일러가 명령 흐름에서 그럴듯한 소스를 복원하지만, 언어보다 몇 해씩 뒤처져 3.9 언저리에서 대체로 멈춥니다. 최신 컴파일러가 제어 흐름을 재구성해 일대일로 되돌릴 수 없기 때문입니다.
같은 소스인데 파이썬 버전이 다르면 바이트코드가 왜 완전히 달라지나요?
명령 집합은 CPython이 자유롭게 바꾸는 구현 세부 사항이기 때문입니다. 3.11은 인라인 캐시를 도입해 앞 명령에 딸린 실제 바이트를 명령 스트림에 넣었고 RESUME·MAKE_CELL 같은 옵코드로 프레임 처리를 옮겼습니다. 3.12는 리스트·집합 컴프리헨션을 인라인해 별도 코드 객체가 아니게 만들었고, 3.13은 번호를 거의 전부 다시 매겼습니다. 매직 넘버가 있는 이유가 바로 이것입니다. .pyc는 그것을 쓴 릴리스에서만 유효하고 다른 인터프리터는 없는 파일로 취급합니다.
오프셋이 2보다 크게 뛰는 이유와 EXTENDED_ARG는 무엇인가요?
명령은 옵코드 1바이트와 인자 1바이트로 항상 2바이트라서, 255보다 큰 인자는 앞에 붙는 EXTENDED_ARG 명령들이 8비트씩 더해 만듭니다. 여기서는 dis와 똑같이 뒤 명령에 접어 넣기 때문에 한 줄에 수천 단위 인자가 보일 수 있습니다. 3.11부터는 앞 명령에 속하며 실행 중 특수화 상태를 담는 인라인 캐시 칸도 스트림에 들어 있습니다. 이 칸은 나열하지 않고 건너뛰므로 오프셋이 2보다 크게 늘어납니다. 디스크의 .pyc에서 그 칸은 언제나 0입니다. 특수화는 메모리에서만 일어나고 빨라진 옵코드는 파일에 기록되지 않습니다.
셀 변수와 자유 변수는 무엇이고, 왜 같은 이름이 두 목록에 있나요?
클로저는 변수가 그것을 만든 프레임보다 오래 살아야 하므로 컴파일러가 셀에 담습니다. 정의하는 함수는 그 이름을 셀 변수에, 그것을 읽는 중첩 함수는 같은 이름을 자유 변수에 올립니다. 3.11부터는 둘이 한 배열에 종류 바이트와 함께 들어가며, 그래서 중첩 함수가 붙잡는 인자는 지역 변수이자 셀 변수로 두 번 나타납니다. 실제로 둘 다이며, 그 함수는 인자를 셀로 옮기는 MAKE_CELL 명령으로 시작합니다.

관련 도구