명령어 집합 구조(Instruction Set Architecture, ISA)마다 각기 다른 어셈블리어를 가지고 있음.
강의는 x86-64 아키텍처 기준으로 진행.
명령어 기본 구조: 명령어+피연산자
피연산자는 0개~3개일 수 있음.

피연산자의 종류
- 상수
- 레지스터
- 메모리 - [] 안에 쓰여서 표현됨. 괄호 앞에 크기 지정자 TYPE PTR이 올 수 있음. 종류는 BYTE PTR(1바이트), WORD PTR(2바이트), DWORD PTR(4바이트), QWORD PTR(8바이트).


피연산자 표기법

산술 연산
- add <destination>, <source>: dst=dst+src (피연산자: 레지스터, 상수, 메모리)
- sub <destination>, <source>: dst=dst-src (피연산자: 레지스터, 상수, 메모리)
- mul A: 적절한 레지스터 = rax 레지스터 값 * A (피연산자: 레지스터, 메모리 주소)
mul 명령어는 부호 없는 곱셈을 한 후, 알맞은 레지스터에 (필요한 경우 쪼개서) 결과를 저장한다.
8비트(AL 값) x 8비트 = 16비트 -> AX에 저장
16비트(AX 값) x 16비트 = 32비트 -> 상위 16비트는 DX, 하위 16비트는 AX에 저장
32비트(EAX 값) x 32비트 = 64비트 -> 상위 32비트는 EDX, 하위 32비트는 EAX에 저장
64비트(RAX 값) x 64비트 = 128비트 -> 상위 64비트는 RDX, 하위 64비트는 RAX에 저장

- imul A: mul과 똑같이 연산 후 저장, 부호 있는 정수에 대해 곱셈
- imul <dst>, <src>: dst=dst*src
- imul <dst>, <src>, imme: A=B*imme
이때 연산 결과가 destination 크기를 초과하여 데이터 손실이 발생하면, 캐리 플래그(CF)와 오버플로우 플래그(OF)를 1로 설정하여 이를 알림.
- div/idiv 제수(나누는 수)
피제수의 크기에 따라 AX, DX:AX, EDX:EAX, RDX:RAX에 저장 -> 나눗셈 연산 -> 몫과 나머지를 크기에 따라 AL:AH, AX:DX, RAX:RDX에 저장
- idiv는 부호 있는 정수 나눗셈
- inc A: 레지스터/메모리에 저장된 값을 1씩 증가시킴
- dec A: 레지스터/메모리에 저장된 값을 1씩 감소시킴
논리연산
- AND A, B: 두 개의 비트가 모두 1일 때만 결과가 1 (* 특정 비트를 선택적으로 유지하는 비트 마스킹에 사용)
- OR A, B: 하나 이상의 비트가 1이면 결과가 1 (* 특정 비트를 설정하는 데 사용)
- XOR A, B: 두 개의 비트가 다르면 결과가 1 (* 비트 토클 및 암호화에 사용)
- NOT A: 비트 반전
데이터 이동 연산
- mov <destination>, <source>: source(레지스터/메모리/상수)값을 destination(레지스터/메모리)에 대입
- lea <destination>, <source>: source(메모리 피연산자)의 메모리 주소를 계산해서 destination 레지스터에 유효 주소 저장

