휴리스틱 없는 결정론적 완전 정적 전체 바이너리 번역
(arxiv.org)- 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을 더해 정리함
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개 아키텍처에서 동작하도록 설계됐기 때문에 한계가 있음
- 맞음, QEMU JIT은 이기기 쉬운 목표에 가까움
-
.text섹션이 50배 커지는 것은 엄청나지만, 완전 결정적 변환을 얻기 위한 대가로는 납득 가능한 수준처럼 보임
많은 경우 크기 증가의 불편함보다 에뮬레이션 대비 성능 차이가 더 클 것임
멀티스레딩과 예외 처리가 불가능한 게 아니라 이 프로젝트의 범위 밖이라는 점도 흥미로움
다음 단계는 휴리스틱으로 가능성 공간을 잘라내 바이너리 크기를 줄이는 것일지 궁금함
그러면 변환 보장은 깨지겠지만 바이너리 이식성은 현실적으로 좋아질 수 있음- 에뮬레이션 대비 성능 차이가 더 클 거라는 건 아님
이 변환기는 Box64나 FEX보다 훨씬 느리고, 어떤 이유로든 JIT을 쓸 수 없는 상황이 아니라면 그냥 더 나쁜 선택임
- 에뮬레이션 대비 성능 차이가 더 클 거라는 건 아님
-
번역기가 간접 점프를 어떻게 처리하는지 늘 궁금했음
바이너리를 분석할 때는 목적지 주소를 아는 직접 점프로 연결된 코드 구간만 발견할 수 있음
그러면 간접 점프가 발생할 때마다 대상 함수를 찾고, 필요하면 번역한 뒤 번역된 코드로 돌아가야 한다는 뜻인데, 느리지 않나?
더 빠른 방법이 있는지, 번역된 함수 주소를 원래 함수 주소와 맞출 수 있는지, 아니면 원래 주소에 번역된 코드로 가는 점프를 넣는지 궁금함- 내가 만든 번역기는 취미 수준이지만, “주소 X로 간접
jmp하면 대응 블록은 위치 Y에 있다”는 큰 테이블을 둠
이 방식은 테이블을 쓰지 않는 직접jmp보다 느리지만, 원래 프로그램에서도 간접 점프는 애초에 더 느렸고 보통 성능에 중요한 루프 안에서는 자주 나오지 않음
- 내가 만든 번역기는 취미 수준이지만, “주소 X로 간접
-
상위 집합 제어 흐름 그래프 아이디어가 정말 마음에 들지만, 글을 읽으려는 사람이라면 아래 내용은 알아둘 만함
실행 시간은 약 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의 지저분한 예외 사례가 많음
- 링크된 글을 읽어보면 이 부분을 명시적으로 다룸
-
소스 코드는 어디에 있나?