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의 지저분한 예외 사례가 많음
  • 소스 코드는 어디에 있나?