명령어 포맷 인코딩(Instruction Format Encoding)

개요

개발자는 개발할 때 개발 언어를 통해 아이디어를 구체화합니다.

리버서는 만들어진 프로그램을 디스어셈블러를 통해 어셈블러로 번역해서 보거나 디컴파일러로 C/C++ 코드로 보기도 합니다.

그렇다면 CPU는 개발자가 컴파일러로 만든 최종 결과물 PE파일이나 ELF파일에서 명령어 단위를 어떻게 해석하는 것일까요?

Intel Manual에서는 CPU가 이를 해석하는 방식에 대해 설명하고 있습니다.

https://cdrdv2.intel.com/v1/dl/getContent/671110

Chapter2의 Instruction Format에 대해 설명한 글을 여러 자료와 함께 정리해보도록 하겠습니다.

Instruction Format

명령어 포맷은 위와 같이 순차적으로 나열되어 있으며, ModR/M부터 SIB, Diplacement, Immediate는 옵션입니다.

Prefix는 주로 Rex Prefix가 사용되어 명령어의 크기, 피연산자, 주소를 확장하는데 사용됩니다.

Opcode는 명령어로 1~3바이트가 사용되며 ModR/M의 reg 필드에서 추가로 3비트를 더 사용하기도 합니다.

ModR/M은 diplacement 사용 여부를 알려주는 Mode와 reg, r/m 필드를 가지고 있습니다.

SIB는 Scale, Index, Base로 이뤄져 있으며 이를 통해 배열과 같은 순차적인 요소에 쉽게 접근할 수 있습니다.

Diplacement는 메모리나 주소에 접근할 때 추가로 더해지는 값으로, 구조체의 멤버 변수에 접근할 때 사용합니다.

Immediate는 상수 값으로, 데이터의 값이 상수일 때 주로 사용됩니다.

Prefix

Prefix는 Opcode가 최대 3바이트+3비트의 제약을 갖고 있기에 이를 더 확장시키고자 사용하는 것입니다.

예를 들어 Opcode에서 mov 명령어는 32비트 형식의 Mov opcode까지만을 제공하는데, 여기서 x64비트 형식에

명령어를 추가하려면 위의 제약인 3바이트+3비트 크기를 넘어가게 됩니다.

Windows는 16비트부터 시작해서 32비트, 64비트로 단계별로 넘어오게 되었으며, 이들이 다 호환될 수 있도록 설계되었습니다.

따라서 예전부터 정해진 Opcode의 크기를 바꾼다는 것은 이전 버전의 윈도우와의 호환 체계의 문제가 발생하기 때문에 Prefix라는 개념을 도입하기 시작했습니다.

이런 방식을 통해 윈도우는 Opcode 자체만으로 명령어를 이해할 수도 있고, Prefix를 통해 Opcode를 추가적으로 해석할 수 있게 됩니다.

Intel Manual에서 크게 설명하는 Legacy Prefix는 총 4가지 그룹입니다.

Group 1 : Lock(0xF0) and Repeat Prefixes (0xF2, 0xF3)

Group 2 : Segment override Prefixes

Group 3 : Operand-size override Prefix (0x66)

Group 4 : Address-size override Prefix (0x67)

여기서 주로 사용되는 Prefix는 Group1과 Group3입니다.

Opcode

Opcode란 명령어(Instruction)에 할당된 숫자 값입니다.

Mov 명령어는 0x88, 0x89 등 여러 값으로 할당되어 있으며, 각 Opcode는 메모리나 레지스터의 크기가 16, 32, 64처럼 바뀝니다.

제가 자주 이용하는 사이트입니다. (Opcode Manual)

여기서 원하는 명령어를 검색하고 해당 내용을 찾아볼 수 있습니다.

ModR/M

Opcode Manual에서 Mov 명령어를 찾아가보면 아래와 같은 표를 보실 수 있습니다.

이때 Opcode 뒤에 /r이라고 써져 있는 것이 Mod R/M입니다.

Mod R/M은 mod, reg/opcode, r/m 필드로 총 3군데로 나뉘어집니다.

mod는 주소 지정 방식을 정합니다. (메모리에서 얼마만큼 displacement를 떠나 접근할 지 아니면 레지스터로 접근할 지 정합니다.)

reg/opcode는 사용할 레지스터를 지정하거나, Opcode를 확장할 때 사용합니다.Z

r/m은 레지스터 또는 메모리 주소를 지정할 때 사용합니다.

Mov r32, r/m32에서 r32가 reg필드가 지정한 레지스터, r/m32는 r/m값이 지정한 레지스터 혹은 메모리입니다.

SIB

SIB란 Scale, Index, Base의 약자로 displacement처럼 특정 메모리 위치에서 떨어진 값을 조회할 때 사용됩니다.

다만 배열이나 클래스처럼 포인터에서 특정 위치가 정해진 형식의 자료형에 접근할 때 주로 사용합니다.

배열의 포인터를 ptr이라고 가정할 때 저희는 *(ptr+0), *(ptr+1)처럼 배열의 값을 조회할 수 있습니다.

이때 scale은 배열의 크기(int형 배열일 경우 4), index는 0,1,2,3….이 되고, base는 0이 됩니다. (배열이 구조체나 클래스를 담고 있을 때 주로 사용합니다.)

Diplacement

Displacement는 메모리에서 얼마만큼 떨어진 값을 조회할 때 사용합니다.

MOD R/M의 mod 값이 01, 10일 때 8비트 값이나 32비트 값으로 사용됩니다.

Immediate

Immediate는 말 그대로 상수값입니다.

레지스터나 메모리에 상수값을 통하여 접근하거나(Lea) 값을 대입(Mov)하거나 더하거나 빼기(Add/Sub)와 같은 명령어에 상수값을 사용할 때 이를 지칭하는 말입니다.