2P by GN⁺ | ★ favorite | 댓글 1개
  • PS2의 VU 부동소수점 곱셈에는 1비트 연산 오차가 있어, 특정 값에서는 1 * XX와 달라질 수 있음
  • VU 개발자 매뉴얼 기준으로 X * 1은 정확성이 보장되지만 1 * X는 같은 보장이 없어, 이 차이가 에뮬레이터 감지 신호가 됨
  • 예제는 브루트포스로 찾은 문제 발생 값 중 129.5f를 사용해 실제 PS2와 에뮬레이터의 동작 차이를 확인함
  • 구현은 VU0 매크로 모드에서 129.5f1을 곱한 뒤, 원래 입력과 결과가 다른지만 비교하는 단순한 구조임
  • PCSX2, Play!, DobieStation, hps2x64는 현재 이 동작을 에뮬레이트하지 않으며, 감지 난이도는 1/5로 평가됨

PS2 VU 곱셈에서 생기는 1비트 오차

  • 이 방법은 PS2 에뮬레이터 감지 시리즈의 두 번째 항목이며, VU1, VU0 마이크로 모드, VU0 매크로 모드에서 사용할 수 있음
  • 예시는 구현을 단순하게 만들기 위해 VU0 매크로 모드를 사용함
    • VU0를 코프로세서처럼 사용하므로 EE CPU에서 직접 실행 가능함
    • 별도의 VU 프로그램을 다루지 않아도 됨
  • VU 개발자 매뉴얼의 MUL, MULi 같은 곱셈 명령어에는 1비트 연산 오차가 있다는 주석이 있음
    • 1 * X는 원래 값 X와 다를 수 있음
    • VF[fs]를 피승수로 사용하면 X * 1 형태의 결과 정확성이 보장됨
  • 비트가 왜 손실되는지는 정확히 확인되지 않음

감지값과 구현 방식

  • 이 오차를 감지하려면 문제를 유발하는 숫자가 필요하며, 가장 쉬운 탐색 방법은 브루트포스
  • 글쓴이는 과거에 0.5 간격으로 문제를 일으키는 첫 250개 숫자 목록을 만들었고, 해당 목록을 gist에 공개함
  • 예제 코드는 129.5f를 감지 대상 숫자로 사용함
    • QMTC2VF1129.5f를 설정함
    • VADDwVF21을 만듦
    • VMULVF1 = 1 * 129.5f를 계산함
    • QMFC2로 결과를 EE 쪽으로 가져와 입력과 비교함
  • 반환값은 in[0] != out[0]이며, 원래 값과 곱셈 결과가 다르면 VU 곱셈 오차가 존재한다고 판단함

에뮬레이터별 영향

  • 현재 PCSX2, Play!, DobieStation, hps2x64는 이 PS2 VU 곱셈 동작을 에뮬레이트하지 않음
  • 숫자 하나와 1을 곱한 뒤 결과만 확인하면 되기 때문에, 이 감지 방식의 난이도는 1/5 수준임

댓글과 토론

