1P by GN⁺ 3시간전 | ★ favorite | 댓글 1개
  • 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 2call 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, Reg8RFLAGS의 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바이트 정렬을 요구함
    • RSPSP에 직접 매핑하면 함수 프롤로그의 연속 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 반환 명령이 기대하는 스택 위치에 푸시함
  • 콜백에서 외부 라이브러리로 반환

    • 번역된 코드가 콜백에서 외부 라이브러리로 돌아갈 때는 진입 과정을 반대로 수행함
    • 반환 주소를 스택에서 꺼내고, X6X7을 푸시하며 할당한 스택 공간은 스택 포인터에 0x10을 더해 정리함
Hacker News 의견들
  • 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배 커지는 것은 엄청나지만, 완전 결정적 변환을 얻기 위한 대가로는 납득 가능한 수준처럼 보임
    많은 경우 크기 증가의 불편함보다 에뮬레이션 대비 성능 차이가 더 클 것임
    멀티스레딩과 예외 처리가 불가능한 게 아니라 이 프로젝트의 범위 밖이라는 점도 흥미로움
    다음 단계는 휴리스틱으로 가능성 공간을 잘라내 바이너리 크기를 줄이는 것일지 궁금함
    그러면 변환 보장은 깨지겠지만 바이너리 이식성은 현실적으로 좋아질 수 있음

    • 에뮬레이션 대비 성능 차이가 더 클 거라는 건 아님
      이 변환기는 Box64FEX보다 훨씬 느리고, 어떤 이유로든 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의 지저분한 예외 사례가 많음
  • 소스 코드는 어디에 있나?