본문 바로가기
  • 안아주는 다람쥐
seKUrity_Study : System & Reversing

[ System ] 어셈블리어에 대하여

by Sapphire. 2023. 3. 9.

1. 어셈블리어란?

어셈블리어로 작성된 간단한 프로그램

어셈블리어(Assembly language)는 컴퓨터 아키텍처의 명령어를 기반으로 하는 저급 언어로, 기계어보다 이해하기 쉽고 수정하기도 쉽습니다.

어셈블리어는 기계어의 대부분 명령어를 가지고 있으며, 기계어의 문제점인 가독성과 수정 용이성을 개선하고자 만들어졌습니다.

 

즉 어셈블리어란 사용자가 이해하기 어려운 기계어 대신에 명령 기능을 쉽게 연상할 수 있는 기호를 기계어와 1:1로 대응시켜 코드화한 기호 언어입니다.

 

 

2. 어셈블리어의 특징

1. 하드웨어에 직접 접근

어셈블리어는 CPU가 직접 이해할 수 있는 기계어로 작성되기 때문에 하드웨어에 직접적으로 접근할 수 있습니다. 따라서 어셈블리어는 하드웨어와의 상호작용이 필요한 시스템 프로그래밍, 임베디드 시스템, 드라이버 등에서 많이 사용됩니다.

 

2. 저수준 언어

어셈블리어는 CPU의 명령어 집합을 직접 사용하기 때문에, 다른 고급 언어와 비교하면 매우 저수준(low-level)인 언어입니다. 이러한 특성 때문에 어셈블리어는 메모리와 레지스터의 관리, 데이터 타입의 변환, 분기 등을 직접 처리해야 하기 때문에 작성하기가 어렵고 복잡합니다.

 

3.높은 성능

어셈블리어는 하드웨어에 직접 접근하고, 저수준인 언어이기 때문에 일반적으로 높은 성능을 가집니다. 다른 고급 언어와는 달리 최적화를 거의 하지 않아도 빠른 속도로 실행할 수 있습니다.

 

4. 이식성이 낮음

어셈블리어는 CPU의 명령어 집합에 직접 의존하기 때문에, 다른 아키텍처에서는 동작하지 않을 수 있습니다. 따라서 어셈블리어로 작성한 코드를 다른 아키텍처로 이식하기가 어렵습니다.

 

5. 코드 가독성이 낮음

어셈블리어는 기계어와 거의 일대일 대응되는 언어이기 때문에 코드 가독성이 매우 낮습니다. 특히 코드가 길어지면 디버깅이나 유지보수가 매우 어렵습니다. 따라서 어셈블리어는 복잡하고 긴 코드를 작성할 때는 적합하지 않습니다.

 

3.  어셈블리어의 문법

어셈블리어는 CPU가 이해할 수 있는 기계어를 인간이 이해하기 쉬운 형태로 표현한 것이라고 위에서 말했었습니다.  또한, 어셈블리어의 문법은 CPU가 지원하는 명령어와 레지스터 등의 하드웨어에 의해 결정됩니다. 이제부터 어셈블리어의 기본적인 문법에 대해 알아봅시다.

 

1.  레지스터

어셈블리어는 CPU 내부의 레지스터(register)를 직접 사용할 수 있습니다. 레지스터는 CPU 내부의 기억 장치로서 빠른 속도로 데이터를 읽고 쓸 수 있습니다. 레지스터는 일반적으로 간단한 이름으로 표시되며, x86 아키텍처에서는 eax, ebx, ecx, edx 등이 있습니다.

 

2. 메모리 

어셈블리어에서는 메모리를 주소(address)와 값(value)으로 구분합니다. 메모리의 주소는 대개 레지스터나 상수(constant)로 표현되며, 값은 레지스터나 상수, 다른 메모리 위치 등으로 표현됩니다. 또한, 어셈블리어에서는 데이터의 크기를 지정해야 합니다. 예를 들어, x86 아키텍처에서는 byte, word, dword, qword 등의 데이터 타입을 지원합니다.

 

3. 연산자

어셈블리어에서는 다양한 연산자(operator)를 사용하여 데이터를 처리합니다. 대표적인 연산자로는 덧셈(add), 뺄셈(sub), 곱셈(mul), 나눗셈(div), 논리 연산자(and, or, not), 쉬프트 연산자(shift), 비교 연산자(cmp) 등이 있습니다.

 

