어셈블리어는 기본적으로 레지스터, 메모리, 상수 등을 직접 조작하는 저수준 언어입니다. 여기에서는 x86_64 아키텍처 기반의 어셈블리어 기본 문법에 대해 설명하겠습니다.
;
또는 #
으로 시작하는 라인은 주석입니다.; 이것은 주석입니다.
# 이것도 주석입니다.
instruction destination, source
movq %rax, %rbx ; rax의 값을 rbx에 복사합니다.
x86_64 아키텍처에서는 여러 종류의 데이터 레지스터가 있으며, 각각의 레지스터는 특정한 용도로 사용될 수 있습니다.
일반 목적 레지스터 (General-Purpose Registers)
%rax
, %rbx
, %rcx
, %rdx
, %rsi
, %rdi
, %r8
~ %r15
스택 포인터 레지스터 (Stack Pointer Register)
%rsp
push
, pop
, call
, ret
등의 명령어에서 자동으로 조정됩니다.베이스 포인터 레지스터 (Base Pointer Register)
%rbp
인스트럭션 포인터 레지스터 (Instruction Pointer Register)
%rip
상태 레지스터 (Status Register)
%rflags
세그먼트 레지스터 (Segment Registers)
%cs
, %ds
, %es
, %fs
, %gs
, %ss
이 레지스터들은 어셈블리어 프로그래밍에서 중요한 역할을 하며, 특히 함수 호출, 데이터 처리, 조건 분기 등 다양한 상황에서 활용됩니다. 이해를 돕기 위해 각 레지스터의 일반적인 역할을 간략하게 설명했지만, 실제로는 더 복잡한 명령어와 상황에서 다양하게 사용됩니다.
(%rax)
와 같이 괄호 안에 레지스터 이름을 넣으면 해당 레지스터에 저장된 주소의 메모리를 가리킵니다.movq $42, (%rax) ; rax가 가리키는 메모리 위치에 42를 저장합니다.
$
기호를 이용해 상수를 표현할 수 있습니다.movq $42, %rax ; rax 레지스터에 42를 저장합니다.
x86_64 어셈블리어에서 사용되는 다양한 연산자에 대해 설명하겠습니다. 이 연산자들은 산술, 논리, 비트, 비교, 분기 등 다양한 카테고리로 나뉩니다.
addq src, dest
: dest = dest + srcsubq src, dest
: dest = dest - srcmulq src
: RDX:RAX = RAX * srcdivq src
: RAX = RDX:RAX / src, RDX = RDX:RAX % srcimulq src, dest
: dest = dest * src (signed multiplication)idivq src
: Similar to divq
, but for signed divisionandq src, dest
: dest = dest & srcorq src, dest
: dest = dest | srcxorq src, dest
: dest = dest ^ srcnotq src
: src = ~srcshlq n, dest
: dest = dest << nshrq n, dest
: dest = dest >> nsalq n, dest
: dest = dest << n (Arithmetic shift left)sarq n, dest
: dest = dest >> n (Arithmetic shift right)cmpq src, dest
: 비교 후 결과에 따라 플래그 설정 (dest - src)testq src, dest
: 논리곱 후 결과에 따라 플래그 설정 (dest & src)jmp label
: Unconditional jump to labelje label
: Jump if equal (ZF=1)jne label
: Jump if not equal (ZF=0)jl label
: Jump if less (SF != OF)jle label
: Jump if less or equal (ZF=1 or SF != OF)jg label
: Jump if greater (ZF=0 and SF=OF)jge label
: Jump if greater or equal (SF=OF)이 외에도 lea
(Load Effective Address), nop
(No Operation), int
(Interrupt), syscall
(System Call) 등 여러 다른 연산자와 명령어가 있습니다. 이 연산자들은 코드에 따라 다양하게 조합되어 복잡한 연산과 프로시저를 수행합니다.
분기(Branching)와 루프(Looping)는 어셈블리어 또는 고수준 프로그래밍 언어에서 프로그램 흐름을 제어하는 두 가지 기본적인 메커니즘입니다.
분기는 특정 조건에 따라 프로그램의 실행 흐름을 변경합니다. x86_64 어셈블리에서는 일반적으로 cmp
명령어로 두 값을 비교한 뒤, 조건에 따라 분기를 수행하는 j*
명령어를 사용합니다.
cmpq %rax, %rbx ; RAX와 RBX 비교
je equal ; 만약 RAX = RBX이면 'equal' 레이블로 분기
jne not_equal ; 만약 RAX ≠ RBX이면 'not_equal' 레이블로 분기
루프는 특정 조건이 만족되는 동안 코드 블록을 반복적으로 실행합니다. 어셈블리에서는 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) {
// 코드
}
분기와 루프는 프로그램 로직을 구성하는 중요한 빌딩 블록입니다. 효과적으로 활용하면 복잡한 프로그램도 구현할 수 있습니다.
함수 호출은 프로그램 실행 중에 특정 코드 블록(함수)을 실행하기 위해 사용되는 매커니즘입니다. 어셈블리어에서는 일반적으로 call
과 ret
명령어를 사용하여 함수 호출과 반환을 처리합니다.
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
레지스터는 스택 포인터를 저장합니다. pushq
와 popq
명령어를 이용해 스택에 데이터를 삽입하거나 제거할 수 있습니다.