ImFe's study

InstructionCycle, 레지스터, 함수호출규약, DataSize, FLAGS, Instruction Format 본문

Reversing

InstructionCycle, 레지스터, 함수호출규약, DataSize, FLAGS, Instruction Format

ImFe 2020. 5. 14. 02:50

어셈블리 코드는 기계 코드와 대응되므로, 기계 코드가 동작할 CPU가 어떤 역할을 하고 어떻게 동작하는지를 알아보는것이 어셈블리 코드를 이해하는데에 도움이 됩니다.

 

 

 

CPU의 기본동작은

 

다음 실행할 명령어를 읽어오고(Fetch) → 읽어온 명령어를 해석한 다음(Decode) → 해석한 결과를 실행하는(Execute) 과정을 반복합니다. 이렇게 한 개의 명령어, 다시 말해 기계 코드가 실행되는 한 번의 과정을 Instruction Cycle이라고 합니다.

 

CPU는 Instruction Cycle을 수행하기 위해 기계 코드에 해당하는 각종 명령어를 해석하기 위한 구성 요소 외에도 읽어온 명령어가 저장된 공간을 임시로 기억해 둘 구성 요소나, 명령어를 실행한 결과를 저장해 둘 구성 요소가 필요합니다. 이렇게 CPU의 동작에 필수적인 저장 공간의 역할을 하는 CPU의 구성 요소를 레지스터(Register)라고 합니다.

 

한편, CPU가 실행할 명령어(Instruction)들은 수행하는 동작에 따라 조금씩 형태가 다릅니다. 다양한 명령어의 종류와 각각의 문법에 대해 이어서 이어서 자세히 알아보겠습니다.

 

 

 

 

-레지스터

 

레지스터는 CPU가 사용하는 저장 공간입니다. 대개 레지스터는 특별히 쓰임새가 정해져있지 않고 관행적으로 용도를 정해놓고 쓰는 레지스터도 있고, 엄격히 정해진 용도로만 쓰이는 레지스터가 있기도 합니다.

 

 

--범용 레지스터

 

범용 레지스터는 말 그대로 용도를 특별히 정해두지 않고 다양하게 쓸 수 있는 레지스터입니다.

수학문제풀때 쓰던 연습장과 같은 맥락입니다.

 

rax rcx rdx r8 r9 r10 r11 rbx rsi rdi rbp r12 r13 r14 r15 rsp

rip

 

rax는 함수가 실행 된 후 리턴값을 저장 하기 위해 쓰입니다.

어떤 함수의 실행이 종료되고 나면 해당 함수의 결과값이 반환될때 이 rax 레지스터에 담겨 반환됩니다.

그러나 rax가 리턴값을 위해서만 쓰이는건 아닙니다.

함수가 반환되기 전까지 범용 레지스터로 자유롭게 사용되다가, 종료 후 리턴값을 반환하기 위한 레지스터로는 rax만이 사용됩니다.

 

 

--함수 호출 규약

x64의 범용 레지스터들 중에서는 함수가 실행될 때 필요한 인자들을 저장하는 용도로 사용하는 레지스터들도 있습니다. 이를 함수 호출 규약(Calling Convention)이라고 부르며, 운영체제의 종류나 함수의 종류에 따라 조금씩 다릅니다.

(이 카테고리에서는 windows 64bit 기준으로 진행됩니다.)

 

rcx, rdx, r8, r9는 windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장합니다.

 

첫번째 인자는 rcx에.. 두번째 인자는 rdx에.. 하는 방식으로 인자를 레지스터에 담아 함수를 호출합니다.

 

rax와 마찬가지로, 함수 호출 규약에서 쓰이는 레지스터들 역시 함수를 호출할 때 인자를 전달하는 용도로 이 레지스터들이 정해집니다. 함수가 호출된 이후에는 이들 역시 범용 레지스터로 자유롭게 사용할 수 있습니다.

 

rsp는 범용레지스터이지만 용도가 정해져있습니다. rsp는 스택포인터(Stack Pointer)로, 스택의 가장 위쪽 주소를 가리킵니다. 스택은 함수가 사용할 지역 변수들(local variables)을 저장하기 위해 준비해놓는 공간입니다.

 

