[Assembly] 어셈블리어 기초 사용법 & 예제 총정리

    어셈블리어란?

    어셈블리어(assembly language)는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어입니다. 전류가 흐른다 그렇지 않다로 구성되는 0과 1의 이진수로 프로그램을 하는 기계어는 인간의 관점에서는 컴퓨터가 바로 읽을 수 있다는 점만 빼면 장점이 없는 언어이기 때문에 이를 보완하기 위해 나온 언어가 어셈블리어입니다. 기계어와 명령어가 1:1로 대응되는 단어들로 구성되어 있습니다. 고급언어는 컴파일하는 시간이 오래 걸리는 단점이 있는 반면 저급 언어는 컴퓨터와 가까운 언어이기 때문에 컴파일을 해도 간단한 명령으로 실행돼서 실행 속도가 굉장히 빠릅니다. 하지만 저급 언어는 배우기가 어렵고 유지보수가 힘들다는 이유로 특수한 경우를 제외하고는 사용되지 않고 있습니다. 하지만 임베디드 시스템이나 커널 프로그래밍, 컴퓨터 보안 등은 어셈블리어를 알아야 하고 디버깅이 불가능한 라이브 서비스를 진행하고 있는 환경에서 어느정도 디버깅을 할 수 있게 해주며 특히 어셈블리에 대한 전반적인 지식을 배움으로써 컴퓨터의 구조를 더 자세히 알 수 있어 어셈블리어로 코딩을 할 정도는 아니어도 구글링을 하며 해석 정도는 할 수 있어야 한다고 생각합니다.

     

     Intel 문법과 AT&T 문법 

    어셈블리어에는 Intel문법과 AT&T 문법이 존재하며 또한 이들은 서로 호환되지 않습니다. 서로의 문법에 대해 차이점이 존재합니다.

     

    명령어 수행 방식

    만약 위와 같은 명령어가 있다고 가정할 때 Intel Operand의 경우 Operand2가 Source고 Operand1이 Destination이 되며 EBX의 값을 EAX에 더한다라는 뜻이 되고 AT&T의 경우에는 반대가 되어 EAX의 값을 EBX로 더한다라는 뜻이 됩니다. 

     

    Opcode : 명령어 
    Operand : 피 연산자 

     

    숫자 표기 방식

    Intel (숫자를 그대로 사용)

    ex) 1, 2, 3, 4, 5 

     

    AT&T (앞에 $를 붙여서 사용)

    ex) $1, $2, $3, $4, $5

    레지스터 표기 방식

    Intel (명칭 그대로 사용)

    ex) EAX, EBX, EBP

     

    AT&T (앞에 %를 붙임)

    ex) %EAX, %EBX, %EBP

    이렇듯 두 가지 문법이 서로 차이점이 있습니다. 이 두 가지 문법 중 저희와 같은 개발자들이 많이 사용하는 것은 단연 Intel문법입니다. 이 포스팅 또한 Intel문법의 사용법을 기준으로 작성되었습니다.

     

     어셈블리에서 사용되는 레지스터 종류 

    레지스터란?

    레지스터란 CPU의 요청을 처리하는 데이터의 임시공간입니다. 레지스터는 공간이 작고 가격은 비싸지만 CPU에 직접 연결되어 있어서 연산 속도가 매우 빠릅니다. (RAM 보다도 훨씬 더) CPU는 자체적으로 데이터를 저장할 수 없으므로 레지스터를 이용하여 연산처리 와 번지지정을 도와줍니다. 32비트의 경우에는 레지스터의 처음이 E로 시작하고 64비트는 R로 시작합니다.

     

    범용 레지스터

    EAX : 사칙연산 등 산술 연산에 자동으로 사용되며, 함수의 반환 값을 처리할 때도 사용됩니다.

    EBX : 간접 번지 지정에 사용됩니다. 산수, 변수를 저장합니다.

    ECX : 반복(Loop)에서 반복 Count 역할을 수행합니다.

    EDX : EAX를 보조하는 역할을 합니다. 예를 들어 나누기를 진행할 경우 몫은 EAX에 나머지는 EDX에 저장됩니다.

     

    인덱스 레지스터

    ESI : 복사나 비교를 할 경우 출발지 주소를 저장하는 레지스터입니다.

    EDI : 복사나 비교를 할 경우 목적지 주소를 저장하는 레지스터입니다.

     

    포인터 레지스터

    EIP : 다음에 실행할 명령어의 주소를 가지고 있는 레지스터입니다. 현재 실행하고 있는 명령어가 종료되면 이 레지스터에 있는 명령어를 실행하게 됩니다.

    ESP : Stack Pointer의 가장 최근에 저장된 공간의 주소를 저장하는 레지스터입니다.

    EBP : Stack Pointer의 기준점(바닥 부분)을 저장하는 레지스터입니다.

     

    어셈블리어 자주 사용하는 명령어

    명령어 예제 설명 분류
    push push eax eax의 값을 스택에 저장 스택 조작
    pop pop eax 스택 가장 상위에 있는 값을 꺼내서 eax에 저장 스택 조작
    mov mov eax, ebx 메모리나 레지스터의 값을 옮길때 사용 데이터 이동
    inc lnc eax eax의 값을 1증가시킨다 (++) 데이터 조작
    dec dec eax eax의 값을 1감소시킨다 (--) 데이터 조작
    add add eax, ebx 레지스터나 메모리의 값을 덧셈할때 쓰인다. 논리, 연산
    sub sub eax, ebx 레지스터나 메모리의 값을 뺄셈할때 쓰인다. 논리, 연산
    call call proc 프로시저를 호출한다. 프로시저
    ret ret 호출했던 바로 다음 지점으로 이동 프로시저
    cmp cmp eax, ebx 레지스터와 레지스터의 값을 비교 비교
    jmp jmp proc 특정한 곳으로 분기 분기
    int  int $0x80 OS에 할당된 인터럽트 영역을 system call 인터럽트
    nop nop 아무 동작도 하지 않는다. (No Operation)  

    주로 사용되는 건 위의 명령어들이며 이 정도만 알아두어도 어셈블리어를 해석하는데 큰 문제가 없습니다. 혹시나 추가적으로 나오는 명령어가 있다면 구글링 하셔서 진행 만약 좀 더 다양한 명령어를 보고 싶다면 아래 글을 참고해주시기 바랍니다.

    [Assembly] 어셈블리어 명령어 총정리

     

    어셈블리어 예제

    #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 함수를 통해 [ebp-20h] 이 부분이 변수 c가 되겠습니다. 여기에 방금 add한 값을 넣어줍니다. 그런 뒤 call 009A10CD을 통해 print함수를 출력합니다. 그리고 마지막으로 사용이 끝난 레지스터들을 pop해줍니다.

     

    간단한 C언어 코드인데도 어셈블리어로 번역하면 이렇게 됩니다. 명령어 위주로 되어있어 명령어만 안다면 해석이 어렵지는 않으나 전체적으로 해석해야 할 문장이 너무나도 길죠. 간단한 문장이니 이 정도로 끝났지 긴 프로그래밍이면 전체 번역은 불가능하다고 봐야 합니다. 그래서 결국 부분 부분 끊어서 봐야 합니다. 요즘에는 어셈블리어에 가독성이 좋게끔 고급 프로그래밍 언어를 위에 추가해준다거나 변수 이름을 보이게 하여서 가독성이 좋게 해주는 방법이 많이 나오고 있습니다. 이를 활용하는 것도 좋은 방법이 될 수 있겠습니다.

     

    댓글(1)

    • 2021.06.01 16:07 신고

      깔끔한 정리 정말 감사드립니다. 어셈블리 내용을 간략하게 복습해보려는데 크게 도움이 되었습니다.

    Designed by JB FACTORY