본문으로 건너뛰기

© 2026 Molayo

GN헤드라인2026. 05. 15. 06:53

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

요약

본 기사는 QEMU의 사용자 모드 JIT 엔진을 개선하고, 아키텍처 간 변환(x86-64 ↔ aarch64)에 대한 결정론적이고 완전한 정적 전체 바이너리 번역기 개발 과정을 다룹니다. 이 번역기는 명령어와 CPU 상태를 1대다로 매핑하며, 네이티브 재컴파일 대비 성능은 느리지만 QEMU의 JIT보다 훨씬 빠르다는 것을 보여줍니다. 특히 규제 산업에서 인증된 코드가 필수적인 환경에 실질적인 대안을 제시합니다.

핵심 포인트

  • 개발된 번역기는 명령어와 CPU 상태를 1대다로 매핑하는 결정론적 정적 전체 바이너리 변환 방식을 사용합니다.
  • 이 방식은 QEMU의 JIT 엔진보다 성능 면에서 우위를 점하며, 특히 특정 아키텍처 조합에 특화될 경우 큰 이점을 가집니다.
  • 전통적인 에뮬레이션(QEMU) 방식과 달리, 본 번역기는 실행되는 코드가 인증된 바이너리 형태로 존재해야 하는 규제 산업 환경에 적합한 대안을 제시합니다.
  • 성능 최적화를 위해 '휴리스틱'을 도입할 경우 바이너리 크기를 줄일 수 있지만, 변환의 완전성을 보장하기 어려워집니다.

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대다로 매핑했고, 네이티브 재컴파일 코드 대비 대략 25배 느린 정도였음
나중에 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의 지저분한 예외 사례가 많음

AI 자동 생성 콘텐츠

본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0