명령어 포인터 (Instruction Pointer)는 범용 레지스터들과는 달리 그 용도가 엄격히 정해져 있습니다.

명령어 포인터인 rip는 다음에 실행될 명령어가 위치한 주소를 가르키고 있습니다. 범용으로 사용되지 않습니다.

 

 

 

--Data Size

 

CPU가 사용하는 값의 크기 단위를 WORD라고 합니다. 오래 전 16bit CPU가 처음 등장했을 때 당시 CPU가 사용하는 값의 단위였던 16bit를 WORD라고 하면서, WORD 단위를 처리할 수 있는 범용 레지스터의 이름이 ax, cx, dx, bx로 붙은 것이 현재 우리가 사용하는 레지스터 이름의 기원이 되었습니다. 다른 글에서도 레지스터에 대해 다루었지만 레지스터 맨 앞글자인 e는 extended를 의미합니다.

 

우리가 사용하는 64bit CPU인 x64의 레지스터들이 담을 수 있는 크기는 64(8x8)bit(8byte, QWORD)입니다. 그러나 꼭 8바이트 단위로만 값을 저장해야 하는 것은 아닙니다.

 

그림과 같이 rcx 레지스터를 예로 들어 설명하면, rcx 레지스터에 저장된 값 중 하위 32bit (4byte, DWORD)만 연산에 사용할 수도 있고, 혹은 하위 16bit (2byte, WORD)나 하위 8bit(1byte, BYTE)만 사용하는 것도 가능합니다. 이렇게 레지스터의 하위 비트만 접근하려면 어셈블리 코드에서 접근할 레지스터 이름으로 ecx, cx를 사용하면 됩니다.

 

마찬가지로 r8 ~ r15까지 64비트에서 새로 추가된 범용 레지스터들도 하위 일부 비트만 접근하여 사용하기 위해 다른 레지스터 이름을 사용할 수 있습니다. r8의 경우 r8d, r8w, r8b를 통해 각각 하위32비트 16비트, 8비트에 접근할 수 있습니다. 이렇게 d, w, b 와 같은 접미사를 붙이는 방식은 r8 ~ r15 레지스터들이 동일하게 사용합니다.

 

 

 

 

--FLAGS

 

FLAGS는 상태 레지스터입니다. '깃발'을 의미하는 단어 뜻 그대로, 현재 상태나 조건을 0과1로 나타내는 레지스터입니다. 다른 레지스터와는 달리, FLAGS레지스터를 구성하는 64개의 비트들 각각이 서로 다른 의미를 지닙니다.

 

다시 말해 0번째비트,1번째비트,2번째비트... 등 각각의 비트가 서로 다른 상태를 나타낸다고 볼 수 있습니다.

 

 

 

-Instruction Format

 

x64 어셈블리 코드에서 많이 볼 수 있는, 명령어를 구성하는 두 개의 큰 요소인 명령 코드(Opcode)와 피연산자(Operand)에 대해 알아보겠습니다.

 

--Opcode (Operation Code)

명령 코드(Opcode, Operation Code)는 명령어에서 실제로 어떤 동작을 할지를 나타내는 부분입니다. 자료를 옮기거나, 산술 연산을 하거나, 자료를 제어하는 등 다양한 종류의 명령 코드가 있습니다.

 

 

 

--Opcode(명령코드) == MachineCode(기계코드)

컴파일러가 만드는 결과물인 바이너리를 구성하고 있으며, CPU가 실제로 수행할 작업을 나타내는 숫자입니다.

이 명령코드는 (사진) CPU의 종류별로 다른 값일 수 있으며, 명령 코드에 따라 피연산자(Operand)가 필요하기도 합니다.

 

 

 

--Operand

명령 코드가 연산할 대상을 피연산자(Operand)라고 합니다. 명령 코드를 함수라고 생각하면, 피연산자는 함수에 들어가는 인자라고 생각하면 조금 더 이해하기가 쉽습니다.

 

mov rbp,rsp

; rbp = rsp

mov eax,0x0

; eax = 0x0

add rcx,0x8

; rcx = rcx+0x8

dec rcx

; rcx =rcx-1

 

(인텔문법)

 

명령코드가 작업을 수행할 대상인 피연산자는 어떤 상수일수도 있고, 레지스터에 들어 있는 값일 수도 있으며, 어떤 주소에 들어있는 값일 수도 있습니다. (피연산자 = 상수or레지스터값or주소에 들어있는값)

 

 

 