비교 연산
- cmp <destination>, <source> -> 두 값의 차를 계산하여 여러 플래그를 다르게 설정
ZF(Zero Flag): source-destination=0이면 1
SF(Sign Flag): source-destination>0이면 1
OF(Overflow): 오버플로우 발생 시 1 - test <destination>, <source> -> AND 연산 후 결과는 버리고 플래그 레지스터만 갱신, 특정 비트/레지스터가 0인지 확인하기 위해 사용
ZF=1이면 두 피연산자 중 적어도 하나가 0임을 알 수 있음
분기문
- jmp <label>: 무조건 지정된 주소로 점프, 특정 주소로 rip을 바꿈
- je/jz <label>: Jump if equal/Jump if zero, ZF=1이면 점프, 직전에 비교한 두 피연산자가 같으면 점프 (==)
- jne/jnz <label>: Jump if Not Euqal/Jump if Not zero, ZF=0이면 점프, 직전에 피교한 두 피연산자가 다르면 점프 (!=)
- jg <label>: Jump if Greater, (ZF==0) && (SF==OF)면 점프, 직전에 비교한 두 피연산자 중 전자가 더 크면 점프 (>)
- jge <label>: Jump if Greater or Equal, SF==OF면 점프, 직전에 비교한 두 피연산자 중 전자가 더 크거나 같으면 점프 (>=)
- jl <label>: Jump if Less, SF!=OF면 점프, 직전에 비교한 두 피연산자 중 전자가 더 작으면 점프 (<)
- jle <label>: Jump if Less or Equal, (ZF==1) || (SF!=OF)면 점프, 직전에 비교한 두 피연산자 중 전자가 더 작거나 같으면 점프 (<=)
반복문
비교 연산과 분기문을 조합하여 만듦
1. 카운터 기반 반복문 (C의 for문)
mov rcx, 10 ; 반복 횟수 설정
loop_start:
; 반복할 코드
loop loop_start ; RCX를 감소시키며 0이 아닐 때까지 반복
2. 조건 기반 반복문 (C의 while문)
check_condition:
cmp rax, 50 ; 특정 조건 확인
jge loop_exit ; 조건이 만족되면 종료
; 반복할 코드
jmp check_condition ; 다시 비교 연산으로 이동
loop_exit:
3. 후조건 검사 반복문 (C의 do-while문)
do_loop:
; 반복할 코드 실행
cmp rax, 10 ; 조건 검사
jl do_loop ; 조건이 만족하면 다시 실행반복문에서 주의할 점
함수 (=서브루틴=프로시저)
어셈블리어에서 프로그램이 처리해야 할 명령어들을 한 덩어리로 모아 높은 코드 블록. Label로 특정 구역을 표시하고 call 명령어 등을 활용해 함수를 사용할 수 있음.
함수를 호출하는 함수 caller, 호출된 함수 callee.
Caller는 call 명령어로 callee를 호출, callee는 ret 명령어로 다시 caller에서 실행 중이던 코드로 돌아감.
스택 관련 명령어
스택 자료 구조: LIFO (Last in, First out, 후입선출)
x86 아키텍처에서 스택은 높은 메모리 주소에서 낮은 메모리 주소로 자람. 즉, 데이터 추가하면 메모리 주소 감소, 데이터 제거하면 메모리 주소 증가.
- rsp: 스택 포인터 레지스터, 항상 스택의 top을 가리킴 -> push하면 rsp 감소, pop하면 rsp 증가
- push val: 값 val를 스택에 저장함, 내부적으로는 rsp-=8, [rsp]=A 연산 수행됨 (rsp 한 칸 내리고, 그 위치에 값 저장)
- pop reg: 스택의 top 값을 꺼내서 reg 레지스터에 대입, 내부적으로는 reg=[rsp], rsp+=8 연산 수행됨 (rsp 값=top 값을 다른 곳에 저장한 뒤 rsp 한 칸 올려서 스택 줄이기)
함수 호출 및 반환 관련 명령어
호출: 함수를 부르는 행위
반환: 함수에서 돌아오는 것

