[Rev] 리버싱 · 작성: 2026-07-19 19:32:50 · 조회 4
디컴파일러가 있어도 결국 중요한 분기나 이상하게 최적화된 부분은 어셈블리를 직접 읽어야 확실해진다. 이 글은 리버싱에서 실제로 가장 자주 마주치는 x86-64 명령어 패턴 위주로 정리한다 — 명령어 전체를 외우는 게 목표가 아니라, "이 패턴을 보면 뭘 하려는 코드인지" 감을 잡는 게 목표다.
같은 명령어라도 두 가지 표기법이 있다. 리버싱 도구들은 대부분 Intel 문법을 기본으로 쓴다 (더 읽기 편해서).
AT&T: mov %eax, %ebx ; 방향: 왼쪽 → 오른쪽
Intel: mov ebx, eax ; 방향: 오른쪽 → 왼쪽 (목적지가 먼저 온다)
이 글은 Intel 문법 기준이다.
mov eax, 5 ; eax에 5를 대입
mov eax, [rbp-8] ; rbp-8 주소에 있는 값을 eax로 읽어옴 (지역 변수 읽기)
mov [rbp-8], eax ; eax 값을 rbp-8 주소에 저장 (지역 변수 쓰기)
lea rax, [rbp-8] ; rbp-8 이라는 "주소 자체"를 rax에 저장 (값이 아니라 포인터)
[rbp-8]처럼 대괄호가 있으면 "그 주소에 있는 값"을 가리킨다. lea(Load Effective Address)만 예외적으로 주소 계산 결과 자체를 저장한다.
cmp eax, 0x1337
je success ; eax == 0x1337 이면 success로 점프
test eax, eax
jz fail ; eax == 0 이면 fail로 점프 (NULL/실패 코드 체크에 흔함)
cmp eax, ebx
jg greater ; eax > ebx (signed)
ja above ; eax > ebx (unsigned)
cmp A, B는 A - B를 계산해서 결과 플래그(Zero, Sign, Carry 등)만 세팅하고, 그 다음 조건부 점프(je/jne/jg/jl/ja/jb...)가 그 플래그를 보고 분기한다. "정답 체크" 로직은 거의 항상 이 cmp + 조건분기 조합이라서, crackme를 풀 때 제일 먼저 찾는 패턴이다.
test eax, eax는 cmp eax, 0과 사실상 같은 효과를 내는 관용적인 표현이다.
mov edi, 5 ; 첫 번째 인자
mov esi, 10 ; 두 번째 인자
call add_numbers
; 리턴값은 eax(또는 rax)에 담겨서 돌아옴
x86-64 System V 호출 규약에서 처음 6개 정수형 인자는 RDI, RSI, RDX, RCX, R8, R9 순서로 들어간다. 함수 호출부만 봐도 몇 개의 인자를 넘기는지, 대략 뭘 하는 함수인지 유추할 수 있다.
xor ecx, ecx ; ecx = 0 (카운터 초기화, 흔한 관용구)
loop_start:
cmp ecx, 10
jge loop_end ; ecx >= 10 이면 종료
; ... 반복할 코드 ...
inc ecx
jmp loop_start
loop_end:
for/while 문이 컴파일되면 결국 "비교 + 조건 점프 + 뒤로 점프"의 조합으로 풀어진다. 디컴파일러는 이걸 다시 for문으로 재구성해서 보여주지만, 최적화가 심하면 이 재구성이 부정확할 수 있어서 원본 어셈블리로 검증하는 게 안전하다.
여기까지가 정적으로 코드를 읽는 데 필요한 최소한이다. 실제 실행 흐름(어떤 값이 어떻게 변하는지)을 확인하려면 동적 분석(디버거)이 필요하고, 다음 글에서 다룬다.