Electron 44 렌더러에서의 V8 바이트코드 컴파일
⚡

Electron 44 렌더러에서의 V8 바이트코드 컴파일

Tags
Node.js
Game
Electron
JavaScript
bytenode
Description
Electron 업그레이드 뒤 렌더러가 SIGTRAP으로 종료됐습니다. 바이트코드 헤더 불일치를 추적하고 타깃 렌더러 안에서 컴파일하도록 빌드를 바꾼 과정입니다.
Published
2026년 8월 30일 발행
On this page
제가 개발하는 Electron 앱은 렌더러의 JavaScript 소스를 그대로 싣는 대신, bytenode로 컴파일한 V8 바이트코드(.jsc)를 배포합니다.
Electron 44로 업그레이드하자 렌더러가 시작 중 SIGTRAP으로 종료됐습니다. 에러 창도 스택 트레이스도 없었습니다. 바이트코드 헤더를 추적한 끝에, 빌드 과정에서 캐시를 컴파일하는 위치를 바꿨습니다.

로더가 검증 필드를 덮어쓰고 있었습니다

V8은 바이트코드 캐시를 받아들일지 판단할 때 헤더에서 검증 필드 몇 개를 읽습니다. 버전 해시, 플래그 해시, 그리고 읽기 전용(RO) 스냅샷 체크섬입니다.
비교한 빌드에서는 V8 버전이 같을 때 macOS ARM64와 Windows x64 바이트코드의 버전 해시가 같았습니다. 두 플랫폼을 구분한 값은 RO 스냅샷 체크섬이었습니다.
고정해서 쓰던 구버전 bytenode의 fixBytecode 루틴은 로드 시점에 해당 헤더 바이트를 로딩 프로세스의 값으로 덮어씁니다. 기존 빌드 파이프라인은 Mac에서 Windows용 바이트코드를 크로스 컴파일하고 있었지만, 덮어쓴 필드로는 원래 캐시가 로딩 환경과 맞는지 확인할 수 없었습니다.
이전에 배포하던 Electron 빌드에서는 브라우저 프로세스와 Node 컨텍스트, 렌더러가 RO 스냅샷을 공유했습니다. 이전 실행에서 문제가 드러나지 않았던 이유를 여기서 찾을 수 있었습니다.

Electron 44: 렌더러의 체크섬이 달랐습니다

테스트한 Electron 44(V8 15.2) 빌드에서는 렌더러의 RO 스냅샷 체크섬이 node/browser와 달랐습니다. 실측값은 다음과 같습니다.
ㅤ
node/browser
renderer
darwin/arm64
40968fc3
fc334340
win32/x64
67178eb7
16278fc6
이 환경에서는 Node 컨텍스트에서 컴파일한 .jsc와 이를 로드하는 렌더러의 값이 맞지 않았습니다. 헤더 패치가 이 불일치를 가리면서 V8이 cachedDataRejected로 캐시를 거절할 기회를 없앴습니다. 이후 렌더러는 생성된 코드 안에서 SIGTRAP으로 종료됐고, JavaScript의 try/catch로는 잡히지 않았습니다.
확인한 도구와 이슈 트래커에서는 이 렌더러 구성에 맞는 해결 방법을 찾지 못했습니다.

로드할 환경에서 컴파일하기

캐시를 실제로 로드할 환경에서 컴파일해 보기로 했습니다.
바이트코드를 렌더러 안에서, 그것도 타깃 플랫폼에서 컴파일하면 됩니다.
테스트한 빌드에서는 타깃 플랫폼의 렌더러에서 .jsc를 컴파일하자, 로드 시점에 값을 덮어쓰지 않아도 RO 체크섬이 일치했습니다.
핵심 변경은 숨은 BrowserWindow를 띄우고 그 렌더러 컨텍스트에서 vm.Script(source, { produceCachedData: true })를 실행하는 것이었습니다. 이어서 플랫폼별 빌드와 패키징 과정을 테스트했습니다.
  • 난독화된 6.1MB 실제 번들을 Windows 렌더러에서 구워 13.9MB .jsc를 얻었고, 컴파일에 걸린 시간은 약 1초였습니다.
  • Windows 빌드 머신에서 구운 뒤 Mac 패키징 단계로 가져왔고, 패키징된 아카이브에서 헤더를 다시 읽어내 정말 그 파일이 실렸는지까지 확인했습니다. 이름뿐 아니라 실제 산출물까지 확인한 것입니다.
  • Electron 44에서 깨끗하게 부팅했고, 밤새 돌리는 장시간 테스트도 통과했습니다.
