에뮬레이터는 파일 내의 바이너리 코드를 구문 분석합니까?

에뮬레이터는 파일 내의 바이너리 코드를 구문 분석합니까?

실행한다고 주장하는 일부 에뮬레이터를 본 적이 있는데, 실행하더라도 소스 코드에서는 명령을 결정하기 위해 모든 1과 0을 직접 구문 분석하지 않는다는 것을 보여줍니다.

내 질문은 에뮬레이터가 실제 CPU와 동일한 opcode를 에뮬레이션해야 한다면 CPU를 합법적으로(또는 전혀) 에뮬레이션하기 위해 게임의 올바른 바이너리 opcode 형식을 구문 분석해야 하지 않을까 하는 것입니다.

예를 들어, 게임 파일에는 다음과 같이 표시된 하나의 명령어, 1바이트를 저장합니다.

0000 1111

내 프로그램은 이 명령이 실제로 의미하는지 확인해야 합니다(예: "A 레지스터에 1 추가"). 그러나 이를 보장하기 위해 텍스트 파일의 모든 0과 1을 확인할 필요가 있지 않습니까?

그런 다음 에뮬레이터는 전체 바이트를 구문 분석하지만 전체 바이트는 다시 8비트이고 변동 패턴이 작업 출력을 변경합니다.

예를 들어 0000 1111은 A에 1을 더한다는 의미일 수 있지만 0000 1110은 A에 A를 더한다는 의미일 수 있습니다.

답변1

박람회 - 질문에 직접적으로 답하려고 노력함

에뮬레이터의 소스 코드를 읽고 있는데 바이너리(실행 가능) 파일의 특정 비트를 읽지 않고 여전히 코드를 충실하게 실행하는 경우 가능한 결과는 세 가지입니다.

  1. 당신은잘못된에뮬레이터가 파일의 모든 비트를 읽지는 않는다고 생각하면실제로는 그렇지, 당신은 단순히 착각하고 있습니다.
  2. 당신은옳은, 그리고 에뮬레이터는 모든 단일 비트를 읽지 않습니다. 왜냐하면 에뮬레이터는 수행해야 할 작업을 알기 위해 모든 단일 비트를 읽을 필요가 없도록 에뮬레이션하는 프로그램의 동작에 대한 특정 사실을 가정할 수 있기 때문입니다(아마도 실행할 특정 게임 엔진, 특정 유형의 그래픽 API, 특정 유형의 사운드 API 등).
  3. 당신은옳은, 그리고 에뮬레이터는 프로그램을 올바르게 실행하는 데 필요하지 않은 실행 파일의 특정 비트가 있기 때문에 모든 단일 비트를 읽지 않습니다. 그것들은 레거시 "엉터리"이거나 메타데이터이거나 실제로 프로그램 기능으로 구성되지 않는 단순한 추가 보풀일 수 있습니다.
  4. 당신은옳은, 그리고 에뮬레이터는 코드의 특정 작업을 더 높은 수준의 작업으로 변환하고 하위 수준의 프로세서/하드웨어 관련 명령을 완전히 우회하기 때문에 모든 단일 비트를 읽지 않습니다. 예를 들어, 복잡한 작업을 수행하는 사람의 비디오 녹화에서 그 사람이 무엇을 하고 있는지 정확히 흉내내라는 요청을 받고 "이제 상자 측면에 구멍을 뚫으세요"라고 말한다면 멈추고 싶은 유혹이 생길 것입니다. 비디오를 시청하고 비디오에 나오는 사람의 문자 그대로 동작을 따르는 대신 사물에 구멍을 뚫는 방법에 대한 기존 경험을 활용하십시오(적절한 드릴을 갖추고 있고 일반적으로 생활 경험이 있다고 가정). 마찬가지로, 에뮬레이터가 프로그램이 주어진 좌표 세트에서 화면에 32x32 이미지를 그리도록 요청한다고 추론할 수 있는 경우, 그것이 어떤 이미지인지, 어떤 형식인지 이해하자마자 코드 읽기를 중지할 수 있습니다. 그리고 그것을 어디에 그릴지 - 에뮬레이트된 시스템이 그것을 어떻게 그리는지 볼 필요가 없습니다.

에뮬레이터 작동 방식

