어셈블리어는 기본적으로 레지스터, 메모리, 상수 등을 직접 조작하는 저수준 언어입니다. 여기에서는 x86_64 아키텍처 기반의 어셈블리어 기본 문법에 대해 설명하겠습니다.
; 또는 # 으로 시작하는 라인은 주석입니다.; 이것은 주석입니다.
# 이것도 주석입니다.
instruction destination, sourcemovq %rax, %rbx ; rax의 값을 rbx에 복사합니다.
x86_64 아키텍처에서는 여러 종류의 데이터 레지스터가 있으며, 각각의 레지스터는 특정한 용도로 사용될 수 있습니다.
일반 목적 레지스터 (General-Purpose Registers)
%rax, %rbx, %rcx, %rdx, %rsi, %rdi, %r8 ~ %r15스택 포인터 레지스터 (Stack Pointer Register)
%rsppush, 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 명령어를 이용해 스택에 데이터를 삽입하거나 제거할 수 있습니다.