본문 바로가기
일반IT/IT보안

[이론] C언어 vs 어셈블리어, 완벽 매핑 가이드! 🧩 (리버싱의 기초 체력 다지기)

by gasbugs 2025. 12. 12.

안녕하세요, 보안 꿈나무 여러분! 🌱

악성코드 분석이나 리버스 엔지니어링을 공부하다 보면 필연적으로 마주치는 거대한 장벽이 있습니다. 바로 어셈블리어(Assembly Language) 입니다.

MOV, LEA, CMP, JE... 알 수 없는 영어 약어들이 줄줄이 나열된 검은 화면을 보면 "나는 누구? 여긴 어디?"라는 생각이 절로 들죠. 😵‍💫

하지만 겁먹지 마세요! 어셈블리어는 외계어가 아닙니다. 우리가 흔히 쓰는 C언어(고수준 언어)가 옷만 갈아입은 것일 뿐입니다. 컴파일러는 정해진 규칙(Pattern)에 따라 C언어를 기계어로 번역하기 때문에, 이 '변환 규칙(Mapping)'만 알면 어셈블리어를 술술 읽을 수 있습니다.

오늘은 리버스 엔지니어링의 핵심, C언어와 어셈블리어의 1:1 매핑 구조를 완벽하게 파헤쳐 보겠습니다. 이 원리를 알면 디컴파일러 없이도 코드의 흐름이 보이기 시작할 거예요! 🚀


1. 변수의 할당 (Variable Assignment) 📥

C언어에서 변수에 값을 넣는 행위는 어셈블리에서 데이터 이동(Move)으로 표현됩니다.

📌 C언어

int a = 10;
int b = a;

⚙️ 어셈블리어 (Assembly)

MOV [RBP-4], 0xA    ; 1. 메모리(변수 a의 위치)에 10(0xA)을 넣는다.
MOV EAX, [RBP-4]    ; 2. 변수 a의 값을 레지스터(EAX)로 가져온다.
MOV [RBP-8], EAX    ; 3. 레지스터 값을 메모리(변수 b의 위치)에 넣는다.
  • 해설:
    • CPU는 메모리에서 메모리로 직접 데이터를 옮길 수 없습니다. 그래서 EAX 같은 레지스터(임시 저장소)를 거쳐서 이동합니다.
    • [RBP-4] 같은 형태는 스택 메모리 주소(지역 변수)를 의미합니다.
    • 핵심: MOV A, B는 A = B라고 생각하면 됩니다.

2. 산술 연산 (Arithmetic Operations) 🧮

더하기, 빼기 같은 연산은 매우 직관적입니다.

📌 C언어

a = a + 5;
a++;

⚙️ 어셈블리어 (Assembly)

ADD [RBP-4], 5      ; a = a + 5 (ADD: 더하기)
INC [RBP-4]         ; a++ (INC: 1 증가)
  • 해설:
    • ADD: 더하기
    • SUB: 빼기
    • INC: 1 증가 (Increment)
    • DEC: 1 감소 (Decrement)
    • IMUL / IDIV: 곱하기 / 나누기

3. 조건문 (If-Else) ⚖️

프로그램의 흐름을 바꾸는 분기문입니다. 어셈블리에서는 비교(CMP)점프(JUMP) 두 단계로 이루어집니다.

📌 C언어

if (a == 10) {
    // True Block
    b = 1;
} else {
    // False Block
    b = 0;
}

⚙️ 어셈블리어 (Assembly)

CMP [RBP-4], 0xA    ; 1. a와 10을 비교한다. (실제로는 a - 10을 해봄)
JNE 0x401050        ; 2. 결과가 같지 않다면(Not Equal), Else 블록(0x401050)으로 점프!

; --- True Block ---
MOV [RBP-8], 1      ; b = 1
JMP 0x401060        ; Else 블록을 건너뛰고 탈출!

; --- False Block (0x401050) ---
MOV [RBP-8], 0      ; b = 0
  • 해설:
    • CMP A, B: A와 B를 비교합니다. (내부적으로 A - B를 수행하여 0이 나오면 같다고 판단)
    • JE (Jump Equal): 같으면 점프
    • JNE (Jump Not Equal): 다르면 점프
    • JG / JL: 크면(Greater) / 작으면(Less) 점프
    • 고수준 언어의 { } 블록 구조가 어셈블리에서는 GOTO (점프) 형태로 바뀐다는 점을 꼭 기억하세요!