다른 플랫폼 및/또는 CPU에 대한 코드를 실행하는 에뮬레이터(예:와인)는 다양한 단계에서 작업을 수행합니다. 에뮬레이터가 작동하려면 일부 단계가 반드시 필요합니다. 다른 단계는 선택 사항이며 성능 최적화 가능성을 나타냅니다.

  • 필수의: 실행 가능한 코드(기계어 코드, MSIL, Java 바이트코드 등)를 "파싱"합니다.파싱으로 구성되다:

    • 실행 가능한 코드의 각 비트를 읽습니다.
    • 네이티브 코드의 각 비트/바이트(또는 사용하려는 기타 개별 정보 측정 단위)의 레이아웃/형식(구문) 및 목적(의미)을 충분히 이해하여 네이티브 코드가 수행하는 작업을 이해합니다.
    • 이해하다무엇프로그램에서는 에뮬레이터가 다음을 이해해야 한다고 말합니다.통사론바이너리 형식의의미론. 구문은 "32비트 부호 있는 정수를 최소 부호 비트 형식으로 표현합니다"와 같은 것으로 구성됩니다. 의미론은 "네이티브 코드에 opcode가 포함된 경우"와 같은 것으로 구성됩니다.52, 이는 함수 호출을 의미합니다."
    • 니모닉(이것이 왜 필요한지 기억하는 데 도움이 됩니다): 내가 레시피를 따르는데 전념했다면, 그 레시피를 완전히 무시하고 심지어읽다즉, 내가 무작위로 여러 가지를 시도해 보지 않고는 그 방법을 따라갈 수 없다는 것입니다.레시피에 필요한 것과 동일한 단계를 수행합니다. 마찬가지로, 프로그램과 동일한 기능을 수행할 때까지 무작위 CPU 명령을 실행하는 무작위 몬테카를로 시뮬레이션이 없다면 모든 에뮬레이터는 이를 이해해야 합니다.무엇프로그램이 말합니다.

  • 필수의: 구문 분석된 코드(보통 일종의 추상 데이터 모델, 상태 머신, 추상 구문 트리 등)를 다음 중 하나로 "번역"합니다.높은 레벨명령(예: C 또는 Java의 명령문) 또는낮은 수준의명령(예: x86 프로세서에 대한 CPU 명령). 높은 수준의 명령이 더 최적인 경향이 있습니다. 예를 들어, 긴 CPU 명령어 시퀀스의 코드 흐름을 분석하고 높은 수준에서 요청하는 것이 디스크에서 특정 MP3 파일을 재생하는 것이라고 판단하는 경우 전체 명령어 수준 에뮬레이션을 건너뛰고 기본 명령어만 사용할 수 있습니다. 플랫폼의 MP3 디코더(프로세서에 최적화될 수 있음)를 사용하여 동일한 MP3 파일을 재생합니다. 반면에 에뮬레이트된 프로그램의 실행을 가능한 한 문자 그대로 "추적"하려는 경우 명령을 기본적으로 실행함으로써 얻을 수 있는 최적화의 대부분을 포기하게 되므로 속도가 느려지고 최적이 아닙니다.

  • 선택 과목: 에뮬레이트된 프로그램 코드 또는 전체 프로그램의 대규모 코드 흐름을 "최적화"하고 분석하여 전체 실행 순서를 결정하고 에뮬레이터가 이 동작을 에뮬레이트하는 방법에 대한 매우 상세하고 정교한 모델을 구성합니다. 네이티브 플랫폼의 기능을 활용합니다. Wine은 어느 정도 이 작업을 수행하지만 번역하는 코드가 x86에서 x86으로 변환된다는 사실이 도움이 됩니다. 즉, 두 경우 모두 CPU가 동일한 명령어 세트이므로 Windows를 연결하기만 하면 됩니다. 코드를 외부 UNIX 기반 환경에 추가하고 "기본적으로" 실행되도록 합니다.


케이크 비유

에뮬레이터의 성능을 고려할 때 다음 시나리오에서 누군가가 케이크를 굽는 비디오(오디오 포함)를 보고 있는 경우 직접 지침을 작성하는 데 몇 장의 종이가 필요한지 생각해 보세요.

  • 만약 당신이 인생에서 이전에 손을 움직이거나 몸의 근육을 운동한 적이 없다면;(힌트: 손 움직임, 손과 눈의 협응, 각도, 속도, 위치, 잡기, 도구 잡기, 반죽 등과 같은 기본 기술의 세부 단계를 문서화하려면 수천 장의 종이가 필요합니다.)

  • 기본적인 운동 조절 능력은 있지만(걷고 스스로 식사할 수 있음), 평생 동안 음식을 준비한 적이 없는 경우;(힌트: 개별 단계를 문서화하려면 수십 장의 종이가 필요하고 반죽하고 익숙하지 않은 도구를 잡는 것과 같은 작업을 익히려면 많은 연습이 필요할 수 있지만 훨씬 적은 시간에 문서화할 수 있습니다. 이전 사례보다 시간이 더 걸렸습니다)

  • 인생에서 케이크를 구운 적이 없지만 음식 준비는 해 본 적이 있다면;(힌트: 종이 두 장 정도가 필요하지만 10장을 넘지 않아야 합니다. 이미 재료 계량, 젓기 등에 익숙할 것입니다.)

  • 이전에 케이크를 여러 번 구웠고 그 과정에 매우 익숙하지만 특정 종류/맛의 케이크를 굽는 방법을 모르는 경우(힌트: 기본 재료와 오븐에 필요한 시간을 기록하려면 종이 반 장만 있으면 됩니다.).

