본문 바로가기
지식창고/IT 지식

x86-64 어셈블리어

by eteo 2022. 10. 20.

 

어셈블리어

어셈블리 언어는 컴퓨터의 기계어와 치환되는 언어이다. 기계어가 여러 종류라면 어셈블리어도 여러 종류여야 함을 의미하며, CPU에 사용되는 ISA(명령어 집합 구조)에 따라서 IA-32, X86-64, ARM, MIPS 등 많은 종류의 어셈블리어가 존재한다.

 

 

어셈블리어 기본 구조

어셈블리의 문장은 동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성된다.

Opcode Operand1, Operand2

 

 

어셈블리어 명령어의 종류

  • 데이터 이동 : mov, lea
  • 산술 연산 (Arithmetic) : inc, dec, add, sub
  • 논리 연산 (Logical) : and, or, xor, not
  • 비교 (Comparison) : cmp, test
  • 스택 (Stack) : push, pop
  • 프로시저 (Procedure) : call, ret, leave
  • 시스템 콜 (System Call) : syscall

 

 

 

어셈블리어의 피연산자

피연산자에는 상수, 레지스터, 메모리가 올 수 있다.

 

메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다. 여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.

 

ex. 메모리 피연산자의 예

QWORD PTR[0x8048000]  :  0x8048000의 데이터를 8바이트만큼 참조한다.
DWORD PTR[0x8048000]  :  0x8048000의 데이터를 4바이트만큼 참조한다.
WORD PTR[rax]  :  rax가 가르키는 주소에서 데이터를 2바이트만큼 참조한다.

 

 

 

참고.

WORD의 크기가 2바이트인 이유

Intel 사에서 IA-16 아키텍처 기반 16비트 프로세서를 만들었을 때 기본 처리 단위인 16비트를 WORD로 재정의(typedef) 했었다. 이후 IA-32, X86-64 아키텍처가 등장했지만 기존의 프로그램들이 새로운 아키텍처와 호환되지 않을 수 있기 때문에 WORD 자료형의 크기를 16비트로 유지하기로 하였다. 대신 DWORD (Double Word, 32bit)와 QWORD(Quad Word, 64bit) 자료형을 추가로 만들었다.

 

 

 

대표 명령어

명령어 예시 설명
mov mov dst, src src에 들어있는 값을 dst에 대입한다.
lea lea dst, src src의 유효주소(Effective Address, EA)를 dst에 저장한다.
add add dst, src dst에 src의 값을 더한다.
sub sub dst, src dst에 src의 값을 뺀다.
inc inc op inc op : op의 값을 1 증가시킨다.
dec dec op dec op  :  op의 값을 1 증가시킨다.
and and dst, src dst와 src의 비트가 모두 1이면 1, 아니면 0
or or dst, src dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
xor  xor dst, src src의 비트가 서로 다르면 1, 같으면 0
not not op  op의 비트를 전부 반전시킨다.
cmp  cmp op1, op2 op1과 op2를 비교한다.
두 피연산자를 빼서 대소를 비교하는데, 서로 같은 두 수를 빼면 ZF = 1 로 ZeroFlag가 설정되어 두 값이 같았는지 판단할 수 있다.
test  test op1, op2  op1과 op2를 비교
두 피연산자에 AND 비트연산을 취하여 대소를 비교한 후 ZF를 설정한다.
jmp  jmp addr  addr로 rip를 이동시킨다.
je  je addr 직전에 비교한 두 피연산자가 같으면 점프한다. (Jump to Equal)
jg  jg addr 직전에 비교한 두 연산자 중 전자가 더 크면 점프한다. (jump if greater)
push  push val  val을 스택 최상단에 쌓음
pop pop reg 스택 최상단의 값을 꺼내서 reg에 대입
call call addr addr에 위치한 프로시져 호출
leave  leave  스택 프레임 정리
ret ret 호출자의 실행 흐름으로 돌아감
int  int $0x80 OS에 할당된 인터럽트 영역을 system call
nop nop 아무 동작도 하지 않는다. (No Operation)

 

 

출처 : DreamHack

 

 

 

 

어셈블리어 해석

#include<stdio.h>

int main() {
    int a = 1;
    int b = 2;	
    int c = a + b;
    
    printf("%d", c);
}
 push        ebp  
 mov         ebp,esp  
 sub         esp,0E4h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp+FFFFFF1Ch]  
 mov         ecx,39h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,9AC003h  
 call        009A1316  
 mov         dword ptr [ebp-8],1  
 mov         dword ptr [ebp-14h],2  
 mov         eax,dword ptr [ebp-8]  
 add         eax,dword ptr [ebp-14h]  
 mov         dword ptr [ebp-20h],eax  
 mov         eax,dword ptr [ebp-20h]  
 push        eax  
 push        9A7D08h  
 call        009A10CD  
 add         esp,8  
 xor         eax,eax  
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0E4h  
 cmp         ebp,esp  
 call        009A123F  
 mov         esp,ebp  
 pop         ebp  
 ret

 

 

우선 특정 함수에 접근하려면 사용할 스택의 공간을 만들어야 한다. ebp를 push한뒤 esp의 값을 ebp에 넣어주어 기존에 사용하던 esp의 값(스택의 꼭대기)을 스택의 가장 밑 부분으로 만들었다.그런 뒤 esp(스택의 꼭대기)에 0E4h(16진수)의 크기를 할당하여 ebp ~ esp까지 공간을 할당하여 사용할 스택의 크기를 만든다.

 

dword ptr [ebp-8]에 1이라는 값을 넣고 dword ptr[ebp-14h]에 2라는 값을 넣는다. 이것이 위의 코드에서 변수 a에 1을 넣고 변수 b에 2를 넣은 부분이다. 이렇듯 어셈블리어에서는 변수의 이름 같은 것이 없고, 단지 메모리 주소만 있을 뿐이다.

 

그런 다음 eax에 다시 [ebp-8]에 있는 값을 넣고 eax와 [ebp-14]의 값을 더한다. 이 부분이 바로 a+b이다. 다음 mov 함수를 통해 변수 c인 [ebp-20h] 부분에 방금 add한 값을 넣어준다.

 

call 009A10CD을 통해 print함수를 출력한다. 마지막으로 사용이 끝난 레지스터들을 pop해주고 ret한다.

 

출처 : https://coding-factory.tistory.com/651