Hacker News 의견들
  • 오래된 ARM 에뮬레이션을 감지하는 가장 단순한 요령으로, Game Boy Advance 복사 방지에도 쓰였던 것 같음: PC+4 위치, 즉 바로 다음 명령어에 함정 명령을 저장하는 방식임
    실제 ARM은 파이프라인 때문에 PC에서 실행하고 PC+4를 디코딩하는 동안 PC+8을 읽으므로, 새로 저장한 명령은 영향을 주지 않아야 함. 하드웨어 파이프라인을 에뮬레이션하지 않는 에뮬레이터라면 그 명령을 실행하게 됨
    2004년의 여러 에뮬레이션 방해 기법과 함께 더 자세히 설명된 글: https://mgba.io//2014/12/28/classic-nes/
    • Texas Instruments TI320C40 디지털 신호 처리기는 파이프라인 문제가 더 이상했음: 분기 뒤의 하나 이상 명령이 실제 분기 전에 실행되는 분기 지연 슬롯(https://en.wikipedia.org/wiki/Delay_slot)이 있었고, 레지스터에 저장한 값이 몇 명령 뒤에야 나타나는 적재 지연 슬롯도 있었음
      아마 몇 사이클 동안 레지스터 값이 정의되지 않았던 것 같음. 이런 칩에 빡빡하게 최적화한 어셈블리 코드를 쓰는 건 유난히 취향 나쁜 Zachtronics 클론을 하는 느낌이라 꽤 끔찍했음
    • x86에서도 프리페치 큐가 비슷한 동작을 만들었지만, Intel이 Pentium 이후 CPU에서 자기 수정 코드를 감지하기로 하면서 곧 실행될 명령을 수정하면 항상 효과가 나게 됨
      훨씬 나중에 누군가 감지되지 않는 또 다른 경계 사례를 찾았는데, 자기 자신을 덮어쓰는 반복 문자열 명령이었음
      https://silviocesare.wordpress.com/2009/02/02/anti-debugging...
    • 일부 파이프라인 CPU는 자기 수정 코드와의 호환성을 유지해서, 파이프라인에 올라와 있는 명령을 덮어쓰면 이를 감지하고 파이프라인을 비움
      x86에는 그런 장치가 있는데, 64비트 변형에서 결국 제거했는지는 잘 모르겠음
    • 이게 VisualBoyAdvance에서 Dragon Ball Z: The Legacy of Goku II 롬이 가끔 동작하지 않았던 이유인가?
  • 100% 정확도를 목표로 하는 에뮬레이션이 업계에서 장인정신의 영역인 이유가 바로 이것임
    원본 하드웨어와 소프트웨어의 모든 특이 동작을 알아야 할 뿐 아니라, 아무리 기묘해도 그대로 재현해야 함. 그 자체로도 어렵지만 성능 영향까지 고려해야 함
    • 에뮬레이터는 정확도에 대해 현실적이어야 함. 더 현대적인 시스템을 에뮬레이션할 때는 100% 하드웨어 정확도와 쓸 만한 성능을 동시에 목표로 삼는 게 보통 불가능하므로, 실제 하드웨어와 기술적으로는 다르지만 실제로 관찰 가능한 차이는 거의 없는 절충을 받아들이게 됨
      JIT 재컴파일러를 쓰는 것은 원본 하드웨어와 완벽히 사이클 단위로 정확할 수 없지만, 게임 코드가 의도적으로 에뮬레이터를 깨도록 만들어진 경우가 아니라면 대체로 문제되지 않음
      Dolphin도 몇몇 상용 Wii 게임이 실제 Wii CPU의 캐시 동작 세부사항을 악용한 에뮬레이터 방지 코드를 넣었을 때 이 균형을 다뤄야 했음. 이론적으로는 실제 CPU 캐시를 에뮬레이션해 게임을 매끄럽게 돌릴 수 있었지만, 성능 오버헤드가 아마 10배 느려지는 수준이라 플레이할 수 없었을 것이므로 우회 패치를 택했음
      https://dolphin-emu.org/blog/2017/02/01/dolphin-progress-rep...
  • 에뮬레이션은 어떻게 입문하는 걸까? 소프트웨어 안에서도 엄청나게 어려운 틈새 분야처럼 보임
    사실상 전자공학과 깊은 프로그래밍 마법을 이해해야 하는 것 같음
    • 다른 것과 마찬가지로 점진적으로 시작하면 됨
      시작할 때, 그리고 대부분은 끝낼 때도 깊은 마법을 이해할 필요는 없음. 대체로 명세를 보고 그 내용대로 구현하면 됨. 엉망이 되지 않게 코드 구조를 잡는 능력은 필요하지만, 흔한 패턴이 있고 에뮬레이터를 한두 개 만들어보면 훨씬 쉬워짐
      전자공학을 이해해야 하는 경우도 거의 없음. 에뮬레이션하는 것은 동작뿐임. 원본 하드웨어 동작의 버그가 발견되면 보통 에뮬레이터에 특수 처리를 넣으면 됨. 그런 동작이 왜 생겼는지 이해하는 데 전자공학 지식이 도움이 될 수는 있지만, 실용적이라기보다는 역사적 흥미에 가까움
      고유한 어려움은 있음. 문제가 생기면 보통 하드웨어에 대한 이해, 에뮬레이터 구현, 에뮬레이션 중인 게임이라는 세 가지를 동시에 디버깅하게 됨. 정확한 원인을 좁히기 어려울 수 있음. 그래도 일단 대충 만들어보는 걸 권함. 깔끔하진 않지만 모든 에뮬레이터는 인기 게임을 어떻게든 동작시키기 위한 특수 처리로 가득함. 지저분한 해킹 몇 개로 게임이 돌아간다면 그렇게 하면 됨. 원본 하드웨어 동작을 정확히 구현할 필요는 없고, 게임이 돌아가게 만들면 됨
    • 하드웨어 문서를 읽는 것부터 시작하면 되고, 기계를 전자 회로 수준으로 이해할 필요는 없음. 디지털 회로 시뮬레이션이 아니므로 그렇게 복잡할 필요가 없음
      8비트 CPU는 몇 바이트의 상태, 즉 레지스터만 가진 단순한 상태 기계임. 프로그램을 한 바이트씩 읽고, CPU가 그 바이트를 읽은 뒤 할 동작을 흉내 내면 됨. 숫자를 더하고 빼거나, 바이트를 읽고 저장하는 식의 아주 단순한 연산들임
      http://www.6502.org/users/obelisk/6502/registers.html
      http://www.6502.org/users/obelisk/6502/instructions.html
      6502 CPU 에뮬레이터는 프로그램의 다음 몇 바이트를 읽고, 그 바이트들을 명령어로 해석하고, 명령어를 실행함. 이 과정에서 CPU의 레지스터나 카운터 몇 개를 갱신하고, 산술 또는 비트 연산을 수행하며, 필요하면 한 위치에서 다른 위치로 데이터 1바이트를 읽거나 저장함. 이 과정을 무한 루프로 반복함
      이것이 인출-해독-실행 주기의 시뮬레이션임
      https://en.wikipedia.org/wiki/Instruction_cycle
    • 에뮬레이션이라는 말로 무엇을 뜻하느냐에 따라 다름
      예전에 SID 음악 파일을 재생하려고 6502 인터프리터를 UNIX에서 Classic Macintosh로 포팅한 적이 있음. 충분히 빠르게 돌기만 하면 됐기 때문에 클록 사이클 정확도는 중요하지 않았음
      인터프리터에서 C 코드를 호출하는 방식으로 동작했음
    • 오래전에 입문해보려 했을 때는, 아주 단순하고 문서화가 잘된 것부터 시작해서 거기서 실력을 쌓으라는 지침이었음
      아직도 해보고 싶은 마음은 있지만 시간이 없음
    • 홍보를 조금 하자면, 지난 FOSDEM에서 이 주제로 발표했음: https://fosdem.org/2024/schedule/event/fosdem-2024-2146-how-...
      그 외에는 @xcv123의 형제 댓글이 정말 맞는 말임
  • 소프트웨어 에뮬레이션에서는 아마 신경 쓸 가치가 별로 없는 종류의 흥미로운 예시임. 그 버그까지 에뮬레이션하면 상당히 느려질 것임
    언젠가 FPGA로 PS2를 복제하는 것이 가능해지면, 이 동작이 어떻게 일어났는지 밝혀내는 일이 누군가에게 재미있는 프로젝트가 될 것임
    • FPGA 구현도 소프트웨어 에뮬레이션 프로젝트의 코드나 문서를 바탕으로 만들어지는 경우가 많음
      PS2의 FPGA 버전이라고 해서 같거나 비슷한 버그를 구현하지 않으리라는 보장은 없음
    • 그 버그가 게임을 망가뜨리는지 여부에 달려 있음
    • 이 버그를 고치는 일은 다른 여러 부동소수점 버그, 더 구체적으로는 반올림과 클램핑 문제를 고치는 작업의 일부가 될 것임
      소프트웨어 부동소수점은 느리겠지만, 일반적인 해법은 아마 PS4의 PS2 에뮬레이터를 따를 것임. 각 게임별로 소프트웨어 부동소수점 경로를 허용할 코드 구간을 화이트리스트로 지정하는 방식임
    • 상대적으로 비싼 FPGA로 낡고 형편없는 MIPS CPU를 에뮬레이션하고 싶은 이유가 뭘까? 오래된 콘솔을 에뮬레이션하는 핵심은 하드웨어에 독립적으로, 컴퓨터나 휴대폰에서 예전 게임을 할 수 있게 하는 것임
  • 제목을 보고 마우스나 키보드가 왜 수학을 해야 하는지 헷갈린 사람이 또 있었나?
    이게 마우스와 키보드를 연결하는 Personal System/2 포트가 아니라 PlayStation 2 이야기라는 걸 알아내는 데 너무 오래 걸렸음
    • 하나는 PS2이고, 다른 하나는 PS/2임
    • 반대표를 누른 사람들에게: 자기에게 명확하다고 해서 모두에게 명확한 건 아님
      세 글자 약어는 맥락을 찾기 정말 어렵게 만들 수 있음. 약어만 Google에 넣으면 꽤 자주 거의 관련 없는 결과가 대부분 나오기 때문임