# 휴리스틱 없는 결정론적 완전 정적 전체 바이너리 번역

> Clean Markdown view of GeekNews topic #29507. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29507](https://news.hada.io/topic?id=29507)
- GeekNews Markdown: [https://news.hada.io/topic/29507.md](https://news.hada.io/topic/29507.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-05-15T04:57:11+09:00
- Updated: 2026-05-15T04:57:11+09:00
- Original source: [arxiv.org](https://arxiv.org/abs/2605.08419)
- Points: 1
- Comments: 1

## Topic Body

- **Elevator**는 디버그 정보·소스 코드·바이너리 레이아웃 가정 없이 x86-64 실행 파일 전체를 AArch64로 정적으로 번역함
- 코드·데이터 판별 휴리스틱 대신 각 바이트의 가능한 해석을 모두 담은 **superset CFG**를 만들고 종료 경로만 제거함
- x64 상태를 AArch64 레지스터에 일대일 매핑하고, 원본 주소에서 번역 코드로 가는 **조회 테이블**로 간접 분기를 처리함
- 오프라인 **타일 뱅크**는 x64 명령 의미를 C 템플릿으로 작성한 뒤 LLVM 20으로 AArch64 바이트 시퀀스로 컴파일됨
- 결과물은 런타임 번역 없는 자체 포함 AArch64 바이너리이며, **SPECint 2006**에서 QEMU 사용자 모드 JIT와 동등하거나 더 나은 성능을 냄

---

### Elevator의 목표
- **Elevator**는 x86-64 실행 파일 전체를 AArch64로 옮기는 완전 정적 바이너리 번역기임
- 디버그 정보, 소스 코드, 원본 바이너리의 코드 패턴, 바이너리 레이아웃에 대한 가정을 사용하지 않음
- 기존 정적 번역기는 코드와 데이터를 구분하기 위해 휴리스틱이나 런타임 폴백에 의존하지만, Elevator는 원본 실행 파일의 모든 바이트를 가능한 해석별로 미리 번역함
- 어떤 바이트든 데이터, opcode 일부, opcode 인자 일부가 될 수 있으므로 가능한 제어 흐름을 모두 포함한 **superset CFG**를 만들고, 예외적 프로그램 종료로 이어지는 경로만 제거함
- 출력은 번역 코드, 원본 x64 바이너리, 주소 조회 테이블, 런타임 드라이버를 포함한 **자체 포함 AArch64 바이너리**로 구성됨
- 번역 완료 뒤에는 JIT나 런타임 번역 지원 없이 실행 가능함
- 같은 입력 바이너리를 두 번 번역하면 같은 출력 비트열이 생성되며, 테스트·검증·인증·암호화 서명 대상이 실제 배포 코드와 일치함
- 주요 비용은 **코드 크기 증가**이며, 그 대가로 에뮬레이터나 JIT 컴파일러보다 배포 전 검증 가능성이 커짐
- 평가에는 전체 **SPECint 2006** 벤치마크와 손으로 만든 바이너리가 포함됐고, 성능은 JIT 가속을 쓰는 QEMU 사용자 모드 에뮬레이션과 동등하거나 더 나은 수준으로 나옴
- 연구진은 프로젝트 종료 시 전체를 오픈소스로 공개하겠다고 밝힘

### 정적 번역이 필요한 이유와 기존 한계
- 하드웨어가 한 ISA에서 다른 ISA로 전환될 때 기존 소프트웨어를 새 플랫폼으로 가져가야 하며, 남아 있는 소스 코드를 재컴파일하는 방식만으로는 충분하지 않을 수 있음
- 검증 또는 인증된 레거시 코드에서는 소스 코드가 아니라 잘 테스트된 특정 **권위 있는 바이너리 실행 파일**이 인증 대상인 경우가 많음
- 나중에 소스에서 동일한 바이너리를 비트 단위로 재현하려면 당시의 컴파일러, 링커, 빌드 시스템 버전이 필요할 수 있어 현실적으로 어려움
- 제조사가 소스 코드를 거치지 않고 바이너리에 직접 패치를 적용한 경우, 보관된 소스로 다시 빌드하면 이미 수정된 오류가 되살아날 수 있음
- 기존 바이너리 직접 처리 방식은 에뮬레이션, 정적 번역, 동적 번역을 조합하지만, 번역된 프로그램과 함께 실행되는 추가 시스템 컴포넌트가 신뢰 기반 코드에 포함됨
- 동적 동작은 테스트 순서나 입력에 따라 달라질 수 있어 전체 신뢰성을 확인하기 어려움
- Horspool과 Marovac은 1980년에 실행 파일을 역변환하려면 코드와 데이터를 확실히 구별해야 하며, 대부분의 아키텍처에서 이는 **정지 문제**와 동등해 일반적으로 풀 수 없음을 보였음
- 기존 정적 바이너리 리프터는 코드와 데이터 구분을 휴리스틱으로 근사하며, 특히 간접 제어 흐름 전송의 목표를 예측할 때 문제가 커짐
- LLBT는 ARM 명령을 LLVM IR로 올려 대상 아키텍처로 재컴파일하지만, 간접 분기 목표 탐지에 휴리스틱을 사용하고 입력 바이너리에 여러 가정을 둠
- 좋은 휴리스틱도 일부 입력에서는 실패하며, 전체 바이너리를 올바르게 리프팅하려면 모든 코드·데이터 판별이 맞아야 하므로 바이너리가 클수록 실패 가능성이 커짐
- 동적 방식은 실제 실행된 명령 흐름을 따라가므로 명령 복구와 간접 제어 흐름을 처리할 수 있지만, 구체 실행에서 도달하지 않은 명령은 리프팅하지 못함
- x64처럼 **가변 길이 명령**을 가진 ISA에서는 명령 시퀀스 안에 다른 명령 시퀀스가 중첩될 수 있고, 다중 바이트 명령 중간으로 분기하면 기존 피연산자가 별도 명령으로 디코드될 수 있음
- ROP 공격과 코드 난독화는 이 특성을 활용할 수 있음
- Apple의 Rosetta II와 Microsoft의 Prism은 사전 번역과 동적 번역 컴포넌트를 결합함
- WYTIWYG와 Polynima는 동적 프로파일링으로 식별한 제어 흐름 경로를 따라 정적으로 리프팅하고, 보지 못한 목표 주소에 도달하면 동적으로 제어 흐름 정보를 수집하는 폴백을 사용함
- Elevator는 어떤 바이트가 코드인지 데이터인지, 명령어 워드인지 인자인지 결정하지 않고, 실행 파일의 각 바이트를 가능한 모든 해석으로 별도 제어 흐름 경로에 포함함
- 이 방식은 **superset disassembly**를 정적 재컴파일과 크로스 ISA 컴파일에 적용한 것으로, 디코딩 정밀도를 코드 증가와 맞바꿈

### 제어 흐름과 상태 보존
- Elevator는 번역된 AArch64 코드 안에서 **x64 상태 전체 보존**을 원칙으로 동작함
- x64 레지스터와 AArch64 레지스터를 일대일로 매핑해 각 x64 레지스터 상태를 대응 AArch64 레지스터에서 에뮬레이션함
- x64 스택은 AArch64 스택 위에서 직접 에뮬레이션되며, 실행 중 일반적인 스택 확장은 운영체제가 처리함
- 입력 x64 바이너리의 ABI를 분석하지 않고, 외부 코드로 실행이 넘어가거나 돌아오는 지점에서만 x64 System V ABI와 AArch64 Procedure Call Standard 규칙에 따라 ABI 번역을 수행함
- 완전한 상태 보존과 일대일 레지스터 대응 덕분에 각 x64 명령은 앞뒤 명령을 알지 못해도 독립적으로 번역될 수 있음
- 원본 바이너리의 각 실행 가능 바이트 오프셋은 데이터이면서 잠재적 명령 시퀀스의 시작점으로 동시에 해석됨
- 간접 점프, 콜백, 런타임 디스패치처럼 정적으로 분석할 수 없는 모든 잠재 목표에는 재작성된 바이너리 안에 대응 착지점이 생김
- 런타임에는 원본 명령 주소에서 번역된 코드 주소로 가는 **조회 테이블**을 최종 바이너리에 포함해 목표를 해석함
- ## 중첩 명령 예시
  - `Listing 1`은 `.byte 0xB0`에서 디코드를 시작하면 `MOV AL, 0xC3` 뒤에 `RET`이 나오고, 한 바이트 뒤 `ReturnC2`에서 시작하면 `RET`만 나오는 구조임
  - 두 디코드는 앞선 `jz`에서 모두 도달 가능하며, 번역기가 두 바이트에 대해 하나의 해석만 선택하면 한 경로를 놓치게 됨
- ## 계산된 간접 분기 예시
  - `Listing 2`는 `call Label`이 테이블 기준 주소를 만들고, `pop rsi`로 이를 회수한 뒤 입력 의존 오프셋을 더해 `jmp rsi`의 목표를 구성함
  - 분기는 인코딩 스트림에서 2바이트 간격으로 놓인 네 개의 `inc eax` 명령 중 하나로 착지할 수 있음
  - 정적으로 해석 가능한 점프 목표만 재작성하는 번역기는 이런 분기를 착지시킬 위치가 없음
- ## 호출·반환·분기
  - `call`, `return`, `branch` 명령은 반환 주소 위치, 프로그램 카운터, 조건 플래그 레이아웃이 x64와 AArch64에서 달라 C 타일로 표현할 수 없음
  - 직접 호출은 원본 x64 반환 주소를 에뮬레이션 스택에 푸시하고 callee의 번역 타일로 분기함
  - 간접 호출은 목표가 번역된 바이너리 내부인지 외부 라이브러리인지 확인하고, 내부 목표는 x64 오프셋-타일 테이블로 번역해 해당 타일로 분기함
  - 외부 목표는 AArch64 라이브러리가 돌아올 `X30`에 역 ABI 번역 gadget 주소를 넣고, exit ABI 번역을 수행한 뒤 외부 목표로 분기함
  - 반환은 에뮬레이션 스택에서 8바이트 반환 주소를 꺼내 내장 x64 바이너리 범위와 비교하고, 내부 반환이면 조회 테이블로 주소를 번역해 해당 타일로 분기함
  - 직접 분기는 번역 시점에 목표가 알려지며, 조건 분기는 `X14`에 보관된 x64 플래그 비트를 검사하는 AArch64 조건 분기로 번역됨
  - 간접 분기는 간접 호출·반환과 같은 bounds check를 방출하고, 목표가 외부이면 exit ABI 번역을 수행함

### 타일 기반 번역 파이프라인
- Elevator의 번역은 오프라인 **타일 뱅크** 생성, 입력 바이너리별 재작성, 최종 패키징의 세 단계로 나뉨
- 오프라인 단계는 x64 명령 의미를 C 함수로 표현하고, 고정된 x64-to-AArch64 레지스터 매핑 아래 피연산자 조합별로 특수화한 뒤 수정된 LLVM 20으로 컴파일해 재사용 가능한 AArch64 바이트 시퀀스를 만듦
- 입력 바이너리별 단계는 superset disassembly를 수행하고, 발견된 각 후보 명령에 대해 이름으로 타일을 찾아 AArch64 바이트 시퀀스를 이어 붙임
- 제어 흐름 전송과 ABI 경계처럼 C 타일로 표현하기 어려운 명령 범주는 손으로 만든 작은 템플릿으로 처리함
- 패키징 단계는 번역된 코드, 원본 x64 바이너리, 주소 조회 테이블, 런타임 드라이버를 결합해 독립 실행 AArch64 바이너리를 생성함
- ## 오프라인 타일 뱅크
  - x64 명령마다 동등한 AArch64 명령 시퀀스를 손으로 쓰는 방식은 실용적이지 않음
  - `ADD Reg8, Reg8` 같은 하나의 템플릿도 256개의 구체 레지스터 조합으로 확장되며, 전체 x64 명령 집합에는 레지스터, 메모리 피연산자, 즉시값 주소 지정 변형이 많음
  - Elevator는 각 x64 명령 의미를 작은 C 함수로 작성하고, 구체 피연산자 조합별로 특수화한 뒤 LLVM이 AArch64로 컴파일하게 함
  - `ADD Reg8, Reg8` 예시에서 템플릿은 목적지 레지스터의 하위 8비트를 8비트 합으로 갱신하고 상위 56비트는 유지해 x64의 부분 레지스터 쓰기 의미를 맞춤
  - x64 `ADD Reg8, Reg8`은 `RFLAGS`의 Carry, Parity, Auxiliary Carry, Zero, Sign, Overflow 플래그도 바꾸므로, 단일 반환값만 갖는 C 함수 제약 때문에 플래그 갱신은 별도 **플래그 타일**로 캡처됨
  - 하나의 x64 명령은 하나 또는 여러 타일에 대응할 수 있으며, 방출 시 이들을 다시 연속으로 붙여 전체 의미를 복원함
  - `aarch64_custom_reg` 속성은 LLVM이 반환값과 각 인자를 어느 AArch64 레지스터에 배치할지 선언함
  - 고정 매핑은 x64 System V와 AAPCS64의 callee-saved·caller-saved 성격을 맞추고, 정수 인자 레지스터 위치 재배열을 줄이며, 남는 AArch64 callee-saved 레지스터를 향후 그림자 상태용으로 남기도록 선택됨
  - x64의 `RFLAGS` 비트와 `XMM` 레지스터 파일도 같은 일대일 원칙 아래 전용 AArch64 레지스터에 보관됨
  - 수정된 LLVM 20은 함수별 `aarch64_custom_reg` 속성을 처리하고, 에뮬레이션된 x64 상태를 담는 AArch64 레지스터를 레지스터 할당기 안에서 callee-saved로 재분류함
  - `TileGen`은 C 템플릿을 순회해 허용 가능한 피연산자 조합마다 특수화된 사본을 만들고, 템플릿의 매개변수 위치와 레지스터 매핑으로 속성을 기계적으로 합성함
- ## 입력 바이너리별 재작성
  - 입력 x64 바이너리가 주어지면 per-binary 단계는 superset disassembly를 수행하고 결과 CFG를 순회함
  - 각 노드에서 포매터는 디코드된 명령의 opcode와 피연산자로부터 타일 이름을 만들며, 여러 타일이 필요한 명령에는 여러 이름을 조합함
  - x64는 스택 포인터 정렬 제한이 없지만, AArch64는 스택 포인터를 메모리 피연산자에 사용할 때 **16바이트 정렬**을 요구함
  - `RSP`를 `SP`에 직접 매핑하면 함수 프롤로그의 연속 `PUSH`처럼 일반적인 x64 코드 패턴이 AArch64에서 정렬 예외를 일으킬 수 있음
  - Elevator는 타일이 별도 레지스터 `X25`를 통해 스택에 접근하게 하고, 타일이 실제로 필요로 할 때만 `SP`를 그 안에 구체화함
  - LLVM으로 컴파일된 타일은 진입 시 16바이트 `SP` 정렬을 기대하므로, 스필 공간을 할당하는 것으로 감지된 타일을 실행하기 전 `SP`를 아래로 정렬하고 실행 뒤 복원함
  - 플래그 계산 타일은 상대적으로 비싸기 때문에, 이후 post-dominating 명령에서 읽히기 전에 플래그가 덮어써지는 경우 현재 노드의 플래그 계산을 제거함
  - 현재 미지원 명령은 주로 x64의 **AVX2 및 이후 와이드 벡터 확장**이며, 해당 위치에는 타일 대신 인터럽트 명령을 삽입함
  - SPECint 2006 전체 평가에서는 전체 x86-64 정수 ISA와 SPECint가 사용하는 SSE 부분집합만으로 모든 벤치마크 실행에 충분했음
  - 추가 명령 지원은 새 타일을 더하는 방식으로 확장 가능하지만, 추가 엔지니어링이 과학적 통찰을 더할 가능성은 낮다고 봄

### ABI 경계 처리
- Elevator는 동적 링크 바이너리만 지원함
- 정적 링크 바이너리는 `CPUID` 같은 아키텍처 특화 명령을 직접 포함할 수 있지만, 동적 링크 바이너리는 이를 `libc`에 위임하므로 번역 필요가 줄어듦
- 동적 링크 라이브러리와 상호작용할 때 emulated x64 환경과 native AArch64 라이브러리 코드 사이를 오가기 위해 x64 Linux ABI와 AArch64 Linux ABI 사이의 전환을 지원함
- ABI 번역이 필요한 핵심 요소는 **인자 배치**와 반환 주소 위치임
- System V x64 ABI는 `RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9` 여섯 레지스터를 인자 레지스터로 사용하고, 추가 인자는 `[RSP+8]`부터 스택에 전달함
- x64 `CALL`은 반환 주소를 `[RSP]`에 저장함
- AArch64 Procedure Call Standard는 `X0-X7` 여덟 인자 레지스터를 사용하고, 남은 인자를 `[SP]`의 스택에 두며, 반환 주소는 `X30`에 저장함
- ## 외부 라이브러리 호출
  - 번역된 x64 호출이 외부 라이브러리를 목표로 하면 AArch64 호출 규약에 맞게 인자 레이아웃을 바꿔야 함
  - 먼저 `SP`에서 8을 빼 16바이트 경계에 다시 맞추고, 이미 스택에 있던 x64 반환 주소를 `[SP+0x8]`에 둠
  - `[SP+0x10]`, `[SP+0x18]` 위치의 값을 `X6`, `X7`로 로드해 x64 코드가 스택에 둔 잠재적 7번째, 8번째 인자를 AArch64 라이브러리가 볼 수 있게 함
  - 남은 잠재 스택 인자는 `[SP+0x20]`부터 남아 있어 AArch64가 기대하는 위치와 맞지 않음
  - x64 반환 주소와 `X6`, `X7`로 옮긴 값을 스택에서 제거하는 방식은 해당 값이 실제 인자가 아니라 caller spill space나 caller 스택에 할당된 구조체 일부일 수 있어 안전하지 않음
  - Elevator는 caller의 스택 레이아웃을 건드리지 않고 `n×8` 바이트의 추가 스택 공간을 할당한 뒤, 현재 위치에서 잠재 8바이트 인자 `n`개를 복사함
  - 기본 `n`은 10이며, 입력 바이너리가 외부 라이브러리 함수에 총 16개보다 많은 인자를 전달하면 설정으로 늘릴 수 있음
  - 마지막으로 외부 라이브러리가 돌아올 gadget 주소를 `X30`에 저장함
- ## 외부 라이브러리에서 돌아오기
  - 외부 라이브러리 호출 전 `X30`에 저장한 gadget으로 제어가 돌아오면, 이전에 복사한 스택 인자를 정리하기 위해 스택 포인터에 `n×8`을 더함
  - 외부 라이브러리 반환값을 `X0`에서 emulated x64 코드가 기대하는 `RAX` 위치인 `X9`로 이동함
  - 원본 x64 반환 주소와 관련 패딩을 스택에서 꺼내 주소를 번역한 뒤 그곳으로 분기해 원래 `CALL` 다음 실행을 재개함
- ## 번역 코드로 들어오는 콜백
  - native AArch64 코드가 번역된 바이너리를 호출하면 AArch64 호출 규약을 x64 호출 규약으로 변환해야 함
  - emulated x64 코드는 7번째와 8번째 인자를 `X6`, `X7`이 아니라 스택에서 기대하므로, `X7`을 먼저 푸시하고 그다음 `X6`을 푸시해 x64가 기대하는 스택 위치에 배치함
  - callee가 실제로 7번째와 8번째 인자를 기대하지 않으면 이 푸시된 값들은 영향을 주지 않음
  - 외부 라이브러리의 AArch64 branch-and-link 명령이 `X30`에 넣은 반환 주소를 x64 반환 명령이 기대하는 스택 위치에 푸시함
- ## 콜백에서 외부 라이브러리로 반환
  - 번역된 코드가 콜백에서 외부 라이브러리로 돌아갈 때는 진입 과정을 반대로 수행함
  - 반환 주소를 스택에서 꺼내고, `X6`와 `X7`을 푸시하며 할당한 스택 공간은 스택 포인터에 `0x10`을 더해 정리함

## Comments



### Comment 57490

- Author: neo
- Created: 2026-05-15T04:57:11+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48117810) 
- QEMU의 사용자 모드 JIT이 정확히 뭘 하는지는 모르겠지만, 개선 여지가 꽤 커 보임  
  2013년에 **x86-64에서 aarch64로 변환하는 JIT 엔진**을 만들었고, 당시 Fedora 베타 aarch64 바이너리를 실행하며 x86_64 Linux에서 Fedora의 aarch64 포트 대부분을 다시 빌드할 수 있었음  
  반대 방향인 aarch64 → x86-64 JIT도 만들었고, 재미로 같은 프로세스 안에서 x86-64 → aarch64 → x86_64 식으로 두 JIT이 서로를 루프백 형태로 실행하는 것도 보여줬음  
  내가 만든 JIT은 명령어와 CPU 상태를 1대다로 매핑했고, 네이티브 재컴파일 코드 대비 대략 2~5배 느린 정도였음  
  나중에 QEMU JIT과 비교해보니 QEMU는 10~50배 느린 범위처럼 보였음  
  아쉽게도 오픈소스 라이선스 설정이 아니어서 증명할 코드를 공개할 수는 없었음
  - 맞음, **QEMU JIT**은 이기기 쉬운 목표에 가까움  
    특히 설계를 “x86에서 aarch64만”, “사용자 모드만”으로 특화해도 된다면 얻을 수 있는 성능 이득이 많음  
    QEMU의 사용자 모드 지원은 시스템 에뮬레이션 지원에 붙은 “어쩌다 보니 동작하는” 부록에 가깝고, 전체 JIT 구조도 “게스트 → 중간 표현 → 호스트” 방식이라 여러 게스트 아키텍처와 여러 호스트 아키텍처를 지원하기엔 좋지만, “x86은 정수 레지스터가 적으니 하드 할당할 수 있다”거나 “aarch64 CPU를 적절한 모드로 두면 복잡한 부동소수점 의미론이 항상 맞는다” 같은 특정 게스트/호스트 조합의 성질을 활용하기는 어려움  
    게다가 QEMU 개발에서는 성능 최적화 기회를 찾는 일보다 “새 아키텍처 기능 X를 에뮬레이션하기”에 더 많은 시간이 들어가는데, 개발 비용을 대는 쪽이 그걸 더 중요하게 보기 때문임
  - QEMU는 변환기라기보다 **TCG**이고, n개 아키텍처에서 동작하도록 설계됐기 때문에 한계가 있음

- `.text` 섹션이 **50배 커지는 것**은 엄청나지만, 완전 결정적 변환을 얻기 위한 대가로는 납득 가능한 수준처럼 보임  
  많은 경우 크기 증가의 불편함보다 에뮬레이션 대비 성능 차이가 더 클 것임  
  멀티스레딩과 예외 처리가 불가능한 게 아니라 이 프로젝트의 범위 밖이라는 점도 흥미로움  
  다음 단계는 휴리스틱으로 가능성 공간을 잘라내 바이너리 크기를 줄이는 것일지 궁금함  
  그러면 변환 보장은 깨지겠지만 바이너리 이식성은 현실적으로 좋아질 수 있음
  - 에뮬레이션 대비 성능 차이가 더 클 거라는 건 아님  
    이 변환기는 **Box64**나 **FEX**보다 훨씬 느리고, 어떤 이유로든 JIT을 쓸 수 없는 상황이 아니라면 그냥 더 나쁜 선택임

- 번역기가 **간접 점프**를 어떻게 처리하는지 늘 궁금했음  
  바이너리를 분석할 때는 목적지 주소를 아는 직접 점프로 연결된 코드 구간만 발견할 수 있음  
  그러면 간접 점프가 발생할 때마다 대상 함수를 찾고, 필요하면 번역한 뒤 번역된 코드로 돌아가야 한다는 뜻인데, 느리지 않나?  
  더 빠른 방법이 있는지, 번역된 함수 주소를 원래 함수 주소와 맞출 수 있는지, 아니면 원래 주소에 번역된 코드로 가는 점프를 넣는지 궁금함
  - 내가 만든 번역기는 취미 수준이지만, “주소 X로 간접 `jmp`하면 대응 블록은 위치 Y에 있다”는 큰 테이블을 둠  
    이 방식은 테이블을 쓰지 않는 직접 `jmp`보다 느리지만, 원래 프로그램에서도 간접 점프는 애초에 더 느렸고 보통 성능에 중요한 루프 안에서는 자주 나오지 않음

- **상위 집합 제어 흐름 그래프** 아이디어가 정말 마음에 들지만, 글을 읽으려는 사람이라면 아래 내용은 알아둘 만함  
  실행 시간은 약 4.75배 빨라짐(QEMU보다 빠르지만 Box64보다는 상당히 느림), 실행 명령어 수는 7배 증가, 바이너리 크기는 50배 증가함  
  외부 호출 전까지 x86 ABI를 에뮬레이션함  
  EFLAGS 같은 x86 CPU 상태의 큰 부분을 에뮬레이션해야 하고, 복잡한 `mov`도 개별적으로 계산해야 함  
  단일 스레드 바이너리만 지원함  
  예외 처리와 스택 풀기(unwinding)는 없음  
  전체 명령어 집합을 지원하지는 않음

- 흥미로운 작업임  
  자세히 보지는 않았지만, 상대 오프셋은 여전히 문제가 될 수 있을 것 같음  
  어차피 코드 생성 결과의 크기가 달라질 테니 일종의 번역 계층이나 MMU가 있어야 할 것 같고, 주로 점프 테이블과 내부 분기에 영향을 줄 듯함  
  주로 90년대 물건을 다루는데, 역어셈블러는 코드의 시작과 끝에 대해 많은 가정을 함  
  하지만 가끔은 고정 위치의 엔트리 포인트 포인터 같은 사전 지식이 없으면 바이너리 덩어리를 발견할 수 없는 경우도 있음  
  몇 번의 패스를 거치면 바이너리를 “확실히 코드인 영역”으로 정제할 수 있을 것 같음

- “Elevator는 모든 바이트의 가능한 해석을 모두 고려하고, 가능한 각각에 대해 별도 번역을 미리 생성하며 [...] 비정상 종료로 이어지는 경우만 가지치기한다”면, **충돌 가능성이 있는 실제 프로그램**은 전부 가지치기되는 건가?
  - 아마 주소→코드 조회 테이블에서 표준화된 충돌 경로로 설정할 것 같음  
    그러면 여전히 충돌은 나지만, 직접 실행된 잘못된 코드의 충돌과 같지는 않을 것임

- 나에게 가장 흥미로운 부분은 **인증** 관점임  
  항공, 의료기기 같은 규제 산업에서는 실행되는 코드가 인증받은 코드여야 해서 정확히 이런 이유로 JIT을 못 쓰는 경우가 많음  
  서명 가능한 바이너리를 만들어내는 정적 변환은 코드 팽창을 감수하더라도 실질적인 돌파구가 될 수 있음
  - 소프트웨어 산업에서 이 영역이 얼마나 큰지 궁금함  
    아마 이쪽은 LLM도 대규모로 적용할 방법이 없을 텐데, “업무에서의 AI”라는 큰 담론에서는 이런 부분이 거의 다뤄지지 않음

- **50배**는 합리적이지 않고, 캐시 재앙임  
  JIT을 피해서 얻는 성능 이득이 전부 잡아먹힐 수 있음
  - 실제 실행 시간에 그 코드가 전부 쓰일 때만 그렇고, 가능한 디코딩 시작점의 대다수는 아마 사용되지 않을 것임
  - 이건 **링크 시점 코드 재배치**에 아주 잘 맞는 사례임  
    뜨거운 코드를 한곳에 모아두면 사용되지 않는 코드는 절대 로드되지 않게 만들 수 있음
  - 성급히 결론 내리지는 않겠음  
    명령어는 어차피 그렇게 크지 않고, CPU가 실행 중에 최적화하기도 함

- **자기 수정 코드**를 처리할 수 있나?  
  왜 x86_64만인지도 궁금함  
  오래된 게임 같은 32비트 프로그램을 변환하는 쪽이 더 의미 있어 보임
  - 링크된 글을 읽어보면 이 부분을 명시적으로 다룸  
    “자기 수정 및 JIT 컴파일 코드. Elevator는 모든 완전 정적 바이너리 재작성기와 마찬가지로 자기 수정 코드나 JIT 컴파일 코드를 지원하지 않는다”
  - JIT 런타임 바깥의 자기 수정 코드는 요즘 80~90년대에 비하면 꽤 드문 편이라고 봄  
    요즘 `.text` 섹션은 대부분 읽기 전용이고, 보안 요구사항이 줄어들 일도 없을 것임
  - 자기 수정 코드를 처리한다면 더 이상 “완전 정적”이 아니게 됨  
    근본적으로 모순됨
  - 새로 x86을 개발하는 쪽에서 보면, **자기 수정 코드**는 가능하긴 해도 보통 끔찍함  
    캐시 라인과 파이프라인 분기 예측 성능을 망가뜨리기 때문임  
    또한 W^X를 위반하므로 보통 JIT 호환 메모리 페이지에서만 써야 함  
    그래서 거의 항상 피해야 함  
    486이나 P5 시절에는 즉시값을 내부 루프 변수처럼 쓰는 식으로 어느 정도 쓰였지만, 지금은 별로 그렇지 않음  
    완벽에 가까운 에뮬레이션이나 번역을 달성하려면 처리해야 할 x86의 지저분한 예외 사례가 많음

- **소스 코드**는 어디에 있나?
