본문 바로가기

고등학교 동아리 (리버싱), (잠정 무기한 폐지)

<리버싱 공부> 두 번째 : 레지스터랑 어셈블리 코드

저번 시간에 예고했듯이 이번 시간은 레지스터랑 어셈블리 코드에 대해서 공부해보자. 

블로그 이미지에서는 맥북이지만 사실 난 삼성 오디세이 노트북을 쓴다 헷


어셈블리 코드는 바이너리 코드와 1:1 대응되므로, 바이너리 코드가 실제로 동작할 CPU에 따라 기계 코드 역시 달라지게 된다. 다시 말해서 CPU에 따라 어셈블리 코드도 다르다. 나는 Intel 구조의 64bit 버전 명령어 집합에서 쓰이는 x64 명령어 집합에 대해 공부했다.

-레지스터(Register)

CPU안에 있는 다목적 저장공간이다. CPU가 RAM에 있는 데이터를 불러오려면 물리적으로 먼 길을 돌아가야 하기 때문에 시간이 오래 걸리지만, 레지스터는 CPU안에 있기 때문에 데이터처리 속도가 빠르다. Instruction Cycle(명령어 읽기 -> 명령어 해석 -> 해석 결과 실행)을 수행하기 위해 바이너리 코드에 해당하는 여러 명령어를 해석하기 위한 구성 요소 이외에도 읽어온 명령어가 저장된 공간을 임시로 기억해 둘 구성 요소나, 명령어를 실행할 결과를 저장해 둘 곳이 필요하다. 이렇게 CPU의 동작에 필수적인 저장 공간의 역할을 하는 CPU의 구성요소를 레지스터라고 한다.
레지스터는 쓰임새가 정해진 것은 아니지만 관행적으로 용도를 정해놓고 쓰는 레지스터도 있고, 엄격히 정해진 용도로만 쓰이는 레지스터가 있기도 하다.
주기억 장치보다 저장 용량이 적고 기억장치 중에서는 속도가 가장 빠르고, 휘발성 메모리이면서 읽고 쓰기가 가능하다.범용 레지스터 : 용도를 특별히 정해두지 않고 다양하게 쓸 수 있는 레지스터. x64에서의 레지스터 몇 개만 빠르게 훑고 넘어가겠다.

  • rax : 어떤 함수의 실행이 종료되고 나면 해당 함수의 결과값이 반환될 때 rax 레지스터에 담겨 반환된다. 그러나 이 레지스터가 리턴값을 위해서만 쓰이는 것이 아니라, 함수가 반환되기 전까지 범용 레지스터로 자유롭게 사용 되다가, 종료 후 리턴값을 반환하기 위한 레지스터로 rax만이 사용된다.
  • rbx : 메모리 주소를 저장하기 위한 용도
  • rcx : Window 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장
  • rip :  명령어 포인터, 다음에 실행될 명령어가 위치한 주소를 가리키고 있는데, 프로그램의 실행 흐름과 관련된 중요한 레지스터

이 정도만 써서 뒤에 설명할 어셈블리 명령어에 대해서 설명할건데, 다른 레지스터에 대해서 궁금하면 'x64 레지스터'를 검색하면 정확하게 나온다.

 

플래그 레지스터 : 시스템 제어 용도 또는 현재 상태나 조건을 나타내는 레지스터. 디버거에 어떤 플래그인지 표시되기 때문에 상세히 알 필요는 없어서 나중에 필요해지면 설명하겠다.

 

-Instruction Format

레지스터에 대해 대충 알아봤으니까, 어셈블리 코드에서 많이 볼수 있는 명령어에 대해서 알아보겠다. 명령어를 구성하는 두 개의 큰 요소는 명령코드(Opcode)피연산자(Operand)가 있다.

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

(사진출처 : 드림핵)

우선 기계코드(Machine Code) 또는 명령코드(Opcode)어셈블리 코드(Assembly Code)의 차이점에 대해서 알아야 한다.

  • 기계코드 또는 명령코드 : 디버거를 사용해 프로그램을 살펴보면 위 사진에서 보이는 결과와 같이 왼쪽의 숫자들처럼 생긴 명령코드를 볼 수 있다. 컴파일러가 만드는 결과물 바이너리를 구성하고 있으면서, CPU가 수행할 작업을 나타내는 숫자이다. 
  • 어셈블리 코드 : 숫자로 이루어진 Opcode는 사람이 이해하기 쉽지 않다. 따라서 이것이 어떤 의미를 갖는지 알아보기 쉽도록 문자로 작성된 중간의 파란글씨가 어셈블리 코드이다. 이것은 전에 말한 것처럼 명령 코드와 1:1로 대응된다. 뿐만 아니라 명령 코드가 연산할때 사용할 피연산자도 알아보기 쉽다. 명령코드와 피연산자를 묶어 하나의 명령어가 된다. 

- 어셈블리 명령어

  • mov : 오른쪽 인자값을 왼쪽에 전달한다.
  • lea : 오른쪽 인자의 주소를 왼쪽 인자에 전달한다.
  • add : 오른쪽 인자값을 왼쪽 인자에 더한다.
  • sub : 오른쪽 인자값을 왼쪽 인자에서 뺀다.
  • inc : 인자값을 1 증가시킨다.
  • dec : 인자값을 1 감소시킨다.
  • call : 함수를 호출한다.
  • nop : 아무것도 안함. 헷

등등 더 있다. 

(사진출처 : 드림핵)

  • 피연산자 : 명령 코드가 연산할 대상을 피연산자(Operand)라고 한다. 함수에 들어가는 인자라고 생각하면 조금 더 이해하기 쉽다. Intel 방식의 어셈블리는 연산한 결과를 왼쪽 피연산자에 저장된다고 이해하는 것이 일반적이다. 피연산자는 상수값, 레지스터,  메모리의 어떤 주소가 올 수 있다.

간단하게 어셈블리 명령어와 Instructions Format을 예시로 설명해보겠다.

 

mov rax, 765F
add rax, 526D

 

이 어셈블리 코드가 실행되면 rcx 레지스터에 들어있는 값은 C8CC 이다.

mov에서 rax에 765F라는 상수가 들어왔고, add로 765F에 526D라는 값을 더해주었기 때문이다.

(참고로 여기에서 상수는 Hex값이다.)

 

상수가 아닌 레지스터는 어떨까?

mov rcx, rbx
sub rcs, rax

rbx=14BF5, rax=D357일때, rcx레지스터에 들어있는 값은 789E이다.

이유는 방금 상수와 같다.

 

마지막으로 레지스터에 상수나 레지스터가 저장되는게 아니라, 메모리 주소가 저장되는 경우이다. 실제로는 해당 메모리 주소를 참조한 값이 피연산자로 사용된다. C언어의 포인터 개념과 비슷하다. 이건 나중에 좀 더 자세하게 설명할 수도 있고 안할 수도 있고ㅎㅎ

 

오늘은 여기까지 ! 다음시간에는 명령어를 좀 더 자세하게 알아보고 Hello World 디스어셈블리 결과를 살펴보겠다