어셈블러는 어셈블리어를 기계어로 치환해주는 프로그램이다
이와 반대로 리버스 엔지니어링으로 개발된 역어셈블러는 기계어를 어셈블리어로 역치환해준다
일반적으로, 아키텍처별로 문법이 상이하다
아키텍처가 같더라도 어셈블러 내 정의에 따라 문법이 다를 수도 있다

대표적인 아키텍처인 Intel사의 x86 기준으로 NASM(Netwide Assembler)의 문법을 간단히 정리해보자
외운다기 보다는 이후 실습을 하면서 자연스럽게 체화를 하면 된다
비교적 익숙한 C언어를 어셈블리어로 변환해주는 사이트가 있다
아래 사이트를 활용하면, 어셈블러 문법을 C언어와 비교해가며 이해하는 데 도움이 된다
Compiler Explorer
gcc.godbolt.org
명령어 종류
NASM의 대략적인 명령어 종류는 다음과 같다
| 데이터 이동 | mov, lea |
| 산술 연산 | inc, dec, add, sub, mul, imul |
| 논리 연산 | and, or, xor, not |
| 비교 | cmp, test |
| 분기 | jmp, je, jg |
| 스택 | push, pop |
| 프로시져 | call, ret, leave |
| 시스템 콜 | syscall |
피연산자
피연산자 종류에는 상수, 레지스터 그리고 메모리 주소가 있다
- 즉시값(Immediate value) : 코드 자체에 직접 포함된 고정된 상수값 (0x1242, 0xffff 등)
- 레지스터 : 레지스터 (rax, rbx 등) 안에 들어있는 값
- 메모리 주소 : 메모리 주소값, 레지스터에 저장된 메모리 주소값,
(C언어 포인터 느낌이다. 레지스터 -> 메모리 -> 참조값)
메모리 주소의 경우에는 [ ] (대괄호) 안에 넣어서 표현한다
그리고 대괄호 앞에 참조 메모리의 크기를 지정하는 PTR이 올 수 있다
필수 사항은 아니지만 오류 방지차원에서 명확한 크기를 명시하는 편이 좋다
mov [rdi], eax ; (X) 오류 발생 가능: 몇 바이트를 가져올 것인지 크기 지정이 명확하지 않음
mov DWORD PTR [rdi], eax ; (O) 명확한 크기 지정 (0x8048000 위치의 메모리를 4byte 참조)
데이터 이동
데이터 이동 연산은 크게 두가지로 나뉜다
직접 값을 복사하여 이동시키는 방식(mov) 과 메모리 주소를 가져오는 방식(lea) 이 있다
대부분의 명령어 연산 문법은 [명령어] [목적지 피연산자] [소스 피연산자] 구조를 이룬다
데이터 이동 연산도 위 구조를 따른다
mov
mov연산은 메모리에 직접 접근하여 담긴 값 자체를 복사하여 이동시킨다
int val = arr[1]
C언어로 표현하면 위와 같다
위 코드는 배열의 첫번째 메모리에 직접 접근해서 해당 값을 int 변수에 저장한다
어셈블리어의 mov연산의 원리도 동일하다
mov rbx, &arr; rbx = arr의 주소
mov rcx, 2; 배열의 인덱스
mov rax, [rbx + rcx * 4]; arr[2]에 직접 접근하여 갑을 rax에 할당한다
lea
lea연산은 메모리에 접근하지 않고 메모리 주소를 가져오는 방식이다
즉, 유효주소 EA(Effective Address)를 저장하는 것이다
C언어의 포인터 개념이라고 생각하면 된다
int *ptr = &arr[1];
어셈블리어로 표현하면 다음과 같다
mov rbx, &arr; rbx = arr의 주소
mov rcx, 1; 배열의 인덱스
lea rax, [rbx + rcx * 4]; arr[1]의 주소를 rax에 대입한다
산술연산
add(덧셈), sub(뺄셈) 연산도 기본 연산 구조 [명령어] [목적지 피연산자] [소스 피연산자]를 그대로 따른다
add eax, 1; eax값에 1을 더한다
sub ecx, edx; ecx에서 dex를 뺀다
곱셈은 조금 다르다
우선, 피연산자를 레지스터에 저장하고 곱셈을 수행할 때 해당 값을 꺼내서 사용한다
(주로 AL, AX, EAX, RAX 레지스터를 사용한다)
피연산자의 크기에 따라 연산 결과의 크기가 다르므로, 결과값이 저장되는 레지스터가 다르다
(e.g., EAX * ESI (32bit * 32bit) 연산은 결과값이 최대 64bit이므로 상위 32bit, 하위 32bit 나누어야 한다)
그리고, 부호가 없는 mul 연산과 부호가 있는 imul 연산으로 나뉜다
mul
8bit 연산
- AL 레지스터의 값과 src 피연산자의 값 곱하기
- 두 숫자의 연산 결과는 최대 16비트 이기때문에, AX (16비트) 레지스터에 보관
section .data
num1 db 5
num2 db 3
section .text
global _start
_start:
mov al, [num1] ; AL 레지스터에 첫 번째 피연산자(5)를 로드
mul byte [num2] ; AL과 num2(3)를 곱함. 결과 15(0x0F)가 AX에 저장
; 프로그램 종료
mov eax, 1
xor ebx, ebx
int 0x80
16bit 연산
- AX 레지스터의 값과 src 피연산자의 값 곱하기
- 곱한 결과의 상위 16비트는 DX 레지스터, 하위 16비트는 AX 레지스터에 보관
mov ax, 0x2000
mov bx, 0x0100
mul bx ; DX:AX = AX * BX = 0x200000. DX=0x0002, AX=0x0000
32bit 연산
- AX 레지스터의 값과, src 피연산자의 값 곱하기
- 곱한 결과의 상위 32비트는 EDX 레지스터, 하위 32비트는 EAX 레지스터에 보관
mov eax, 1000
mov ebx, 50
mul ebx ; EDX:EAX = EAX * EBX. EAX = 50000, EDX = 0
64bit 연산
- RAX 레지스터의 값과 src 피연산자의 값을 곱하기
- 곱한 결과의 상위 64비트는 RDX 레지스터, 하위 64비트는 RAX 레지스터에 보관
section .data
num1 dq 20000000000 ; 64비트 정수, 십진수 20,000,000,000
num2 dq 3 ; 64비트 정수, 십진수 3
section .text
global _start
_start:
; 첫 번째 피연산자를 rax에 로드
mov rax, [num1] ; rax = 20,000,000,000
; 두 번째 피연산자를 rbx에 로드
mov rbx, [num2] ; rbx = 3
; rax와 rbx의 값을 곱함 (unsigned)
; 결과는 rdx(상위 64비트):rax(하위 64비트)에 저장
mul rbx ; rdx:rax = 60,000,000,000
; 프로그램 종료 (Linux)
mov rax, 60 ; sys_exit 호출
mov rdi, 0 ; 종료 코드
syscall
imul (signed 연산)
imul은 부호가 있는 signed 정수에 대한 곱셈을 수행한다
mul과는 다르게 피연산자가 최대 3개까지 올 수 있다
imul <source>; mul 연산과 유사하게 동작
imul <destination>, <source>; 두 피연산자를 곱한 결과를 첫번째 연산자에 저장
imul <destination>, <source>, <immediate>; 두번째, 세번째 피연산자를 곱한 결과를 첫번째 피연산자에 저장
mov ax, 10 ; AX 레지스터에 10을 저장한다
mov bx, 5 ; BX 레지스터에 5를 저장한다
imul ax, bx, 7 ; AX는 BX * 7의 결과를 저장한다
; AX에는 5 * 7 = 35 (0023h)가 저장된다
만약 연산 결과가 목적지 피연산자의 크기를 초과한다면 CF(Carry Flag)와 OF(Overflow Flag)를 1로 설정한다
(이상 없을 시에는 0)
INC, DEC
inc와 dec는 피연산자의 값을 1씩 증가/감소 시키는 연산자이다
mov ax, 10;
inc ax; ax레지스터의 값이 1 증가한다 (11)
mov bx, 10;
dec bx; bx레지스터의 값이 1 감소한다 (9)
논리연산
논리연산자에는 and, or, xor, not 이 있다
각각 bit 논리연산을 수행한다
| 연산자 | 결과값 | 용도 |
| and | 두 bit가 모두 1이면 1, 아니면 0 | 비트마스킹 |
| or | 두 bit중 하나라도 1이 있으면 1 둘다 0이면 0 |
특정 비트 set |
| xor | 두 bit가 다르면 1 두 bit가 같으면 0 |
비트 토글 및 암호화 |
| not | 비트반전 | Complement 연산 |
section .data
val1 db 0xFF ; 이진수로 1111 1111
val2 db 0x0F ; 이진수로 0000 1111
result db 0
section .text
global _start
_start:
mov al, [val1]
xor al, [val2] ; al과 val2의 값을 XOR 연산하여 al에 저장한다 (결과: 0xF0, 이진수 1111 0000)
mov [result], al
비트 기본 연산을 수행하는 연산자이므로 까다로운 부분은 없다
논리연산까지 정리하고 나니 분량이 길어져서 우선 여기서 끊고
다음글에서 남은 연산(비교연산, 분기연산 등) 들을 정리해야겠다
참고자료
- https://en.wikipedia.org/wiki/X86_assembly_language
x86 assembly language - Wikipedia
From Wikipedia, the free encyclopedia Family of backward-compatible assembly languages x86 assembly language is a family of low-level programming languages that are used to produce object code for the x86 class of processors. These languages provide backwa
en.wikipedia.org
- https://docs.oracle.com/cd/E19641-01/802-1948/802-1948.pdf
- https://www.nasm.us/docs/3.01/
NASM - The Netwide Assembler
www.nasm.us
'ComputerScience & Embedded > Pwnable' 카테고리의 다른 글
| Linux Memory Layout (0) | 2025.10.26 |
|---|