4. 라벨과 분기

어셈블리어에서는 라벨(label)과 분기(branch)를 사용하여 프로그램

의 흐름을 제어할 수 있습니다. 라벨은 특정 위치를 지정하는 이름으로, 분기는 프로그램의 실행 흐름을 라벨로 이동시키는 명령어입니다. 예를 들어, 다음과 같은 코드는 값이 0에서 9까지 순차적으로 출력하는 예제입니다.

mov eax, 0  ; eax 레지스터에 0을 저장
loop_start:  ; loop_start 라벨 지정
    push eax  ; eax 값을 스택에 저장
  
mov eax, dword ptr [esp]  ; 스택에 저장된 eax 값을 eax 레지스터에 다시 저장
call print_int  ; eax 값을 출력하는 함수 호출
add eax, 1  ; eax 값을 1 증가
cmp eax, 10  ; eax 값이 10과 같은지 비교
jne loop_start  ; eax 값이 10과 같지 않으면 loop_start 라벨로 분기


위 코드에서 loop_start 라벨은 반복문의 시작 지점을 나타내며, 
jne 명령어는 eax 값이 10이 아니면 loop_start 라벨로 분기합니다.

 

5. 주석 

어셈블리어에서는 주석(comment)을 사용하여 코드를 설명할 수 있습니다. 주석은 대개 세미콜론(;), 쉬프트 연산자(//), 슬래시(/) 등으로 표시됩니다.

 

EX! ) 어셈블리어 프로그래밍 예제

다음은 x86 아키텍처에서 어셈블리어로 작성된 간단한 예제입니다. 이 예제는 두 개의 정수를 더하는 프로그램입니다.

section .data  ; 데이터 섹션
    num1 dw 5  ; num1 변수에 5 저장
    num2 dw 7  ; num2 변수에 7 저장
    result dw ?  ; result 변수 선언

section .text  ; 코드 섹션
    global _start  ; 프로그램의 시작 지점
_start:
    mov ax, [num1]  ; num1 변수의 값을 ax 레지스터에 저장
    add ax, [num2]  ; num2 변수의 값을 ax 레지스터에 더함
    mov [result], ax  ; ax 레지스터의 값을 result 변수에 저장

    mov eax, 1  ; 종료 시스템 콜 번호
    xor ebx, ebx  ; 종료 코드
    int 0x80  ; 시스템 콜

위 코드에서는 .data 섹션에서 num1, num2, result 변수를 선언하고 초기값을 할당했습니다. .text 섹션에서는 _start 라벨을 선언하여 프로그램의 시작 지점을 나타내고, mov와 add 명령어로 num1과 num2 변수의 값을 더해 result 변수에 저장합니다. 마지막으로, 시스템 콜을 호출하면 프로그램이 종료됩니다.

 

4. 아키텍쳐별 어셈블리어 문법

다양한 아키텍처에 따라 어셈블리어의 문법이 다르다고 위에서 설명드렸습니다! 고로 지금부터 각 아키텍처별로 문법을 살펴보도록 하겠습니다.

 

1.  x86 아카텍쳐

x86 아키텍처는 인텔(Intel)과 AMD(Advanced Micro Devices) 등에서 사용하는 아키텍처로, 대부분의 PC와 서버에 사용됩니다. x86 아키텍처의 어셈블리어는 주로 NASM(NASM Assembler)과 GAS(GNU Assembler) 등으로 작성됩니다.

 

  • mov: 값을 복사하는 명령어입니다.
mov destination, source
  • add: 값을 더하는 명령어입니다.
add destination, source
  • lea: 주소를 계산하는 명령어입니다.
lea destination, source

 

2. ARM 아키텍처

ARM 아키텍처는 대부분의 스마트폰과 태블릿 등에서 사용되는 아키텍처로, ARM Holdings에서 개발합니다. ARM 아키텍처의 어셈블리어는 주로 GNU Assembler과 ARM Assembler 등으로 작성됩니다

  • mov: 값을 복사하는 명령어입니다.
mov destination, source
  • add: 값을 더하는 명령어입니다.
add destination, source1, source2
  • ldr: 메모리에서 값을 로드하는 명령어 입니다.
ldr destination, [source]