기본적으로 이렇게 증가하는 "에뮬레이터 역량" 수준에서 에뮬레이터는 "기본적으로"(이미 알고 있는 루틴 및 절차 사용) 더 높은 수준의 작업을 수행할 수 있으며 "추적"(본인이 알고 있는 루틴 및 절차 사용) 작업을 덜 수행해야 합니다. 문자 그대로 에뮬레이트된 프로그램을 따릅니다).

이 비유를 컴퓨터 용어로 표현하면 다음과 같은 에뮬레이터를 상상할 수 있습니다.실제 하드웨어를 에뮬레이트합니다.에뮬레이트된 프로그램은 하드웨어(회로) 수준까지 하드웨어에서 실행되고 해당 하드웨어의 동작을 충실하게 "추적"합니다. 이것은 것입니다매우사운드 파일을 재생하려고 할 때 이해할 수 있을 정도로 정교하게 프로그램을 분석하고 에뮬레이트된 프로그램의 지침을 추적할 필요 없이 해당 사운드 파일을 "기본적으로" 재생할 수 있는 에뮬레이터에 비해 속도가 느립니다.


"추적"(일명 암기 흉내)과 "기본 실행"

마지막 사항: 에뮬레이트하는 항목의 매우 상세하고 복잡한 구성 요소를 "복제"하기 위해 많은 메모리를 사용해야 하고 호스트 CPU에서 명령을 실행하는 대신 명령을 실행해야 하기 때문에 추적이 느립니다.명령어를 실행하는 명령어(간접 수준 참조), 이는 비효율성을 초래합니다. 컴퓨터 시스템의 물리적 하드웨어와 프로그램을 완전히 에뮬레이트한다면 CPU, 마더보드, 사운드 카드 등을 에뮬레이트하게 되며, 결과적으로 프로그램 실행을 "추적"하게 됩니다. 에뮬레이터는 CPU 실행을 "추적"하는데 이렇게 많은 수준의 추적을 사용하면 전체 작업이 매우 느리고 번거로워집니다.

다음은 에뮬레이터가 에뮬레이트하기 위해 입력 프로그램의 모든 비트/바이트를 읽을 필요가 없는 자세한 예입니다.

에뮬레이트된 소프트웨어 환경을 위해 C 또는 C++로 작성된 API(세부 사항은 중요하지 않음)를 알고 있으며 이 API에 기능이 있다고 가정해 보겠습니다 void playSound(string fileName). 이 함수의 의미는 디스크에서 파일을 열고, 그 내용을 읽고, 파일의 인코딩이 무엇인지(MP3? WAV? 다른 것?) 파악한 다음, 이를 스피커에서 재생하는 것임을 알고 있다고 가정해 보겠습니다. 일반/예상 샘플 속도 및 피치. 네이티브 코드에서 "사운드 재생을 시작하려면 playSound 루틴에 들어가세요"라는 일련의 명령을 읽으면 /home/hello/foo.mp3바로 프로그램 코드 읽기를 중단하고 다음을 사용할 수 있습니다.소유하다(최적화되었습니다!) 해당 사운드 파일을 기본적으로 열고 재생하는 루틴입니다. 명령어 수준에서 에뮬레이트된 프로세서를 따라야 합니까? 아니요, API가 수행하는 작업을 알고 있다고 신뢰하는 경우에는 실제로 그렇지 않습니다.


엄청난 차이가 발생합니다! (고지대에서의 문제)

물론 위의 예에서처럼 여러 지침을 읽고 높은 수준의 실행 계획을 "추론"하면 다음과 같은 위험이 발생할 수 있습니다.정확하게원래 하드웨어에서 실행되는 원래 프로그램의 동작을 모방합니다. 예를 들어, 원래 하드웨어에는 동시에 8개의 사운드 파일만 재생할 수 있는 하드웨어 제한이 있었을 수 있습니다. 새로운 컴퓨터가 128개의 사운드 파일을 동시에 잘 재생할 수 있고 playSound높은 수준에서 루틴을 에뮬레이트하는 경우, 한 번에 8개 이상의 사운드 파일을 재생하는 것을 막을 수 있는 방법은 무엇입니까? 이로 인해 에뮬레이트된 프로그램 버전에서 이상한 동작(좋든 나쁘든)이 발생할 수 있습니다. 이러한 경우는 주의 깊게 테스트하거나 원래 실행 환경을 잘 이해함으로써 해결할 수 있습니다.

