어셈블리어 기본 문법

ORCASUIT·2023년 10월 27일
1

어셈블리어는 기본적으로 레지스터, 메모리, 상수 등을 직접 조작하는 저수준 언어입니다. 여기에서는 x86_64 아키텍처 기반의 어셈블리어 기본 문법에 대해 설명하겠습니다.

주석

  • ; 또는 # 으로 시작하는 라인은 주석입니다.
; 이것은 주석입니다.
# 이것도 주석입니다.

명령어 형식

  • 대부분의 명령어는 다음 형식을 따릅니다: instruction destination, source
movq %rax, %rbx  ; rax의 값을 rbx에 복사합니다.

데이터 레지스터

x86_64 아키텍처에서는 여러 종류의 데이터 레지스터가 있으며, 각각의 레지스터는 특정한 용도로 사용될 수 있습니다.

  1. 일반 목적 레지스터 (General-Purpose Registers)

    • %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %r8 ~ %r15
    • 이 레지스터들은 데이터 연산, 메모리 주소 계산 등 일반적인 목적으로 사용됩니다.
  2. 스택 포인터 레지스터 (Stack Pointer Register)

    • %rsp
    • 현재 스택의 상단을 가리킵니다. push, pop, call, ret 등의 명령어에서 자동으로 조정됩니다.
  3. 베이스 포인터 레지스터 (Base Pointer Register)

    • %rbp
    • 주로 함수 호출에서 로컬 변수와 파라미터에 접근할 때 사용됩니다. 일반적으로 스택 프레임을 설정하는 데 사용됩니다.
  4. 인스트럭션 포인터 레지스터 (Instruction Pointer Register)

    • %rip
    • 현재 실행되고 있는 명령어의 주소를 가리킵니다. 이 레지스터는 명시적으로 접근할 수 없으며, 분기 명령어나 함수 호출을 통해 간접적으로 변경됩니다.
  5. 상태 레지스터 (Status Register)

    • %rflags
    • 조건 분기나 산술 연산의 결과를 저장합니다. 여기에는 제로 플래그(ZF), 캐리 플래그(CF), 오버플로 플래그(OF) 등이 있습니다.
  6. 세그먼트 레지스터 (Segment Registers)

    • %cs, %ds, %es, %fs, %gs, %ss
    • 이 레지스터들은 메모리 세그먼트를 선택합니다. 현대의 운영체제에서는 보통 잘 사용되지 않습니다.

이 레지스터들은 어셈블리어 프로그래밍에서 중요한 역할을 하며, 특히 함수 호출, 데이터 처리, 조건 분기 등 다양한 상황에서 활용됩니다. 이해를 돕기 위해 각 레지스터의 일반적인 역할을 간략하게 설명했지만, 실제로는 더 복잡한 명령어와 상황에서 다양하게 사용됩니다.

메모리 접근

  • (%rax)와 같이 괄호 안에 레지스터 이름을 넣으면 해당 레지스터에 저장된 주소의 메모리를 가리킵니다.
movq $42, (%rax)  ; rax가 가리키는 메모리 위치에 42를 저장합니다.

즉시값 (Immediate Values)

  • $ 기호를 이용해 상수를 표현할 수 있습니다.
movq $42, %rax  ; rax 레지스터에 42를 저장합니다.

연산자

x86_64 어셈블리어에서 사용되는 다양한 연산자에 대해 설명하겠습니다. 이 연산자들은 산술, 논리, 비트, 비교, 분기 등 다양한 카테고리로 나뉩니다.

산술 연산자

  1. addq src, dest: dest = dest + src
  2. subq src, dest: dest = dest - src
  3. mulq src: RDX:RAX = RAX * src
  4. divq src: RAX = RDX:RAX / src, RDX = RDX:RAX % src
  5. imulq src, dest: dest = dest * src (signed multiplication)
  6. idivq src: Similar to divq, but for signed division

논리 연산자

  1. andq src, dest: dest = dest & src
  2. orq src, dest: dest = dest | src
  3. xorq src, dest: dest = dest ^ src
  4. notq src: src = ~src

비트 이동 연산자

  1. shlq n, dest: dest = dest << n
  2. shrq n, dest: dest = dest >> n
  3. salq n, dest: dest = dest << n (Arithmetic shift left)
  4. sarq n, dest: dest = dest >> n (Arithmetic shift right)

비교 및 테스트 연산자

  1. cmpq src, dest: 비교 후 결과에 따라 플래그 설정 (dest - src)
  2. testq src, dest: 논리곱 후 결과에 따라 플래그 설정 (dest & src)