크로스 컴파일 파이프라인은 플랫폼별로 컴파일하는 파이프라인이 됐습니다. 이 번들의 컴파일에는 약 1초가 걸렸고, 빌드 머신과 패키징 머신 사이에서 산출물을 전달하는 과정도 마련해야 했습니다.

새 파이프라인에 추가한 빌드 검사

헤더를 덮어쓰지 않는 검증. 플랫폼별로 컴파일하면서 빌드 파이프라인에서 헤더 패치를 유지할 이유가 사라졌습니다. 해당 필드를 그대로 두는 로더라면 불일치를 감추지 않고 캐시를 거절할 수 있습니다.
헤더 지문 검사도 추가했습니다. 출하되는 모든 .jsc의 첫 20바이트를 읽어 버전 해시, 플래그 해시, RO 체크섬을 빌드 타깃과 대조합니다. 검사에는 밀리초 단위의 시간이 걸리고, 이 값들이 맞지 않으면 패키징 전에 원인을 알 수 있습니다.
하나 더 확인할 점은 테스트 환경에서 require('bytenode')가 플래그 해시를 바꿨다는 것입니다. bytenode가 로드 시점에 --no-lazy 같은 V8 플래그를 설정하기 때문입니다. require 전에 기대 지문을 측정했더니 실제 로딩 환경을 반영하지 못한 불일치가 생겼습니다.

바이트코드와 보안의 한계

이번에 테스트한 것은 바이트코드의 호환성이지 역공학에 대한 내성이 아닙니다. 이 결과만으로 배포한 코드의 복원이 얼마나 어려운지 판단할 수는 없습니다. 바이트코드에는 문자열 상수가 남을 수도 있으므로, 이 패키징 방식을 비밀을 보관하는 수단으로 여기지는 않습니다.

정리하며

  1. 바이트코드 크로스 컴파일이 "잘 되고 있다"면, 로더가 검증 필드를 덮어쓰는지 확인해 보시기 바랍니다. 사용하던 로더는 그렇게 하고 있었습니다.
  1. 테스트한 Electron 44 빌드에서는 렌더러의 RO 체크섬이 node/browser와 달랐습니다. 테스트 환경에서는 이 불일치를 가린 것이 크래시로 이어졌습니다.
  1. 바이트코드를 로드할 프로세스와 같은 종류, 같은 타깃 플랫폼에서 컴파일하고, 패키징된 산출물도 확인하시기 바랍니다.
  1. 새 파이프라인은 Electron 44 테스트를 통과했습니다. 헤더 검사는 다음 호환성 문제의 진단을 도와주겠지만, 다음 업그레이드의 테스트를 대신하지는 않습니다.

2026년 11월 후속

10월 30일 배포한 블로샤퍼 1.3.5에는 Electron 45.1.0과 bytenode 2.1을 적용했습니다. 버전은 바뀌었지만, 바이트코드를 실제로 로드할 타깃 플랫폼의 렌더러에서 컴파일한다는 원칙은 그대로 유지하고 있습니다.
현재 로더에서는 예전처럼 RO 스냅샷 체크섬을 덮어쓰지 않습니다. 플래그 해시는 여전히 덮어쓰므로 헤더 패치를 모두 없앤 것은 아닙니다. RO 체크섬은 V8이 검증하도록 남겨 두었고, 다른 플랫폼의 캐시를 넣는 테스트에서도 프로세스가 크래시하는 대신 예외로 잡을 수 있는 형태로 거절되는 것을 확인했습니다.
빌드 과정도 CI에 정착했습니다. Windows용 바이트코드는 GitHub 호스팅 러너의 렌더러에서 컴파일하고 헤더 지문을 검사한 뒤, Mac의 패키징 단계로 전달해 다시 검증합니다. Mac용은 Mac에서 직접 컴파일합니다. 네 가지 배포 타깃이 공유 워크플로우를 사용하면서, 컴파일 환경과 최종 패키지의 산출물을 함께 확인하는 흐름으로 운영하고 있습니다.