- call addr: 주소 값 addr에 위치한 함수 호출
- push return_address ; call 다음 명령어를 스택에 저장, 함수 끝나고 반환할 때 돌아올 주소
- jmp addr ; 해당 함수의 시작 주소인 addr로 rip(instruction 포인터 레지스터) 옮김 - leave: 함수가 반환되기 전, 스택 프레임 정리
- mov rsp, rbp: rbp값을 rsp(top의 주소)에 저장, 그러면 rsp가 rbp까지 한 번에 끌어올려지면서 함수 실행 중 사용된 모든 스택이 정리됨, 그러면 rbp, rsp가 스택에서 같은 위치를 가리키고 있음
- pop rbp: top값을 rbp에 저장, 이때 이 값은 이전 함수의 rbp 주소, rsp는 한 칸 올라감 - ret: rip 레지스터 값을 return address로 변경
- pop rip: top에 있던 return address 값이 rip에 저장되어 rip(intruction 포인터 레지스터)가 return address를 가리키게 됨
어셈블리어에서의 함수 선언
- 함수 시작 위치를 나타낼 라벨(label) 정의
- 스택 프레임이 필요한 경우, 함수 프롤로그를 통해 스택 프레임 구성
- 함수 내부에서 실제 동작 구현
- 함수 마지막에 함수 에필로그를 통해 스택 프레임 해제, ret 명령어로 종료
x86
add:
push ebp ; 이전 함수의 ebp 스택에 저장 (함수 프롤로그)
mov ebp, esp ; 현재 esp가 가리키는 곳을 ebp가 가리키게 해서(둘 다 포인터 레지스터니까) 새로운 스택 프레임 시작점 만듦 (함수 프롤로그)
mov eax, [ebp + 8] ; ebp+8에 첫 번째 인자 X 있음, eax=X
add eax, [ebp + 12] ; ebp+12에 두 번째 인자 Y 있음, eax=X+Y
leave ; (함수 에필로그)
ret ; (함수 에필로그)
- cdecl 함수 호출 규약을 통해 함수를 선언
- cdecl 호출 규약에서는 함수 인자를 스택으로 전달
x64
add: push rbp ; 이전 함수의 rbp 스택에 저장 (함수 프롤로그)
mov rbp, rsp ; 현재 rsp가 가리키는 곳을 rbp가 가리키게 해서(둘 다 포인터 레지스터니까) 새로운 스택 프레임 시작점 만듦 (함수 프롤로그)
mov rax, rdi ; rdi에 저장된 첫 번째 인자 X를 rax에 저장, rax=X
add rax, rsi ; rsi에 저장된 두 번째 인자 Y, rax=X+Y
pop rbp ; (함수 에필로그)
ret ; (함수 에필로그)
- SYSV 함수 호출 규약을 통해 함수를 선언
- 인자를 주로 레지스터로 주고받음
함수 호출 과정
x86
- 32비트 환경에서 cdecl 함수 호출 규약을 사용할 경우, 인자를 스택에 push한 후(컴파일러가 call 명령어 전에 알아서 넣어 줌) call 명령어로 함수 호출
- 함수가 끝난 뒤 caller가 스택 정리해 줌
x64
- 64비트 리눅스에서, System V ADM64 ABI 함수 호출 규약을 주로 사용
- 대부분의 정수 인자가 레지스터를 통해 전달됨
- 첫 번째 인자는 rdi, 두 번재 인자는 rsi에 들어감
Opcode: 시스템 콜
- 커널 모드: 운영 체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한, 모든 메모리/하드웨어 직접 접근 가능
- 사용자 모드: 운영체제가 사용자에게 부여하는 권한, 메모리 접근 제한, 하드웨어 직접 접근 불가
- 시스템 콜: 유저 모드가 어떤 동작을 하고 싶을 때, 커널 모드의 시스템 소프트웨어에게 보내는 요청
시스템 콜 사용
x86
- int 0x80 명령어를 사용하여 시스템 콜 호출
- eax 레지스터에 호출하고자 하는 시스템 콜의 번호를 저장
- 나머지 인자들은 ebx, ecx, edx, esi, edi, ebp 등에 순서대로 저장
- 이후 시스템 콜 반환값은 eax 레지스터에 저장
x64
- syscall 명령어를 사용하여 시스템 콜 호출
- rax 레지스터에 호출하고자 하는 시스템 콜의 번호를 저장
- 나머지 인자들은 rdi, rsi, rdx, r10, r8, r9, 스택 순서대로 저장
- 이후 시스템 콜 반환값은 rax 레지스터에 저장