4. 반복문 (Loop: For / While) 🔄

반복문은 조건문과 점프의 조합입니다. 코드가 아래로 내려갔다가 다시 위로 올라가는 순환 구조를 가집니다.

📌 C언어

for (int i = 0; i < 10; i++) {
    sum += i;
}

⚙️ 어셈블리어 (Assembly)

MOV [RBP-4], 0      ; i = 0 (초기화)
JMP Check_Condition ; 조건 확인하러 가기

Loop_Start:         ; 루프 시작 지점
MOV EAX, [RBP-4]    ; i 값을 가져옴
ADD [RBP-8], EAX    ; sum += i
INC [RBP-4]         ; i++ (증감)

Check_Condition:    ; 조건 확인 지점
CMP [RBP-4], 0xA    ; i와 10 비교
JL Loop_Start       ; i < 10 (작으면) Loop_Start로 다시 점프!
  • 해설:
    • C언어의 for문은 초기화 -> 조건비교 -> 본문 -> 증감 -> 조건비교 순서로 해체됩니다.
    • 어셈블리 코드에서 화살표가 위쪽 방향으로 올라가는 JMP나 Jcc가 보이면 "아, 여기 반복문이구나!"라고 생각하면 됩니다.

5. 함수 호출 (Function Call) 📞

리버싱에서 가장 중요한 함수 호출 규약(Calling Convention)입니다. 함수를 부르기 전에 파라미터(인자)를 준비하고, 함수가 끝나면 리턴값을 받아옵니다.

📌 C언어

int result = Add(10, 20);

⚙️ 어셈블리어 (Assembly - x64 기준)

MOV EDX, 20         ; 2번째 인자(20)를 EDX(또는 RSI) 레지스터에 넣음
MOV ECX, 10         ; 1번째 인자(10)를 ECX(또는 RDI) 레지스터에 넣음
CALL 0x401200       ; Add 함수 호출 (스택에 복귀 주소 저장하고 이동)
MOV [RBP-4], EAX    ; 결과값(EAX)을 result 변수에 저장
  • 해설:
    • 파라미터 전달: x64 환경에서는 주로 레지스터(RDI, RSI, RDX, RCX...) 순서로 인자를 전달합니다. (x86은 스택 PUSH 사용)
    • CALL: 함수가 있는 주소로 점프합니다. 이때, 함수가 끝나고 돌아올 주소를 스택에 몰래 저장합니다.
    • 리턴값(RAX/EAX): 함수가 실행되고 난 결과값은 항상 EAX(또는 RAX) 레지스터에 담겨 있습니다. 그래서 CALL 직후에 EAX를 확인하면 리턴값을 알 수 있습니다.

💡 꿀팁: AI와 함께 공부하기

오늘 배운 패턴들이 눈에 익지 않아도 괜찮습니다. 우리에겐 AI가 있으니까요!

실습하시다가 모르는 어셈블리 코드가 나오면 이렇게 물어보세요.

 

"이 어셈블리 코드를 C언어로 변환(Decompile)해주고, 어떤 구조(if, for 등)인지 설명해줘!"

 

 

AI는 방금 우리가 배운 이 매핑 규칙을 바탕으로 코드를 해석해 줄 것입니다. 하지만 이론을 알고 결과를 보는 것과, 모르고 보는 것은 천지 차이라는 점, 잊지 마세요! 😉

마치며 🎁

C언어와 어셈블리어는 '동전의 양면'과 같습니다.

한쪽 면(어셈블리)만 보고도 반대쪽 면(C언어)을 상상할 수 있는 능력이 생긴다면, 여러분은 이미 중급 분석가로 가는 티켓을 거머쥔 것입니다.

다음 시간에는 이 이론을 바탕으로, AI에게 "어셈블리 코드를 줄 테니 Python 코드로 재구현해줘"라고 시키는 마법 같은 실습을 진행하겠습니다.

오늘도 즐거운 리버싱 되세요! 화이팅! 💪