분기 연산자

  1. jmp label: Unconditional jump to label
  2. je label: Jump if equal (ZF=1)
  3. jne label: Jump if not equal (ZF=0)
  4. jl label: Jump if less (SF != OF)
  5. jle label: Jump if less or equal (ZF=1 or SF != OF)
  6. jg label: Jump if greater (ZF=0 and SF=OF)
  7. jge label: Jump if greater or equal (SF=OF)

이 외에도 lea (Load Effective Address), nop (No Operation), int (Interrupt), syscall (System Call) 등 여러 다른 연산자와 명령어가 있습니다. 이 연산자들은 코드에 따라 다양하게 조합되어 복잡한 연산과 프로시저를 수행합니다.

분기와 루프

분기(Branching)와 루프(Looping)는 어셈블리어 또는 고수준 프로그래밍 언어에서 프로그램 흐름을 제어하는 두 가지 기본적인 메커니즘입니다.

분기 (Branching)

분기는 특정 조건에 따라 프로그램의 실행 흐름을 변경합니다. x86_64 어셈블리에서는 일반적으로 cmp 명령어로 두 값을 비교한 뒤, 조건에 따라 분기를 수행하는 j* 명령어를 사용합니다.

  • 예제
    cmpq %rax, %rbx  ; RAX와 RBX 비교
    je   equal       ; 만약 RAX = RBX이면 'equal' 레이블로 분기
    jne  not_equal   ; 만약 RAX ≠ RBX이면 'not_equal' 레이블로 분기

루프 (Looping)

루프는 특정 조건이 만족되는 동안 코드 블록을 반복적으로 실행합니다. 어셈블리에서는 jmp 명령어와 분기 명령어를 조합하여 루프를 구현합니다.

  • 예제

    loop_start:
        cmpq %rax, %rbx  ; RAX와 RBX 비교
        je   loop_end    ; 만약 RAX = RBX이면 루프 종료
        ; 여기에 반복할 코드 작성
        jmp  loop_start  ; 'loop_start' 레이블로 다시 분기하여 루프 계속
    
    loop_end:
        ; 루프 이후 실행될 코드

고수준 언어와의 비교

고수준 언어에서는 이러한 분기와 루프를 더 직관적으로 표현할 수 있습니다. 예를 들어, C 언어에서는 if, else로 분기를 표현하고, for, while로 루프를 표현합니다.

  • C 언어 예제

    // 분기
    if (a == b) {
        // 코드
    } else {
        // 코드
    }
    
    // 루프
    while (a != b) {
        // 코드
    }

분기와 루프는 프로그램 로직을 구성하는 중요한 빌딩 블록입니다. 효과적으로 활용하면 복잡한 프로그램도 구현할 수 있습니다.

함수 호출

함수 호출은 프로그램 실행 중에 특정 코드 블록(함수)을 실행하기 위해 사용되는 매커니즘입니다. 어셈블리어에서는 일반적으로 callret 명령어를 사용하여 함수 호출과 반환을 처리합니다.

call 명령어

  • 기능: call 명령어는 주어진 레이블 또는 메모리 주소로 점프합니다.
  • 사용 방법: call 레이블_이름 또는 call *메모리_주소
  • 동작 방식: 현재의 명령 포인터가 가리키는 다음 명령의 주소를 스택에 푸시한 다음, 주어진 레이블 또는 메모리 주소로 점프합니다.

ret 명령어

  • 기능: 함수에서 반환할 때 사용합니다.
  • 동작 방식: 스택의 맨 위에 있는 주소를 팝하여 명령 포인터에 저장합니다. 이렇게 하면 call 명령어가 호출되기 전 상태로 돌아갑니다.

함수 호출과 스택 프레임

함수가 호출될 때 일반적으로 스택 프레임이 생성됩니다. 이 프레임은 함수의 로컬 변수, 매개변수, 반환 주소 등을 저장합니다. x86_64 아키텍처에서는 %rbp (Base Pointer)와 %rsp (Stack Pointer) 레지스터를 사용하여 현재 함수의 스택 프레임을 관리합니다.

예제: 어셈블리 코드에서의 함수 호출

section .text
    global _start

_start:
    call my_function
    ; 여기에 'my_function' 반환 후 실행될 코드
    ; ...

my_function:
    pushq %rbp          ; 이전 함수의 Base Pointer 저장
    movq  %rsp, %rbp    ; 현재 함수의 Base Pointer 설정
    ; 여기에 함수 로직 작성
    ; ...
    popq  %rbp          ; 이전 함수의 Base Pointer 복원
    ret                 ; 함수에서 반환

이렇게 어셈블리어에서도 고수준 언어처럼 함수를 정의하고 호출할 수 있습니다. 이는 코드의 재사용성과 구조화에 큰 도움이 됩니다.

스택

  • %rsp 레지스터는 스택 포인터를 저장합니다. pushqpopq 명령어를 이용해 스택에 데이터를 삽입하거나 제거할 수 있습니다.

0개의 댓글