예를 들어, DOSBox에는 에뮬레이트된 DOS 프로그램의 실행 속도를 의도적으로 제한할 수 있는 기능이 있습니다. 왜냐하면 일부 DOS 프로그램이 최고 속도로 실행되도록 허용하면 제대로 실행되지 않기 때문입니다. 실제로는 예상 속도로 실행하기 위해 CPU의 클럭 속도 타이밍에 의존했습니다. 실행 환경을 의도적으로 제한하는 이러한 유형의 "기능"은 다음과 같은 좋은 균형을 제공하는 데 사용될 수 있습니다.충실실행(즉, 에뮬레이트된 프로그램이 제대로 작동하도록 만드는 것) 및능률실행(즉, 최소한의 추적으로 효율적으로 에뮬레이트할 수 있을 만큼 충분히 높은 수준의 프로그램 표현을 구성하는 것)

답변2

내 질문은 에뮬레이터가 실제 CPU와 동일한 opcode를 에뮬레이션해야 한다면 CPU를 합법적으로(또는 전혀) 에뮬레이션하기 위해 게임의 올바른 바이너리 opcode 형식을 구문 분석해야 하지 않을까 하는 것입니다.

"파싱(Parse)"은 "텍스트를 살펴보고 내용이 무엇을 의미하는지 알아내는 것"을 의미합니다. 텍스트 및 언어와 유사한 구문은 개별 "단어"가 사물을 의미할 뿐만 아니라 위치 및 다른 단어와의 근접성에 따라 의미가 변경될 수 있기 때문에 복잡합니다.

아마도 CPU가 명령 스트림(파싱보다 훨씬 간단함)에 수행하는 작업과 관련하여 적용할 더 정확한 용어는 "디코딩"일 것입니다. 그렇습니다. 에뮬레이터는 동일한 방식으로 명령을 "디코딩"해야 합니다. 내 생각에 "파싱"은 x86 명령어 세트의 복잡성을 고려할 때 실제로 그렇게 나쁜 용어는 아닌 것 같습니다.

내 프로그램은 이 명령이 실제로 의미하는지 확인해야 합니다(예: "A 레지스터에 1 추가"). 그러나 이를 보장하기 위해 텍스트 파일의 모든 0과 1을 확인할 필요가 있지 않습니까?

CPU는 실제로 명령을 확인하지 않습니다. 모든 CPU에는 RAM의 메모리 위치를 가리키는 내부 레지스터가 있습니다. CPU는 이를 읽고, 필요한 경우 몇 바이트를 더 읽은 다음 실행을 시도합니다. 최신 CPU는 명령이 잘못된 경우 "예외 프로세스"를 시작합니다.

이전 8비트 6502와 같은 이전 CPU는 그렇게 하지도 않았습니다. 일부 불법 명령은 CPU를 잠그고 다른 명령은 이상하고 문서화되지 않은 작업을 수행했습니다. (일부 검색에서는 x86 판테온의 모든 CPU에서 발견된 문서화되지 않은 x86 명령어가 많이 드러납니다. 예를 들어 이 CPUID명령어는 Intel이 공식적으로 문서화하기 전에 일부 486 CPU에 존재했습니다.)

훌륭하고 정확한 CPU 에뮬레이터는 실제 CPU처럼 맹목적으로 명령 스트림을 씹을 뿐입니다. 하지만 대신 가상 CPU가 죽었다는 대화 상자를 표시하는 등 시스템 잠금을 유발하는 조건에서는 다른 작업을 수행할 수도 있습니다. 에뮬레이터를 잠그는 중입니다.

또한 "텍스트 파일의 모든 0과 1을 확인해야 합니다"라고 말하지만 일반적으로 에뮬레이터에 텍스트 파일을 제공하지 않습니다. CPU 에뮬레이터에는 에뮬레이트된 메모리가 필요합니다. 많은 플랫폼이 메모리 매핑된 I/O를 사용하므로 에뮬레이트된 I/O 장치도 사용합니다. 펌웨어와 같은 것을 에뮬레이션하려면 해당 펌웨어의 바이너리 이미지(실제 펌웨어의 합법적인 복사본 또는 대체 오픈 소스 펌웨어)가 필요합니다. 전체 플랫폼(예: x86 CPU가 구성 요소인 "PC 플랫폼")을 에뮬레이션하려면 CPU 에뮬레이션 이상의 것이 필요합니다.

관련 정보