--Operand Types

주어진 명령 코드의 피연산자로는 상수, 레지스터, 레지스터가 가리키고 있는 메모리의 어떤 주소 가 올 수 있습니다. 마지막 세 번째 경우는 C에서 프로그래밍을 할 때 자주 사용하는 포인터 개념과도 매우 밀접하게 연관되어있습니다.

 

---상수값(Immediate)

가장 간단한 케이스로, 피연산자로 사용되는 값이 상수인 경우입니다.

mov rcx,0xbeef

;rcx = 0xbeef

add rcx,0x1337

;rcx = 0xbeef+0x1337

     = 0xd226

 

 

---레지스터

레지스터도 피연산자로 사용될 수 있습니다.

레지스터를 피연산자로 사용하는 경우에는 레지스터에 들어있는 값이 피연산자로 사용됩니다.

rbx=0xdead, rax=0xc0de일떄,

mov rcx,rbx

;rcx = rbx

sub rcx,rax

;rcx = rcx - rax

rcx의 값은 0x1dcf

 

 

---Addressing Modes

레지스터에 있는 값이 피연산자가 되는 것이 아니라 레지스터에 저장된 메모리 주소를 참조한 값이 피연산자가 되는 경우입니다.

 

=레지스터에 들어있는 값은 메모리 주소로, 실제로는 해당 메모리 주소를 참조한 값이 피연산자로 사용됩니다.

포인터의 개념과 유사합니다.

 

mov [rcx],rax

;*rcx = rax

mov 명령어의 결과로 rax에 들어있는 값을 rcx 레지스터가 참조하는 주소의 메모리에 저장하게 됩니다.

 

mov byte ptr[rcx],al

;*rcx=al

byte ptr은 Pointer Directive라고 하며,

앞서 공부한 Data Size가 실제 어셈블리 코드에서 사용된 케이스입니다.

즉 rax레지스터가 저장하고 있는 값 중 하위 8bit, 곧 1바이트만 rcx가 참조하는 주소에 저장하게 됩니다.

 

레지스터의 값이 메모리 주소로 바로 쓰이기도 하는 반면, 레지스터에 들어있는 값에서 특성 오프셋(offset) 만큼 떨어진 주소값을 참조하기도 합니다.

 

---[reg+d]

레지스터에 들어있는 값을 주소의 기준으로 하여 d만큼 떨어진 오프셋을 실제로 참조한 다음 피연산자로 씁니다.

 

mov dword ptr

[rbp-1Ch],eax

rax레지스터에 들어있는 값을 저장할 때 rbp의 값을 참조한 메모리 주소에 넣는것이 아니라, 그 메모리 주소로부터 -0x1C만큼 떨어진 곳을 계산하여 넣습니다. 여기서도 등장하는 PointerDirective를 고려하면, DWORD에 해당하는 사이즈인 하위 2byte만 넣는 것을 알 수 있습니다.

 

[reg1+reg2]

한 레지스터에 들어있는 값과 다른 레지스터에 들어잇는 값을 더한 결과를 참조할 메모리 주소로 사용하는 경우입니다.

 

 

 

[reg1+reg2*i+d]

 

mov byte ptr

[rdi+rcx*4+3],OFFh

rdi레지스터에 담긴 주소를 기준으로, rcx 레지스터의 값을 단위로 하여 4단위 떨어진 곳에 다시 offset 3만큼 더한 주소를 실제로 참조하고 있습니다. 여기에 1byte 사이즈 값인 0xff가 저장됩니다.

reg2에 해당하는 레지스터에 담긴 값은 대개 자료형이나 구조체의 크기인 경우가 많습니다.

 

 

 

 

 

 

 

 

명령어를 이루는 명령 코드와 피연산자에 대해서 공부했습니다.

 

'Reversing' 카테고리의 다른 글

스택, 함수에필로그, 프롤로그  (0) 2020.05.19
리버스 엔지니어링이란  (0) 2020.05.14
어셈블리 코드 해석  (0) 2020.04.12
어셈블리어  (0) 2020.04.12
어셈블리어로 Hello world 출력하고 디버깅해보기  (0) 2020.04.11
Comments